React 16中e.stopPropagation()失灵的问题详解

3,625 阅读3分钟

问题复现

  在把一份组件代码从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合成事件在组件树中冒泡并触发对应的事件处理函数。
也就是说:

  1. 原生事件的冒泡和React合成事件的冒泡是两个过程,React合成事件的冒泡在原生事件冒泡到document之后才开始。通过jsx指定的事件处理函数会在React合成事件冒泡的过程中被触发。而直接绑定在DOM元素上的事件处理函数会在原生事件冒泡的过程中被触发。
  2. 在开始的代码中,如果是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官方提前说明