事件传播和事件委托
添加事件处理程序的方式:element.addEventListener('click',(event)=>{}),event为事件对象。缺省是冒泡阶段触发事件处理程序,捕获阶段触发事件处理程序:element.addEventListener('click',(event)=>{},true)
事件流的三个阶段:捕获阶段,到达目标阶段,冒泡阶段。捕获阶段是由外到内传播,先被document捕获,然后逐渐传播到事件目标,然后到冒泡阶段,冒泡阶段由内到外传播,逐渐传播到document。
<body>
<div id="parent">
<div id="child">test</div>
</div>
<script>
const parent = document.getElementById("parent");
const child = document.getElementById("child");
document.addEventListener(
"click",
(event) => {
console.log("document 捕获");
},
true
);
parent.addEventListener(
"click",
(event) => {
console.log("parent 捕获");
},
true
);
child.addEventListener(
"click",
(event) => {
console.log("child 捕获");
},
true
);
document.addEventListener("click", (event) => {
console.log("document 冒泡");
});
parent.addEventListener("click", (event) => {
console.log("parent 冒泡");
});
child.addEventListener("click", (event) => {
console.log("child 冒泡");
});
</script>
</body>

事件委托是利用事件冒泡,使用一个事件处理程序管理一类事件。将事件处理程序不添加到具体节点上而是添加到祖先节点,通过event.target区分触发节点。比如一个ul列表中每一项都添加事件处理程序,可以把事件处理程序加在ul上,通过event.target区分是哪一项触发的。
<body>
<ul id="list">
<li id="item1">第 1 项</li>
<li id="item2">第 2 项</li>
<li id="item3">第 3 项</li>
</ul>
<script>
const parent = document.getElementById("list");
parent.addEventListener("click", (event) => {
switch (event.target.id) {
case "item1":
console.log("第 1 项");
break;
case "item2":
console.log("第 2 项");
break;
case "item3":
console.log("第 3 项");
break;
default:
break;
}
});
</script>
</body>
React事件机制
React采用将事件委托给document的方式添加事件处理程序document.addEventListener('click',dispatchEvent),事件处理程序会模拟真实的事件系统先调度捕获阶段的事件处理程序,然后执行冒泡阶段的事件处理程序。因此React 元素上的onClick在真实的Dom上并没有绑定事件处理程序,在document冒泡阶段才会执行节点上的onClick,onClickCapture方法。
class App extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
componentDidMount() {
this.parentRef.current.addEventListener(
"click",
() => {
console.log("父元素 原生 捕获");
},
true
);
this.parentRef.current.addEventListener("click", () => {
console.log("父元素 原生 冒泡");
});
this.childRef.current.addEventListener(
"click",
() => {
console.log("子元素 原生 捕获");
},
true
);
this.childRef.current.addEventListener("click", () => {
console.log("子元素 原生 冒泡");
});
document.addEventListener(
"click",
() => {
console.log("document 捕获");
},
true
);
document.addEventListener("click", () => {
console.log("document 冒泡");
});
}
parentBubble = () => {
console.log("父元素React 冒泡");
};
childBubble = () => {
console.log("子元素React 冒泡");
};
parentCapture = () => {
console.log("父元素React 捕获");
};
childCapture = () => {
console.log("子元素React 捕获");
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.parentBubble}
onClickCapture={this.parentCapture}
>
<p
ref={this.childRef}
onClick={this.childBubble}
onClickCapture={this.childCapture}
>
事件执行顺序
</p>
</div>
);
}
}

模拟实现React事件机制
React通过事件委托响应事件,document.addEventListener("click", dispatchEvent),dispatchEvent应遵循事件模型,先处理捕获阶段的事件监听,后处理冒泡阶段的事件监听。
<body>
<div id="parent">
<div id="child">事件顺序</div>
</div>
<script>
const parent = document.getElementById("parent");
const child = document.getElementById("child");
function dispatchEvent(event) {
const pathes = [];
let path = event.target;
while (path) {
pathes.push(path);
path = path.parentNode;
}
for (let index = pathes.length - 1; index >= 0; index--) {
const path = pathes[index];
path.onClickCapture && path.onClickCapture();
}
for (let index = 0; index <= pathes.length - 1; index++) {
const path = pathes[index];
path.onClick && path.onClick();
}
}
document.addEventListener("click", dispatchEvent);
parent.onClick = () => {
console.log("parent React 冒泡");
};
parent.onClickCapture = () => {
console.log("parent React 捕获");
};
child.onClick = () => {
console.log("child React 冒泡");
};
child.onClickCapture = () => {
console.log("child React 捕获");
};
</script>
</body>
React 16事件机制存在的问题
理想中应遵循先捕获后冒泡,但在添加原生事件处理程序和React事件处理程序是会交替出现捕获、冒泡的情况,如上文图二。因此官方不建议混合使用原生事件处理程序和React事件处理程序。
React 17改进了事件机制,将事件不再委托给document而是委托给容器,并保证了捕获、冒泡不交叉出现。
<body>
<div id="parent">
<div id="child">事件顺序</div>
</div>
<script>
const parent = document.getElementById("parent");
const child = document.getElementById("child");
function dispatchEvent(event, isBubble) {
const pathes = [];
let path = event.target;
while (path) {
pathes.push(path);
path = path.parentNode;
}
if (isBubble) {
for (let index = 0; index <= pathes.length - 1; index++) {
const path = pathes[index];
path.onClick && path.onClick();
}
} else {
for (let index = pathes.length - 1; index >= 0; index--) {
const path = pathes[index];
path.onClickCapture && path.onClickCapture();
}
}
}
document.body.addEventListener(
"click",
(event) => dispatchEvent(event, false),
true
);
document.body.addEventListener("click", (event) =>
dispatchEvent(event, true)
);
parent.onClick = () => {
console.log("parent React 冒泡");
};
parent.onClickCapture = () => {
console.log("parent React 捕获");
};
child.onClick = () => {
console.log("child React 冒泡");
};
child.onClickCapture = () => {
console.log("child React 捕获");
};
</script>
</body>
混合原生和React事件处理程序示例:
<body>
<div id="parent">
<div id="child">事件顺序</div>
</div>
<script>
const parent = document.getElementById("parent");
const child = document.getElementById("child");
function dispatchEvent(event, isBubble) {
const pathes = [];
let path = event.target;
while (path) {
pathes.push(path);
path = path.parentNode;
}
if (isBubble) {
for (let index = 0; index <= pathes.length - 1; index++) {
const path = pathes[index];
path.onClick && path.onClick();
}
} else {
for (let index = pathes.length - 1; index >= 0; index--) {
const path = pathes[index];
path.onClickCapture && path.onClickCapture();
}
}
}
document.body.addEventListener(
"click",
(event) => dispatchEvent(event, false),
true
);
document.body.addEventListener("click", (event) =>
dispatchEvent(event, true)
);
parent.onClick = () => {
console.log("parent React 冒泡");
};
parent.onClickCapture = () => {
console.log("parent React 捕获");
};
child.onClick = () => {
console.log("child React 冒泡");
};
child.onClickCapture = () => {
console.log("child React 捕获");
};
parent.addEventListener(
"click",
(event) => console.log("parent 原生 捕获"),
true
);
parent.addEventListener("click", (event) =>
console.log("parent 原生 冒泡")
);
child.addEventListener(
"click",
(event) => console.log("child 原生 捕获"),
true
);
child.addEventListener("click", (event) =>
console.log("child 原生 冒泡")
);
document.addEventListener(
"click",
(event) => console.log("document 原生 捕获"),
true
);
document.addEventListener("click", (event) =>
console.log("document 原生 冒泡")
);
</script>
</body>

一个应用
页面有一个显示按钮,点击显示模态框,点击模态框其它区域关闭模态框。
class Modal extends React.Component {
state = { show: false };
componentDidMount() {
document.addEventListener("click", () => {
this.setState({ show: false });
});
}
handleClick = (event) => {
this.setState({ show: true });
};
render() {
return (
<div>
<button onClick={this.handleClick}>显示</button>
{this.state.show && <div>this is Modal</div>}
</div>
);
}
}
实际表现:点击按钮却不显示模态框。因为React事件handleClick在document事件监听处理程序处理,即先执行this.setState({ show: true });然后再执行componentDidMount中的this.setState({ show: false });所以不显示模态框。可以通过stopImediatePropagation阻止事件传播来解决这个问题。同一个节点有多个事件监听器,上面的事件监听器调用了stopImmediatePropagation,则后续的事件监听器不执行,事件也不会继续传播。
class Modal extends React.Component {
state = { show: false };
componentDidMount() {
document.addEventListener("click", () => {
this.setState({ show: false });
});
}
handleClick = (event) => {
this.setState({ show: true });
event.nativeEvent.stopImmediatePropagation();
};
handleModalClick = (event) => {
event.nativeEvent.stopImmediatePropagation();
};
render() {
return (
<div>
<button onClick={this.handleClick}>显示</button>
{this.state.show && (
<div onClick={this.handleModalClick}>this is Modal</div>
)}
</div>
);
}
}
总结
- React采用事件委托的方式在document上添加事件监听器;
- React中的事件对象是合成事件对象,抹平了系统的差异性;
- 通过事件委托使用一个事件监听器处理一类事件,代替使用多个事件监听函数的方案,节省内存和读写DOM的开销;
- 源码