事件傳播和事件委托
添加事件處理程序的方式:element.addEventListener('click',(event)=>{}),event為事件對(duì)象。缺省是冒泡階段觸發(fā)事件處理程序,捕獲階段觸發(fā)事件處理程序:element.addEventListener('click',(event)=>{},true)
事件流的三個(gè)階段:捕獲階段,到達(dá)目標(biāo)階段,冒泡階段。捕獲階段是由外到內(nèi)傳播,先被document捕獲,然后逐漸傳播到事件目標(biāo),然后到冒泡階段,冒泡階段由內(nèi)到外傳播,逐漸傳播到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>

事件委托是利用事件冒泡,使用一個(gè)事件處理程序管理一類事件。將事件處理程序不添加到具體節(jié)點(diǎn)上而是添加到祖先節(jié)點(diǎn),通過(guò)event.target區(qū)分觸發(fā)節(jié)點(diǎn)。比如一個(gè)ul列表中每一項(xiàng)都添加事件處理程序,可以把事件處理程序加在ul上,通過(guò)event.target區(qū)分是哪一項(xiàng)觸發(fā)的。
<body>
<ul id="list">
<li id="item1">第 1 項(xiàng)</li>
<li id="item2">第 2 項(xiàng)</li>
<li id="item3">第 3 項(xiàng)</li>
</ul>
<script>
const parent = document.getElementById("list");
parent.addEventListener("click", (event) => {
switch (event.target.id) {
case "item1":
console.log("第 1 項(xiàng)");
break;
case "item2":
console.log("第 2 項(xiàng)");
break;
case "item3":
console.log("第 3 項(xiàng)");
break;
default:
break;
}
});
</script>
</body>
React事件機(jī)制
React采用將事件委托給document的方式添加事件處理程序document.addEventListener('click',dispatchEvent),事件處理程序會(huì)模擬真實(shí)的事件系統(tǒng)先調(diào)度捕獲階段的事件處理程序,然后執(zhí)行冒泡階段的事件處理程序。因此React 元素上的onClick在真實(shí)的Dom上并沒(méi)有綁定事件處理程序,在document冒泡階段才會(huì)執(zhí)行節(jié)點(diǎn)上的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}
>
事件執(zhí)行順序
</p>
</div>
);
}
}

模擬實(shí)現(xiàn)React事件機(jī)制
React通過(guò)事件委托響應(yīng)事件,document.addEventListener("click", dispatchEvent),dispatchEvent應(yīng)遵循事件模型,先處理捕獲階段的事件監(jiān)聽(tīng),后處理冒泡階段的事件監(jiān)聽(tīng)。
<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事件機(jī)制存在的問(wèn)題
理想中應(yīng)遵循先捕獲后冒泡,但在添加原生事件處理程序和React事件處理程序是會(huì)交替出現(xiàn)捕獲、冒泡的情況,如上文圖二。因此官方不建議混合使用原生事件處理程序和React事件處理程序。
React 17改進(jìn)了事件機(jī)制,將事件不再委托給document而是委托給容器,并保證了捕獲、冒泡不交叉出現(xiàn)。
<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>

一個(gè)應(yīng)用
頁(yè)面有一個(gè)顯示按鈕,點(diǎn)擊顯示模態(tài)框,點(diǎn)擊模態(tài)框其它區(qū)域關(guān)閉模態(tài)框。
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>
);
}
}
實(shí)際表現(xiàn):點(diǎn)擊按鈕卻不顯示模態(tài)框。因?yàn)镽eact事件handleClick在document事件監(jiān)聽(tīng)處理程序處理,即先執(zhí)行this.setState({ show: true });然后再執(zhí)行componentDidMount中的this.setState({ show: false });所以不顯示模態(tài)框。可以通過(guò)stopImediatePropagation阻止事件傳播來(lái)解決這個(gè)問(wèn)題。同一個(gè)節(jié)點(diǎn)有多個(gè)事件監(jiān)聽(tīng)器,上面的事件監(jiān)聽(tīng)器調(diào)用了stopImmediatePropagation,則后續(xù)的事件監(jiān)聽(tīng)器不執(zhí)行,事件也不會(huì)繼續(xù)傳播。
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>
);
}
}
總結(jié)
- React采用事件委托的方式在document上添加事件監(jiān)聽(tīng)器;
- React中的事件對(duì)象是合成事件對(duì)象,抹平了系統(tǒng)的差異性;
- 通過(guò)事件委托使用一個(gè)事件監(jiān)聽(tīng)器處理一類事件,代替使用多個(gè)事件監(jiān)聽(tīng)函數(shù)的方案,節(jié)省內(nèi)存和讀寫DOM的開(kāi)銷;
- 源碼