React太劝退,通过anu学合成事件
相信React
开发者或多或少听说过React
有合成事件
(SyntheticEvent)这一概念。
合成事件
这块源码代码量多、耦合了很多其他逻辑,读起来很劝退。
最近刚好在改一个anu
的bug
,发现anu
的合成事件
实现的简单易懂。为什么不通过anu
来学合成事件
呢?
anu是什么
anu
是司徒正美老师开发的类React
框架,他的特点是:
- 支持
React
16的各种新功能 - 跑通官方近800多个单元测试
- 支持
React
全家桶 - 支持99%的
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
树中传递事件。
当重新实现整套事件机制后,要在其上再增加一些特性就再容易不过了,比如:
- 抹平不同浏览器事件机制的不同(IE说的就是你)
- 对事件的定制化需求。
比如在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
中被调用。
- mysql操作命令梳理(5)-执行sql语句查询即mysql状态说明
- 【7】AccessDB快速数据访问
- 4.微信支付
- 【8】数据浏览表格的快速输出
- Silverlight的自定义tooltip提示工具条
- WritableBitmapEx 一瞥
- 2.认证小程序
- 【9】分页浏览的管理
- 温故而知新:silverlight中的图片资源绑定
- 页面状态保持机制(编辑中)
- mysql主从同步(5)-同步延迟状态考量(seconds_behind_master和pt-heartbea)
- ngx_pagespeed-nginx前端优化模块介绍
- Tomcat利用MSM实现Session共享方案解说
- Tomcat集群环境下session共享方案梳理(1)-通过memcached(MSM)方法实现
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法