react是如何实现冒泡的

343 阅读4分钟

两种事件模型
我们知道,在标准里面是支持 bubble 和 capture 两种事件模型的。

React 也支持这两种事件模型,很大可能你还没有使用过 React 的事件捕获,看下面的例子:

使用事件冒泡,如果点击按钮,childOnclick 会被触发,然后 parentOnclick 会被触发,如果 childOnClick 中调用了 event.stopPropagation(),阻止了冒泡,那么 parentOnClick 就不会触发了。这个过程是 child 到 parent,是自底向上的,就像冒泡一样。

<div onClick={this.parentOnClick}>
<button onClick={this.childOnClick}>冒泡的事件!</button>
</div>
像web标准一样,其实也可以反过来,先是父级组件先触发事件,然后再一级往下传递,这种方式被称为捕获。使用 onEventNameCapture,就是使用捕获的方式,下面的代码会先执行 parentOnClick,再执行 childOnClick,如果在 parentOnClick 调用了 stopPropagation 阻止事件传递,那么就会导致 childOnClick 不会被触发。

<div onClickCapture={this.parentOnClick}>
<button onClick={this.childOnClick}>捕获的事件!</button>
</div>
为什么
为什么会有这两种事件模型呢?

一方面从历史沿革来看,在浏览器的早期,Netscape 浏览器是使用的 capture 事件模型,而 IE 使用的是冒泡模型,后来的标准里面就有了这两种模型可选:

element.addEventListner(name, fn, useCapture)
useCapture 为 true 表示使用捕获,useCapture 为 false 表示使用冒泡。

现在,大家从使用习惯上来讲,使用冒泡会比较多。addEventListner 的第 3 个参数 useCapture 的默认值也是 false.

另一方面,从性能上来讲,捕获模型的性能会好一丢丢,见 这里的讨论.

react/类react框架是如何实现冒泡机制的?
前面是铺垫,现在引入主题。

有一个问题一直困惑我:有些事件是不支持事件冒泡的,比如 blur 事件,那么 react 是如何实现这类事件冒泡的?

<div id="el">
<input type="text" id="input">
</div>
如果使用原生的方式,在 el 绑定 blur 事件,在 input 上也绑定 blur 事件,当 input 触发 blur 事件,其父元素并不会触发 blur 事件。下面的代码,只会输出 #2.

const el = document.querySelector('#el');
const ip = document.querySelector('#input');

el.addEventListener('blur', function(e) {
console.log(`#1 new ${e.target.value}`)
}, false)

ip.addEventListener('blur', function(e) {
console.log(`#2 ${e.target.value}`)
})
而在 react 中,当 input blur 事件触发后,会按照 #1 #2 的顺序输出

<div onBlur={this.parentOnBlur}>
<input type="text" onBlur={this.childOnBlur}>
</div>
如果你使用的是一些类 react 的方案,比如 react-lite,可能会存在bug的,上面的代码,在 react-lite 不能按照预期的方式冒泡。

实现方案一
在 ninjia javascript这本书中,有对不能冒泡的特殊事件进行处理,以 change 事件为例,总结来讲就是

实现一个 triggerEvent 方法,能手动触发事件
如果目标元素不支持冒泡,那么使用其他的事件来监测子元素的 change 变化
分别绑定 focusout click keydown beforeactivate 等监控函数
当发现目标元素,比如 input,发生了值的变化,那么调用 triggerEvent
triggerEvent 会被递归的,冒泡调用
如此,实现了冒泡不能冒泡的事件
具体实现参见 Secrets of the JavaScript Ninja 这本书的 13.5 bubbling and delegation
pic1

实现方案二
anu.js 的作者在 blog中写道:

对于focus,blur,change,submit,reset,select等不会冒泡的事件,在标准游览器中,我们可以设置addEventListener的最后一个参数为true轻松搞定

巧妙的使用 addEventListener 的第3个捕获参数,那么首先事件就会在 root 被捕获
然后获取到 e.target 也就是 input元素,然后再通过 input 元素,往上触发事件,实现冒泡
// 使用 capture 参数来实现捕获不能冒泡的事件
const el = document.querySelector('#el');
const ip = document.querySelector('#input');

el.addEventListener('blur', function(e) {
console.log(`#1 new ${e.target.value}`)
}, true); // blur 事件触发,将先打出 #1,再打出 #2

ip.addEventListener('blur', function(e) {
console.log(`#2 ${e.target.value}`)
})
比如在兼容 react 的框架 anu.js 中,对不能冒泡的 blur 事件是这样处理的:

pic2

react 事件是绑定到 document上的,所以 e.currentTarget 是 document,e.target 是 input
根据 input,获取向上冒泡的路径,即会冒泡元素 collectPaths,然后一个循环触发,如果循环中有 stopPropagation,那么终止循环
当然这都不是 react 的实际实现,因为 React 的代码太难读了,盘根错节,我还没有找到具体实现在哪里。如有理解不正确,欢迎指出 ^_^。