引言
官方文档对React事件的介绍包含以下几点
- React事件是合成事件
- 有stopPropagation和preventDefault
- 有nativeEvent上的所有属性
- 可以通过nativeEvent获取到原生事件
- 跨浏览器兼容
那么在看源码之前,有以下疑问:
- 如何监听?监听的什么元素?
- 如何模拟捕获和冒泡?
- 如何实现stopPropagation?
- 为什么要使用合成事件?
React 版本号 16.9.0
源码阅读说明
了解源码最好的方式是单步调试,找一个最简单的例子,在源码中打断点进行调试。本文采用create-react-app创建了最简单的demo,只含有click事件。页面内容如下
import React from 'react';
import './App.css';
class App extends React.Component {
spanClickEvent = null;
headerClickEvent = null;
componentDidMount () {
// document.addEventListener('click', () => {
// console.log('document click');
// })
}
spanClick (event) {
event.stopPropagation();
console.log('spanClick');
console.log(event);
// this.spanClickEvent = event;
}
headerClick (event) {
console.log('headerClick');
console.log(event);
// this.headerClickEvent = event;
// console.log(this.headerClickEvent === this.spanClickEvent);
}
inputChange (event) {
console.log('inputChange');
console.log(event);
}
render () {
return (
<div className="App">
<header className="App-header" onClick={(event) => this.headerClick(event)}>
<div className="btn-wrapper">
<span className="btn" onClick={(event) => this.spanClick(event)}>
<span>点击</span>
</span>
{/* <input onChange={(event) => this.inputChange(event)}/> */}
</div>
</header>
</div>
)};
}
export default App;
事件注册
首先刷新页面,在render过程中会走到如下逻辑
function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
var parentNamespace = void 0;
{
// 此处省略代码...
}
var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
// 将internalInstanceHandle和props挂载在真实的DOM上,后面会用到
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}
updateFiberProps会走到setInitialDOMProperties里面
function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
// 此处省略代码
else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (true && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
// 如果props含有事件相关的属性,则去监听对应的事件
ensureListeningTo(rootContainerElement, propKey);
}
}
}
注意此处使用的registrationNameModules存放了所有React事件。
ensureListeningTo会判断当前是否在iframe里面,以决定监听哪里的事件。最后走到listenTo逻辑里面。
function listenTo(registrationName, mountAt) {
//listeningSet存放了已经监听过的事件,避免重复去监听。
var listeningSet = getListeningSetForElement(mountAt);
var dependencies = registrationNameDependencies[registrationName];
for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
if (!listeningSet.has(dependency)) {
// 初始化span标签的时候会走到这个逻辑里面,header的时候就不会再重复去监听click了
switch (dependency) {
// 此处省略代码
default:
var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(dependency, mountAt);
}
break;
}
listeningSet.add(dependency);
}
}
}
registrationNameDependencies存放了React事件与原生事件需要监听的对应关系。如下图中,如果使用onBlur则会监听window的blur事件,如果使用onChange则会监听blur/change/..等事件
接下来说trapBubbledEvent
function addEventBubbleListener(element, eventType, listener) {
// 注意此处element是document,第三个参数是false
element.addEventListener(eventType, listener, false);
}
此时的listener为dispatchDiscreteEvent
至此,事件注册完成。值得注意的是,React在生成的真实DOM中加入了两个React属性,一个放了元素的props,一个放了元素对应的FiberNode。
原生DOM和FiberNode的一个双向关系。
传中...]
事件触发
点击span元素后,会走到dispatchDiscreteEvent逻辑里面,会带着nativeEvent调dispatchEvent方法。
通过getEventTarget(nativeEvent)
拿到当前的nativeEvent.target
为<span>点击元素</span>
,然后拿到DOM上含有__reactInternalInstance*的最近的元素,此处为<span>点击元素</span>
。调用dispatchEventForPluginEventSystem,调用batchedEventUpdates,中间会调用runExtractedPluginEventsInBatch处理原生事件,将原生事件合成为合成事件。最后会调用到traverseTwoPhase。这个方法主要是找到当前的path链
温习currentTarget和target currentTarget表示事件处理程序当前正在处理事件的那个元素
target 事件的目标
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
var i = void 0;
for (i = path.length; i-- > 0;) {
// 从外层到里层遍历元素,模拟捕获
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
// 从里层到外层遍历元素,模拟冒泡
fn(path[i], 'bubbled', arg);
}
}
调用对应的fn也就是accumulateDirectionalDispatches
function accumulateDirectionalDispatches(inst, phase, event) {
// 省略代码
// 在'bubble'阶段的onClick对应onClick,而captured的onClick对应onClickCaptured。因此我们在捕获阶段没有事件可以触发。感兴趣的可以将demo中的onClick更改为onClickCaptured模拟捕获触发
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
// 依次拿到span.btn和header上的onClick,并且放进event._dispatchListeners
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
最后调用executeDispatchesInOrder,遍历_dispatchListeners依次触发。触发的时候会判断event.isPropagationStopped()是true还是false
function executeDispatch(event, listener, inst) {
var type = event.type || 'unknown-event';
// 赋值给currentTarget
event.currentTarget = getNodeFromInstance(inst);
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}
最后调用到fakeNode.dispatchEvent触发callCallback真正的onClick事件。此处采用fakeNode.dispatchEvent是为了让事件仍然是浏览器发起的。
调用完毕后,会将event初始化为最初的状态
整个过程,可以发现以下问题
- 在事件冒泡阶段,使用的是同一个事件,只不过调用时的currentTarget不一样。即spanClickEvent ==== headerClickEvent
- 因此在处理事件时,一定提前把值取出来。否则像有异步操作的时候,是取不到currentTarget的值的
- onClick对应有onClickCaptured
- 因为整个过程使用的是同一个event,所以stopPropagation在调用的时候会将event上的isPropagationStopped置为true,则后面的事件将不再触发,以此来模拟冒泡停止
总结
回到最初的问题提问
- 如何监听?监听的什么元素?(使用到的根据registrationNameDependencies对应关系才会去监听,且使用一个set避免重复监听。监听了document元素)
- 如何模拟捕获和冒泡?(找到元素的path链,按不同顺序依次取出对应的事件)
- 如何实现stopPropagation?(同一个event,利用其属性stopPropagation是否返回false标记)
- 为什么要使用合成事件?(抹平浏览器差异)
课后题
例子中的span调用了stopPropagation,那么以下代码会触发吗
componentDidMount () {
document.addEventListener('click', () => {
// 依然会触发。为什么?
console.log('document click');
})
window.addEventListener('click', () => {
// 不会触发。为什么?
console.log('document click');
})
}
另一个问题,React是什么时候removeEventListner的?目前的出来的结论是并没有。如下例子,在点击header时会触发React的DispatchEvent没有问题。但是在isShow为false后,点击span元素,仍然会触发DispatchEvent。因此目前的结论是,React并没有去移除无用的EventListner。这个问题欢迎在评论区交流
class App extends React.Component {
constructor () {
super();
this.state = {
isShow: true
};
}
headerClick (event) {
this.setState({
isShow: false
});
}
render () {
return (
<div className="App">
{
this.state.isShow ?
<header className="App-header" onClick={(event) => this.headerClick(event)}>
</header>
: <span>点击</span>
}
</div>
)};
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。