Appearance
阅读前的测试
本节目标:理解“阅读前的测试”的核心思路,并能把它用于实际开发或面试表达。 先看以下问题:
- 点击
button,最后的输出顺序是什么? B,E,F处的type都是啥?
export default class Test extends React.Component {
componentDidMount() {
document.querySelector("#btn").addEventListener("click", (e) => {
console.log("A inner listener");
setTimeout(() => {
console.log("B inner listener timer", e.type);
});
});
document.body.addEventListener("click", (e) => {
console.log("C document listener");
});
window.addEventListener("click", (e) => {
console.log("D window listener");
});
}
outClick(e) {
setTimeout(() => {
console.log("E out timer", e.type);
});
console.log("F out e", e.type);
}
innerClick = (e) => {
console.log("G inner e");
e.stopPropagation();
};
render() {
return (
<div onClick={this.outClick}>
<button id="btn" onClick={this.innerClick}>点我
</button>
</div>
);
}
}
1.最后的输出顺序为
A C G F D B E 2. B,F处的type为click,而E处的type为null 如果错了,不要紧~~咱们往下看吧。
- 如果元素有原生事件处理器,响应之;
- 对于元素上的合成事件,则将其绑定在了
document上; - 事件向外冒泡,父级元素如果有原生事件,则响应之,没有则将父元素上的合成事件绑定到
document之上; - 事件到了
documnet之后,先响应document的原生事件处理器,再依次响应由里到外的合成事件; - 事件冒泡到
window上,先响应原生的,window无法设置react合成事件;
合成事件(SyntheticEvent) 考虑到浏览器的兼容性和性能问题,react拥有自己的事件系统,即合成事件。
react的合成事件不会直接绑定到目标dom节点上,而是利用事件委托绑定到最外层document上;- 用一个统一的监听器去监听,这个监听器上保存着目标节点与事件对象的映射;
- 当你挂载或删除节点时,所对应的事件就会从映射表上绑定或删除。
响应过程(对应第一问) 我们参照上题,详细说一下事件的响应过程:
- 由于我们写的几个监听事件
addEventListener,都没有给第三个参数,默认值为false,所以在事件捕获阶段,原生的监听事件没有响应,react合成事件没有实现事件捕获。所以在捕获阶段没有事件响应。 - 接着到了事件绑定的阶段,
button上挂载了原生事件,于是输出"A",setTimeout中的"B"则进入EVENT LOOP。在上一段中,我们提到react的合成事件是挂载到document上,所以“G”没有输出。 - 之后进入冒泡阶段,到了
div上,与上条同理,不会响应outClick,继续向上冒泡。 - 之后冒泡到了
document上,先响应挂载到document的原生事件,输出"c"。之后接着由里向外响应合成事件队列,即输出"G""F",将'E'放入EVENT LOOP。再向上冒泡。 - 到了
window上,响应‘D’,之后再处理EVENT LOOP上的事件,输出'B' 'E'.
合成事件对象react合成事件是原生事件的扩充,它符合w3c标准。下面是合成事件对象的属性:
| 属性名 | 类型 | 描述 |
bubbles | boolean | 事件是否可冒泡 |
cancelable | boolean | 事件是否可拥有取消的默认动作 |
currentTarget | DOMEventTarget | 事件监听器触发该事件的元素(绑定事件的元素) |
defaultPrevented | boolean | 当前事件是否调用了 event.preventDefault()方法 |
eventPhase | number | 事件传播的所处阶段[0:Event.NONE-没有事件被处理,1:Event.CAPTURING_PHASE - 捕获阶段,2:被目标元素处理,3:冒泡阶段(Event.bubbles为true时才会发生)] |
isTrusted | boolean | 触发是否来自于用户行为,false为脚本触发 |
nativeEvent | DOMEvent | 浏览器原生事件 |
preventDefault() | void | |
isDefaultPrevented() | boolean | 返回的事件对象上是否调用了preventDefault()方法 |
stopPropagation() | void | |
isPropagationStopped() | boolean | 返回的事件对象上是否调用了stopPropagation()方法 |
target | DOMEventTarget | 触发事件的元素 |
timeStamp | number | 事件生成的日期和时间 |
type | string | 当前 Event 对象表示的事件的名称,是注册事件的句柄,如,click、mouseover...etc. |
注意preventDefault()和stopPropagation(),他们只能对react合成事件产生作用,尤其 注意stopPropagation()浏览器原生事件是不会被禁止传播的。比如我们把题目中的注释去掉,会发现“C”依然被输出,原因就在于合成事件其实是通过document上的监听器进行事件委托的(看不懂的同学接着看上一小节。) 事件池(对应第二问) 在react中,合成事件被调用后,合成事件对象会被重用,所有属性被置为null,所以题目中outClick中通过异步方式访问e.type是取不到任何值的,如果需要保留属性,可以调用event.persist()事件,会保留引用。 彩蛋:如果对这里好奇的同学可以在控制台打印一下event,会发现不论是不是异步方法中的event,它的所有属性都是null。但为什么会得到它的属性值呢,大概是因为get方法吧,具体的原因我也没深入。如果了解原理的同学请在评论区留言~~~或者等我之后填坑 源码构成 react事件系统的核心文件renderers/shared/event/EventPluginHub.js,感兴趣的同学可以去看看源码
~~
var EventPluginHub = { injection, putListener, getListener, deleteListener, deleteAllListeners, extractEvents, //当顶层事件被触发,该方法中会传入原生事件,生成合成事件
enqueueEvents,//合成事件进入事件队列
processEventQueue, //调度事件队列上的所有合成事件
}参考文档
React事件系统和源码浅析
react事件系统源码
react官方文档
-- SyntheticEvent> 来自
<https://github.com/mytac/blogs/blob/master/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/react/react%E4%BA%8B%E4%BB%B6%E7%B3%BB%E7%BB%9F.md>