相信React
开发者或多或少听说过React
有合成事件
(SyntheticEvent)这一概念。
合成事件
这块源码代码量多、耦合了很多其他逻辑,读起来很劝退。
最近刚好在改一个anu
的bug
,发现anu
的合成事件
实现的简单易懂。为什么不通过anu
来学合成事件
呢?
anu
是司徒正美老师开发的类React
框架,他的特点是:
React
16的各种新功能React
全家桶antd
组件以上是面向开发者的特点。
源码层面,anu
的架构和React
v17是很像的,体积却只有React
的1/3,通过他来学习React
源码的一些流程再合适不过了。
让我们开始吧。
合成事件是React
在浏览器原有捕获->目标->冒泡
事件运行机制的基础上重新实现的一套事件运行机制。
为什么要在浏览器事件运行机制之上再重新造轮子呢?最主要的原因是:
浏览器原生实现中,event
触发后会在DOM树
中依次完成捕获->目标->冒泡
。
在此过程中经过的DOM
如果注册了event handler
,则handler
会被调用。
而在React
内部,并不直接操作DOM
,而是操作一棵与DOM
树有映射关系的虚拟DOM
树(fiber树)。
比如,对于如下应用:
function App() {
return (
<div>
<p onClick={() => console.log('click')}>click~</p>
</div>
)
}
ReactDOM.render(<App/>, root);
DOM
树与fiber
树分别为:
DOM树: fiber树:
html FiberRootNode
| |
body rootFiber
| |
div App fiber
| |
p div fiber
|
p fiber
可见,DOM
树与fiber
树并不是一一对应的。
onClick handler
作为props
保存在p
对应的fiber
上,而不是p DOM
上。
所以React
需要模拟DOM
树中事件的传递机制,实现一套类似机制在fiber
树中传递事件。
当重新实现整套事件机制后,要在其上再增加一些特性就再容易不过了,比如:
比如在React
中,表单组件的change
事件的触发时机其实对标的是原生DOM
中的input
事件。
再比如在React
中,focus
事件是由原生DOM
中的focusin
与focusout
实现的。
在React
中,不同事件的优先级不同。在不同事件的event handler
中触发的setState
会以不同优先级执行。
以下实现的代码皆来自anu
。
合成事件
的实现原理很好理解:
document
绑定event handler
,通过事件委托
的方式监听事件e.target
获取触发事件的DOM
,找到DOM
对应的fiber
fiber
向根fiber
遍历,收集遍历过程中所有绑定了该类型事件的fiber
的event handler
,保存在数组paths
中paths
,依次调用event handler
,模拟捕获流程paths.reverse()
,依次调用event handler
,模拟冒泡流程接下来我们以click
事件举例:
addGlobalEvent('click')
注册全局handler
用于事件委托
。其中dispatchEvent
为handler
。
function addGlobalEvent(name, capture) {
if (!globalEvents[name]) {
globalEvents[name] = true;
// addEventListener的实现
addEvent(document, name, dispatchEvent, capture);
}
}
DOM
,触发dispatchEvent
。function dispatchEvent(e, type, endpoint) {
e = new SyntheticEvent(e);
// ...一些前置处理,省略
Renderer.batchedUpdates(function() {
// 3. 通过collectPaths收集fiber沿途的click handler
let paths = collectPaths(e.target, terminal, {});
let captured = bubble + 'capture';
// 4. 模拟捕获流程
triggerEventFlow(paths, captured, e);
if (!e._stopPropagation) {
// 5. 模拟冒泡流程
triggerEventFlow(paths.reverse(), bubble, e);
}
}, e);
}
其中triggerEventFlow
就是简单的遍历数组并执行回调。
function triggerEventFlow(paths, prop, e) {
for (let i = paths.length; i--; ) {
let path = paths[i];
let fn = path.events[prop];
if (isFn(fn)) {
e.currentTarget = path.node;
fn.call(void 666, e);
if (e._stopPropagation) {
break;
}
}
}
}
现在我们知道了,当向p组件传递onClick props
,组件本身并不会绑定对应的handler
,组件销毁后也不会有click handler
的解绑操作。
“p对应DOM
响应点击事件”的原因是:
该DOM
对应的fiber
上的onClick
回调在dispatchEvent
方法中的collectPaths
中被收集,并在triggerEventFlow
中被调用。