60行代碼實(shí)現(xiàn)React的事件系統(tǒng)
由于如下原因,React的事件系統(tǒng)代碼量很大:
需要抹平不同瀏覽器的差異
與內(nèi)部的「優(yōu)先級(jí)機(jī)制」綁定
需要考慮所有瀏覽器事件
但如果抽絲剝繭會(huì)發(fā)現(xiàn),事件系統(tǒng)的核心只有兩個(gè)模塊:
SyntheticEvent(合成事件)
模擬實(shí)現(xiàn)的事件傳播機(jī)制
本文會(huì)用60行代碼實(shí)現(xiàn)這兩個(gè)模塊,讓你快速了解React事件系統(tǒng)的原理。
在線DEMO地址[1]
Demo的效果
對(duì)于如下這段JSX:
const?jsx?=?(
??<section?onClick={(e)?=>?console.log("click?section")}>
????<h3>你好h3>
????<button
??????onClick={(e)?=>?{
????????//?e.stopPropagation();
????????console.log("click?button");
??????}}
????>
??????點(diǎn)擊
????button>
??section>
);
在瀏覽器中渲染:
const?root?=?document.querySelector("#root");
ReactDOM.render(jsx,?root);
點(diǎn)擊按鈕,會(huì)依次打?。?/p>
click?button
click?section
如果在button的點(diǎn)擊回調(diào)中增加e.stopPropagation(),點(diǎn)擊后會(huì)打?。?/p>
click?button
我們的目標(biāo)是將JSX中的onClick替換為ONCLICK,但是點(diǎn)擊后的效果不變。
也就是說,我們將基于React自制一套事件系統(tǒng),他的事件名的書寫規(guī)則是形如「ONXXX」的全大寫形式。
實(shí)現(xiàn)SyntheticEvent
首先,我們來實(shí)現(xiàn)SyntheticEvent(合成事件)。
SyntheticEvent是瀏覽器原生事件對(duì)象的一層封裝。兼容所有瀏覽器,同時(shí)擁有和瀏覽器原生事件相同的API,如stopPropagation()和preventDefault()。
SyntheticEvent存在的目的是抹平瀏覽器間在事件對(duì)象間的差異,但是對(duì)于不支持某一事件的瀏覽器,SyntheticEvent并不會(huì)提供polyfill(因?yàn)檫@會(huì)顯著增大ReactDOM的體積)。
我們的實(shí)現(xiàn)很簡單:
class?SyntheticEvent?{
??constructor(e)?{
????this.nativeEvent?=?e;
??}
??stopPropagation()?{
????this._stopPropagation?=?true;
????if?(this.nativeEvent.stopPropagation)?{
??????this.nativeEvent.stopPropagation();
????}
??}
}
接收「原生事件對(duì)象」,返回一個(gè)包裝對(duì)象。原生事件對(duì)象會(huì)保存在nativeEvent屬性中。
同時(shí),實(shí)現(xiàn)了stopPropagation方法。
實(shí)際的SyntheticEvent會(huì)包含更多屬性和方法,這里為了演示目的簡化了
實(shí)現(xiàn)事件傳播機(jī)制
事件傳播機(jī)制的實(shí)現(xiàn)步驟如下:
在根節(jié)點(diǎn)綁定
事件類型對(duì)應(yīng)的事件回調(diào),所有子孫節(jié)點(diǎn)觸發(fā)該類事件最終都會(huì)委托給「根節(jié)點(diǎn)的事件回調(diào)」處理。尋找觸發(fā)事件的DOM節(jié)點(diǎn),找到其對(duì)應(yīng)的
FiberNode(即虛擬DOM節(jié)點(diǎn))收集從當(dāng)前
FiberNode到根FiberNode之間所有注冊(cè)的「該事件對(duì)應(yīng)回調(diào)」反向遍歷并執(zhí)行一遍所有收集的回調(diào)(模擬捕獲階段的實(shí)現(xiàn))
正向遍歷并執(zhí)行一遍所有收集的回調(diào)(模擬冒泡階段的實(shí)現(xiàn))
首先,實(shí)現(xiàn)第一步:
//?步驟1
const?addEvent?=?(container,?type)?=>?{
??container.addEventListener(type,?(e)?=>?{
????//?dispatchEvent是需要實(shí)現(xiàn)的“根節(jié)點(diǎn)的事件回調(diào)”
????dispatchEvent(e,?type.toUpperCase(),?container);
??});
};
在入口處注冊(cè)點(diǎn)擊回調(diào):
const?root?=?document.querySelector("#root");
ReactDOM.render(jsx,?root);
//?增加如下代碼
addEvent(root,?"click");
接下來實(shí)現(xiàn)「根節(jié)點(diǎn)的事件回調(diào)」:
const?dispatchEvent?=?(e,?type)?=>?{
??//?包裝合成事件
??const?se?=?new?SyntheticEvent(e);
??const?ele?=?e.target;
??
??//?比較hack的方法,通過DOM節(jié)點(diǎn)找到對(duì)應(yīng)的FiberNode
??let?fiber;
??for?(let?prop?in?ele)?{
????if?(prop.toLowerCase().includes("fiber"))?{
??????fiber?=?ele[prop];
????}
??}
??
??//?第三步:收集路徑中“該事件的所有回調(diào)函數(shù)”
??const?paths?=?collectPaths(type,?fiber);
??
??//?第四步:捕獲階段的實(shí)現(xiàn)
??triggerEventFlow(paths,?type?+?"CAPTURE",?se);
??
??//?第五步:冒泡階段的實(shí)現(xiàn)
??if?(!se._stopPropagation)?{
????triggerEventFlow(paths.reverse(),?type,?se);
??}
};
接下來收集路徑中「該事件的所有回調(diào)函數(shù)」。
收集路徑中的事件回調(diào)函數(shù)
實(shí)現(xiàn)的思路是:從當(dāng)前FiberNode一直向上遍歷,直到根FiberNode。收集遍歷過程中的FiberNode.memoizedProps屬性內(nèi)保存的「對(duì)應(yīng)事件回調(diào)」:
const?collectPaths?=?(type,?begin)?=>?{
??const?paths?=?[];
??
??//?不是根FiberNode的話,就一直向上遍歷
??while?(begin.tag?!==?3)?{
????const?{?memoizedProps,?tag?}?=?begin;
????
????//?5代表DOM節(jié)點(diǎn)對(duì)應(yīng)FiberNode
????if?(tag?===?5)?{
??????const?eventName?=?("on"?+?type).toUpperCase();
??????
??????//?如果包含對(duì)應(yīng)事件回調(diào),保存在paths中
??????if?(memoizedProps?&&?Object.keys(memoizedProps).includes(eventName))?{
????????const?pathNode?=?{};
????????pathNode[type.toUpperCase()]?=?memoizedProps[eventName];
????????paths.push(pathNode);
??????}
????}
????begin?=?begin.return;
??}
??
??return?paths;
};
得到的paths結(jié)構(gòu)類似如下:

捕獲階段的實(shí)現(xiàn)
由于我們是從目標(biāo)FiberNode向上遍歷,所以收集到的回調(diào)的順序是:
[目標(biāo)事件回調(diào),?某個(gè)祖先事件回調(diào),?某個(gè)更久遠(yuǎn)的祖先回調(diào)?...]
要模擬捕獲階段的實(shí)現(xiàn),需要從后向前遍歷數(shù)組并執(zhí)行回調(diào)。
遍歷的方法如下:
const?triggerEventFlow?=?(paths,?type,?se)?=>?{
??//?從后向前遍歷
??for?(let?i?=?paths.length;?i--;?)?{
????const?pathNode?=?paths[i];
????const?callback?=?pathNode[type];
????
????if?(callback)?{
??????//?存在回調(diào)函數(shù),傳入合成事件,執(zhí)行
??????callback.call(null,?se);
????}
????if?(se._stopPropagation)?{
??????//?如果執(zhí)行了se.stopPropagation(),取消接下來的遍歷
??????break;
????}
??}
};
注意,我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">SyntheticEvent中實(shí)現(xiàn)的stopPropagation方法,調(diào)用后會(huì)阻止遍歷的繼續(xù)。
冒泡階段的實(shí)現(xiàn)
有了捕獲階段的實(shí)現(xiàn)經(jīng)驗(yàn),冒泡階段很容易實(shí)現(xiàn),只需將paths反向后再遍歷一遍就行。
總結(jié)
React事件系統(tǒng)的核心包括兩部分:
SyntheticEvent
事件傳播機(jī)制
事件傳播機(jī)制由5個(gè)步驟實(shí)現(xiàn)。
總的來說,就是這么簡單。
參考資料
在線DEMO地址: https://codesandbox.io/s/optimistic-torvalds-9ufc5?file=/src/index.js
