React事件机制是React重要的基础知识,也是面试过程中经常被提及的问题。本文将介绍React事件机制 - 合成事件的概念以及作用等知识点,本文将重点以React V17版本为准。
本文所用代码仓库
React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,在React中这套事件机制被称之为合成事件。
与原生事件直接在元素上注册的方式不同的是,React的合成事件不会直接绑定到目标DOM节点上,用事件委托机制,以队列的方式,从触发事件的组件向父组件回溯,直到Root节点。因此,React组件上声明的事件最终绑定到了Root对象(React17之前是Document)上。在Root节点,用一个统一的监听器去监听,这个监听器上保存着目标节点与事件对象的映射。当组件挂载或卸载时,只需在这个统一的事件监听器上插入或删除对应对象;当事件发生时(即Root上的事件处理函数被执行),在映射里按照冒泡或捕获的路径去组件中收集真正的事件处理函数,然后,由这个统一的事件监听器对所收集的事件逐一执行。
这样做的好处:
• 对事件进行归类,可以在事件产生的任务上包含不同的优先级
• 提供合成事件对象,抹平浏览器的兼容性差异
• 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 Root上注册一次
• 对开发者友好
JavaScript原生事件的执行需要经过三个阶段,事件捕获-事件目标-事件冒泡。
React合成事件与原生事件很相似,但不完全相同。这里列举几个常见区别:
// 原生事件命名方式
<button onclick="handleClick()">原生事件命名方式</button>
//React合成事件命名方式
<button onClick={handleClick}>合成事件命名方式 </button>
// 原生事件处理函数写法
<button onclick="handleClick()">原生事件处理函数写法</button>
//React合成事件处理函数写法
<button onClick={handleClick}>React合成事件处理函数写法</button>
// 原生事件阻止默认行为方式
<a href="https://www.baidu.com"
onclick="console.log('原生事件阻止默认行为'); return false"
>
原生事件阻止默认行为
</a>
// React合成事件阻止默认行为方式
const handleClick = e => {
e.preventDefault();
console.log('合成事件阻止默认行为');
}
const clickElement = <a href="https://www.baidu.com" onClick={handleClick}>
合成事件阻止默认行为
</a>
在 React 中,合成事件会以事件委托方式绑定在Root对象上。这里我们手写一个简单示例来观察 React 事件和原生事件的执行顺序:
class App extends React.Component {
parentRef;
childRef;
constructor(props) {
super(props);
this.parentRef = React.createRef();
this.childRef = React.createRef();
}
componentDidMount() {
document.addEventListener(
"click",
(e) => {
console.log("原生事件:document DOM 事件捕获---");
},
true
);
this.parentRef.current?.addEventListener(
"click",
() => {
console.log("原生事件:父元素 DOM 事件捕获---");
},
true
);
this.childRef.current?.addEventListener(
"click",
() => {
console.log("原生事件:子元素 DOM 事件捕获---");
},
true
);
document.addEventListener(
"click",
(e) => {
console.log("原生事件:document DOM 事件冒泡!");
},
false
);
this.parentRef.current?.addEventListener(
"click",
() => {
console.log("原生事件:父元素 DOM 事件冒泡!");
},
false
);
this.childRef.current?.addEventListener(
"click",
() => {
console.log("原生事件:子元素 DOM 事件冒泡!");
},
false
);
}
parentClickFun = () => {
console.log("React 事件:父元素事件冒泡!");
};
parentCapture = () => {
console.log("React 事件:父元素事件捕获-----");
};
childClickFun = () => {
console.log("React 事件:子元素事件冒泡!");
};
childCapture = () => {
console.log("React 事件:子元素事件捕获-----");
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.parentClickFun}
onClickCapture={this.parentCapture}
>
<p
ref={this.childRef}
onClick={this.childClickFun}
onClickCapture={this.childCapture}
>
分析事件执行顺序
</p>
</div>
);
}
}
export default App;
触发事件后,可以看到控制台输出:
通过上面流程,我们可以总结得出:
• React 所有事件都委托在root 对象上;
• 当真实 DOM 元素触发事件,会冒泡到 root 对象后,再处理 React 事件;
• 在捕获阶段,先注册的先执行,且React合成事件先于原生事件执行;冒泡阶段,先注册的后执行,且原生事件先于React事件执行
原生事件阻止冒泡:可以使用 e.stopPropagation() 或者 e.cancelBubble=true(IE)来阻止事件的冒泡传播。
React阻止冒泡:通常开发者都希望使用 e.stopPropagation() 方法来阻止React的事件冒泡,但事实上,由于所有的合成事件本身都是在 Root上执行的,所以,e.stopPropagation() 只能阻止合成事件间冒泡,即下层的合成事件,不会冒泡到上层的合成事件。
因此,在react中,阻止冒泡的方式有三种:
存在代码如下
class StopBubbling extends React.Component {
componentDidMount() {
document.body.addEventListener("click", this.handleBodyClick, false);
document.addEventListener("click", this.handleDocumentClick, false);
}
handleDocumentClick = (e) => {
console.log("handleDocumentClick!");
};
handleBodyClick = (e) => {
console.log("handleBodyClick! ");
};
handleClickTestBox = (e) => {
console.warn("handleClickTestBox!");
};
handleClickTestBox2 = (e) => {
console.warn("handleClickTestBox2!");
};
handleClickTestBox3 = (e) => {
console.warn("handleClickTestBox3!");
};
render() {
return (
<div
className="test-box"
onClick={this.handleClickTestBox}
style={{ background: "red" }}
>
Box
<div
onClick={this.handleClickTestBox2}
style={{ background: "green", margin: "10px" }}
>
Box2
<div
id="inner"
onClick={this.handleClickTestBox3}
style={{ background: "yellow", margin: "10px" }}
>
Box3
</div>
</div>
</div>
);
}
}
export default StopBubbling;
handleBodyClick = (e) => {
//阻止合成事件与非合成事件(除了document)之间的冒泡
if (e.target && e.target === document.querySelector("#inner")) {
return;
}
console.log("handleBodyClick!");
};
此时,点击Box3输出:
handleClickTestBox3 = (e) => {
// 阻止与原生事件的冒泡
e.nativeEvent.stopImmediatePropagation();
console.warn("handleClickTestBox3!");
};
此时,点击Box3输出:
handleClickTestBox3 = (e) => {
// 阻止合成事件的冒泡
e.stopPropagation();
console.warn("handleClickTestBox3!");
};
此时,点击Box3输出:
注意,合成事件和原生事件最好不要混用。
因为,React中的执行e.stopPropagation()可以阻止React合成事件冒泡,但是不能阻止原生事件冒泡;反之,在原生事件中执行e.stopPropagation()阻止冒泡行为,会导致所有元素的事件将无法冒泡到Root上,即所有的 React 事件都将无法被注册。
推荐学习:
【React深入】React事件机制:
实现React事件机制(1、2)
参考文献:
探索 React 合成事件
react中阻止事件冒泡