认识
一、认识
React.js 16.x
顶层容器为 Document
。因此, React
执行大多数事件都会调用 document.addEventListener()
。触发事件后, 事件流为: Document
捕获 -> DOM
原生捕获 -> DOM
原生冒泡 -> React
捕获 -> React
冒泡 -> Document
冒泡。
React.js 16.x
捕获阶段和冒泡阶段的事件都是模拟的,本质上都是在冒泡阶段执行的。一定程度上,不符合事件流的执行时机。
二、对比
2.1 事件绑定位置
原生 DOM
将事件绑定在当前元素上
React
将事件函数存储到当前元素对应的 Fiber
中的 memoizedProps
和 pendingProps
2.2 事件执行顺序
React.js 16.x
把事件委托到Document
对象上。触发事件之后, 事件流为: Document
捕获 -> DOM
原生捕获 -> DOM
原生冒泡 -> React
捕获 -> React
冒泡 -> Document
冒泡
import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
componentDidMount() {
this.parentRef.current.addEventListener(
"click",
() => {
console.log("父元素原生捕获");
},
true
);
this.parentRef.current.addEventListener(
"click",
() => {
console.log("父元素原生冒泡");
},
false
);
this.childRef.current.addEventListener(
"click",
() => {
console.log("子元素原生捕获");
},
true
);
this.childRef.current.addEventListener(
"click",
() => {
console.log("子元素原生冒泡");
},
false
);
document.addEventListener(
"click",
() => {
console.log("document 元素捕获");
},
true
);
document.addEventListener(
"click",
() => {
console.log("document 元素冒泡");
},
false
);
}
parentBubble = () => {
console.log("父元素 React 事件冒泡");
};
parentCapture = () => {
console.log("父元素 React 事件捕获");
};
childBubble = () => {
console.log("子元素 React 事件冒泡");
};
childCapture = () => {
console.log("子元素 React 事件捕获");
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.parentBubble}
onClickCapture={this.parentCapture}
>
<div
ref={this.childRef}
onClick={this.childBubble}
onClickCapture={this.childCapture}
>
事件执行顺序
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
点击按钮,执行结果如下:
document 捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
父元素React事件捕获
子元素React事件捕获
子元素React事件冒泡
父元素React事件冒泡
document 冒泡
由上可以看出: React
注册的捕获/冒泡事件,不是在冒泡/捕获阶段执行的, 给元素绑定的事件,不是真正的事件处理函数
2.3 阻止默认事件
在 React
中,我们的所有事件都可以说是虚拟的,并不是原生的事件。我们在 React
中拿到的事件源(e
) 也并非是真正的事件e
,而是经过React
单独处理的e
。
-
原生事件: 可以通过
e.preventDefault()
和return false
来阻止默认事件 -
合成事件: 通过
e.preventDefault()
阻止默认事件, 由于React
注册的事件处理函数不是真正的处理函数, 所以不可以通过return false
来组织默认事件。特别注意, 原生事件和合成事件的e.preventDefault()
并非是同一个函数,React
的事件源e
是单独创立的。
2.3 阻止事件冒泡
阻止合成事件间的冒泡: e.stopPropagation()
阻止合成事件与最外层 document
上的事件间的冒泡: e.nativeEvent.stopImmediatePropagation()
阻止合成事件与除最外层 document
上的原生事件上的冒泡: 通过判断e.target
来避免
document.body.addEventListener('click', e => {
if (e.target && e.target.matches('div.code')) {
return;
}
this.setState({ active: false, }); });
}
三、事件池
在 React.js 16.x
中采取了一个事件池的概念,每次我们用的事件源对象,在事件函数执行之后,可以通过 releaseTopLevelCallbackBookKeeping
等方法将事件源对象释放到事件池中,这样的好处每次我们不必再创建事件源对象,可以从事件池中取出一个事件源对象进行复用,在事件处理函数执行完毕后,会释放事件源到事件池中,清空属性。
测试用例如下:
handerClick = (e) => {
console.log(e.target) // button
setTimeout(()=>{
console.log(e.target) // null
},0)
}
这就是 setTimeout
中打印为什么是 null
的原因了。
四、绑定事件
4.1 存储事件
React.js
中, 通过 JSX
转换, 元素注册的 onClick
、 onChange
事件会存储到对应元素 fiber
的 memoizedProps
和 pendingProps
属性上。
4.2 判断事件
React
处理 props
时, 会判断当前的 propKey
是否为 React
合成事件。 判断方式就是通过判断 propKey
是否在 registrationNameModules
中。判断逻辑如下:
function diffProperties(){
/* 判断当前的 propKey 是不是 React合成事件 */
if(registrationNameModules.hasOwnProperty(propKey)){
/* 这里多个函数简化了,如果是合成事件, 传入成事件名称 onClick ,向document注册事件 */
legacyListenToEvent(registrationName, document);
}
}
registrationNameModules
记录了 React
事件(比如 onBlur
)和与之对应的处理插件的映射。
4.3 注册事件
接着, 如果判断是合成事件, React
通过 legacyListenToEvent
传入合成事件名称, 向顶层容器 document
注册事件。注册逻辑如下:
function legacyListenToEvent(registrationName,mountAt){
const dependencies = registrationNameDependencies[registrationName]; // 根据 onClick 获取 onClick 依赖的事件数组 [ 'click' ]。
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
legacyTrapBubbledEvent(dependency, mountAt);
}
}
registrationNameDependencies
对象保存了 React
事件和原生事件对应关系。取出合成事件对应的原生事件数组, 遍历事件数组, 调用 legacyListenToTopLevelEvent
判断事件是否进行冒泡处理。React.js 16.x
大部分事件都按照冒泡处理,少数事件会按照捕获逻辑处理(比如 scroll
、focus
、blur
事件)。
function legacyTrapBubbledEvent(topLevelType,element){
addTrappedEventListener(element,topLevelType,PLUGIN_EVENT_SYSTEM,false)
}
4.4 绑定容器
React
是如何绑定事件到 document
? 事件处理函数函数又是什么?问题都指向了上述的 addTrappedEventListener
function addTrappedEventListener(targetContainer,topLevelType,eventSystemFlags,capture){
const listener = dispatchEvent.bind(null,topLevelType,eventSystemFlags,targetContainer)
if(capture){
// 事件捕获阶段处理函数。
}else{
targetContainer.addEventListener(topLevelType,listener,false) // document.addEventListener('click',listener,false)
}
}
如上所示, 所有事件都会绑定到 dispatchEvent
对象上。最后, dispatchEvent
会作为真正的事件处理函数。
五、触发事件
5.1 调度事件
React.js 16.x
事件注册时候, 将事件统一绑定到 dispatchEvent
。也就是当我们触发事件后,首先执行的是 dispatchEvent
函数。执行 attemptToDispatchEvent
尝试调度事件。根据真实的事件源对象, 找到 e.target
真实的 DOM
元素。然后根据 DOM
元素找到与它对应的 Fiber
。 (React
在初始化真实 dom
的时候,用一个随机的 key internalInstanceKey
指针指向了当前 dom
对应的 fiber
对象,fiber
对象用 stateNode
指向了当前的 dom
元素。)。然后正式进去 legacy
模式的事件处理系统,
function dispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
/* 尝试调度事件 */
const blockedOn = attemptToDispatchEvent( topLevelType,eventSystemFlags, targetContainer, nativeEvent);
}
function attemptToDispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
/* 获取原生事件 e.target */
const nativeEventTarget = getEventTarget(nativeEvent)
/* 获取当前事件,最近的dom类型fiber ,我们 demo中 button 按钮对应的 fiber */
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
/* 重要:进入legacy模式的事件处理系统 */
dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst,);
return null;
}
5.2 批量更新
在 legacy
模式下, 从 React
事件池中找到对应属性, 赋予给事件, 随后开启批量更新, 执行真正的事件处理函数 handleTopLevel
, 最后释放事件池。
/* topLevelType - click事件 | eventSystemFlags = 1 | nativeEvent = 事件源对象 | targetInst = 元素对应的fiber对象 */
function dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst){
/* 从React 事件池中取出一个,将 topLevelType ,targetInst 等属性赋予给事件 */
const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType,nativeEvent,targetInst,eventSystemFlags);
try { /* 执行批量更新 handleTopLevel 为事件处理的主要函数 */
batchedEventUpdates(handleTopLevel, bookKeeping);
} finally {
/* 释放事件池 */
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}
export function batchedEventUpdates(fn,a){
isBatchingEventUpdates = true;
try{
fn(a) // handleTopLevel(bookKeeping)
}finally{
isBatchingEventUpdates = false
}
}
由上可以看出, 触发事件后, 最终会走到 batchedEventUpdates
函数,进而开启批量更新, 因此 React
合成事件 具有批量更新的功能。示例如下:
state={number:0}
handerClick = () =>{
this.setState({number: this.state.number + 1 })
console.log(this.state.number) //0
this.setState({number: this.state.number + 1 })
console.log(this.state.number) //0
setTimeout(()=>{
this.setState({number: this.state.number + 1 })
console.log(this.state.number) //2
this.setState({number: this.state.number + 1 })
console.log(this.state.number)// 3
})
}
如上述所示,第一个 setState
和第二个setState
在批量更新条件之内执行,所以打印不会是最新的值,但是如果是发生在setTimeout
中,由于eventLoop
放在了下一次事件循环中执行,此时 batchedEventUpdates
中已经执行完isBatchingEventUpdates = false
,所以批量更新被打破,我们就可以直接访问到最新变化的值了。
5.3 执行事件插件函数
batchedEventUpdates
中的 fn(a)
调用的是 handleTopLevel
。
function handleTopLevel(bookKeeping){
const { topLevelType,targetInst,nativeEvent,eventTarget, eventSystemFlags} = bookKeeping
for(let i=0; i < plugins.length;i++ ){
const possiblePlugin = plugins[i];
/* 找到对应的事件插件,形成对应的合成event,形成事件执行队列 */
const extractedEvents = possiblePlugin.extractEvents(topLevelType,targetInst,nativeEvent,eventTarget,eventSystemFlags)
}
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
/* 执行事件处理函数 */
runEventsInBatch(events);
}
handleTopLevel
最后的处理逻辑就是执行我们说的事件处理插件(SimpleEventPlugin
)中的处理函数extractEvents
。 extractEvents
可以作为整个事件系统核心函, 作用如下:
-
生成事件源对象, 保存了整个事件的信息。将作为参数传递给真正的事件处理函数
-
从事件源开始逐渐向上,查找元素类型为
HostComponent
对应的fiber
,收集上面的React
合成事件, 直到顶层容器。在事件捕获阶段, 将事件执行函数unshift
添加到队列的最前面, 模拟事件捕获阶段。在事件冒泡阶段, 将事件执行函数push
添加到队列的最后面, 模拟事件冒泡阶段。 -
最后将事件执行队列,保存到
React
事件源对象上。等待执行。
5.4 执行事件队列
依次执行执行队列, 如果发现阻止冒泡,那么 break
跳出循环,最后重置事件源,放回到事件池中,完成整个流程。