react和浏览器的事件机制大多数人都有了解,本文主要介绍一些容易误解的点。
原生事件的冒泡和捕获
原生事件有冒泡和捕获两个阶段,两个阶段的先后顺序是捕获在前,冒泡在后。
jsx
function EventTest() {
useEffect(() => {
document.querySelector("#wrap").addEventListener(
"click",
(e) => {
console.log("div 捕获");
},
true
);
document.querySelector("#wrap").addEventListener("click", () => {
console.log("div 冒泡");
});
document.querySelector("#btn").addEventListener(
"click",
() => {
console.log("btn 捕获");
},
true
);
document.querySelector("#btn").addEventListener("click", () => {
console.log("btn 冒泡");
});
}, []);
return (
<div id="wrap">
<p>
<button id="btn">btn</button>
</p>
</div>
);
}

stopPropagation
stopPropagation可以阻止事件传播,如果在捕获阶段阻止,那么事件不会进入冒泡阶段。
diff
document.querySelector("#wrap").addEventListener(
"click",
(e) => {
+ e.stopPropagation();
console.log("div 捕获");
},
true
);
stopPropagation不会阻止对同一元素上的同一事件的其他事件监听
javascript
document.querySelector("#wrap").addEventListener(
"click",
(e) => {
e.stopPropagation();
console.log("div 捕获1");
},
true
);
document.querySelector("#wrap").addEventListener(
"click",
(e) => {
console.log("div 捕获2");
},
true
);

stopImmediatePropagation
如果多个事件监听器被附加到相同元素的相同事件类型上,当此事件触发时,它们会按其被添加的顺序被调用。如果在其中一个事件监听器中执行 stopImmediatePropagation()
,那么剩下的事件监听器都不会被调用。
diff
document.querySelector("#wrap").addEventListener(
"click",
(e) => {
- e.stopPropagation();
+ e.stopImmediatePropagation()
console.log("div 捕获1");
},
true
);
document.querySelector("#wrap").addEventListener(
"click",
(e) => {
console.log("div 捕获2");
},
true
);

React事件委托
React事件中的冒泡
React是在 React root节点上利用事件委托获取原生事件,并且默认是在冒泡阶段捕捉原生事件 ,因此如果在原生事件上阻止冒泡,也会阻止React的事件流 ,而阻止React的事件流并不会阻止原生事件(先原生事件,后React事件流),调用stopPropagation对原生事件没有任何影响。
jsx
function EventTest() {
useEffect(() => {
document.querySelector("#wrap").addEventListener(
"click",
(e) => {
console.log("div 捕获");
},
true
);
document.querySelector("#wrap").addEventListener("click", () => {
console.log("div 冒泡");
});
document.querySelector("#btn").addEventListener(
"click",
() => {
console.log("btn 捕获");
},
true
);
document.querySelector("#btn").addEventListener("click", (e) => {
e.stopPropagation(); //react事件无法响应
console.log("btn 冒泡");
});
}, []);
const click = (e) => {
console.log("react click", e);
};
return (
<div id="wrap">
<p>
<button id="btn" onClick={click}>
btn
</button>
</p>
</div>
);
}
React事件中的捕获
React事件可以在捕获阶段监听------使用对应的eventName+Capture,这种情况下,react的委托事件,会在事件的捕获阶段获取到原生事件。这时候,调用stopPropagation会影响原生事件的传播。
javascript
function EventTest() {
useEffect(() => {
document.querySelector("#wrap").addEventListener(
"click",
(e) => {
console.log("div 捕获");
},
true
);
document.querySelector("#wrap").addEventListener("click", () => {
console.log("div 冒泡");
});
document.querySelector("#btn").addEventListener(
"click",
() => {
console.log("btn 捕获");
},
true
);
document.querySelector("#btn").addEventListener("click", (e) => {
console.log("btn 冒泡");
});
}, []);
const click = (e) => {
e.stopPropagation();
console.log("react click", e);
};
const clickCapture = (e) => {
e.stopPropagation();
console.log("react click capture", e);
};
return (
<div id="wrap">
<p>
<button id="btn" onClick={click} onClickCapture={clickCapture}>
btn
</button>
</p>
</div>
);
}
在捕获事件中阻止事件传播后原生监听的事件都没有响应了:
React事件传播
React监听到原生事件后,会寻找对应的Fiber节点(React创建dom对象时会将其与Fiber关联)。 找到目标 Fiber 节点后,则一直往上遍历父节点 Fiber,
收集这一条链上的所有对应事件回调函数。
- 捕获阶段:从 root 向 target 收集;
- 冒泡阶段:从 target 向 root 收集。
然后再按顺序执行这些回调函数,完成对原生事件传播的模拟。
因为Fiber树与原生dom树未必一致,所以React的事件传播与原生事件传播也可能并不一致。比如弹窗上的点击事件可能会冒泡到弹窗外的元素上。
React事件对象:事件池与重用
在React 16中,如果异步使用合成事件对象,会出现如下报错:
这是因为React 16中会重复使用同一事件对象
- 当浏览器触发事件(如
click
)时,React 拦截它; - React 创建一个
SyntheticEvent
对象; - React 会把原生事件属性(如
target
,type
,clientX
等)拷贝到这个合成事件上; - React 调用所有注册在这个事件上的 React 回调;
- 事件处理完后 ,React 会调用
event.release()
将该对象的所有属性清空,放回"事件池"; - 下次有新的事件触发时,React 会复用这个对象(重新写入属性)。 因此它提供了
persist
方法,让该事件不被重用。
不过React 17废除了该特性。
总结
特性 | React 事件冒泡 | 浏览器原生冒泡 |
---|---|---|
冒泡路径 | React 虚拟 DOM 树 | 实际 DOM 树 |
事件对象 | SyntheticEvent (合成事件) |
原生 Event |
stopPropagation() | 阻止 React 内部的合成事件传播,不一定阻止原生事件传播 | 阻止原生事件传播 |