问题复现
在把一份组件代码从React版本为17的项目中迁移到另一个React版本为16的项目中时,发现同样的一份代码却出现了不同的执行结果,简化版的代码如下:
function App() {
useEffect(() => {
document.addEventListener("click", () => console.log('document clicked'));
}, []);
function handleClick(event) {
event.stopPropagation();
console.log("btn clicked");
}
return <button onClick={handleClick}>CLICK ME</button>;
};
ReactDOM.render(<App />, document.getElementById('root'));
代码的用意这样,button和document都监听click事件,当点击按钮时希望阻止click事件冒泡,避免触发直接绑定在document上的处理函数。
在React17中非常正常,点击按钮只输出:btn clicked
;
在React16中出了点意外,点击按钮输出:btn clicked, document clicked
;
这是为啥?
先以React 16为例,大概介绍一下React绑定事件处理函数的机制:我们在jsx中指定的事件处理函数,其实并不会被绑定到对应的DOM元素上。而是都被委托到了更高层的元素上,就像事件委托那样。在React 16中,这个更高层的元素就是document。
当我们在页面中触发原生事件时,原生事件会沿着DOM树先冒泡到document,然后React会找出触发事件的组件,并让React合成事件在组件树中冒泡并触发对应的事件处理函数。
也就是说:
- 原生事件的冒泡和React合成事件的冒泡是两个过程,React合成事件的冒泡在原生事件冒泡到document之后才开始。通过jsx指定的事件处理函数会在React合成事件冒泡的过程中被触发。而直接绑定在DOM元素上的事件处理函数会在原生事件冒泡的过程中被触发。
- 在开始的代码中,如果是React 16,当真正执行handleClick的时候,原生事件已经冒泡到了document节点。
所以在React 16的版本中,即使调用了
event.stopPropagation()
,document上的事件处理函数还是被触发了。
那为啥在React17中没有触发?
React 17中的事件委托机制有所改变,jsx中指定的事件处理函数不再被委托到document,而是被委托到React组件树的容器节点。就是下面代码中的rootNode
。
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
当原生事件冒泡到rootNode
,就会让React合成事件在组件树中冒泡,就会触发并执行handleClick。也就调用了event.stopPropagation()
。这个方法既可以阻止合成事件在组件树中的冒泡,也可以阻止原生事件在DOM树中的冒泡。
所以在React 17的版本中,调用的event.stopPropagation()
虽然在事件冒泡到rootNode
之后才执行,但依然避免了触发document上的处理函数。
如何在React 16中避免触发document上的监听函数?
虽然因为React 16把事件处理函数都委托给了document,让event.stopPropagation()
无能为力。但还有另一个方法:event.nativeEvent.stopImmediatePropagation()
。它可以:
如果多个事件处理函数被附加到同一元素的相同事件上,当此事件触发时,它们会按其被添加的顺序被调用。如果在其中一个事件处理函数中执行 stopImmediatePropagation() ,那么剩下的事件处理函数都不会被调用。
而直接绑定在document上的事件处理函数总是在React合成事件的处理函数之后被执行,所以如果我们把之前的代码改成:
function App() {
useEffect(() => {
document.addEventListener("click", () => console.log('document clicked'));
}, []);
function handleClick(event) {
//event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
console.log("btn clicked");
}
return <button onClick={handleClick}>CLICK ME</button>;
};
ReactDOM.render(<App />, document.getElementById('root'));
这样即使执行handleClick时,事件已经冒泡到了document,依然阻止document上其他事件处理函数的执行。
再次点击按钮,输出:
引用文档:[译]React17官方提前说明