60 行代碼實現 React 的事件系統
大家好,我卡頌。
由於如下原因,React
的事件系統代碼量很大:
-
需要抹平不同瀏覽器的差異
-
與內部的**「優先級機制」**綁定
-
需要考慮所有瀏覽器事件
但如果抽絲剝繭會發現,事件系統的核心只有兩個模塊:
-
SyntheticEvent(合成事件)
-
模擬實現的事件傳播機制
本文會用 60 行代碼實現這兩個模塊,讓你快速瞭解React
事件系統的原理。
在線 DEMO 地址 [1]
Demo 的效果
對於如下這段JSX
:
const jsx = (
<section onClick={(e) => console.log("click section")}>
<h3>你好</h3>
<button
onClick={(e) => {
// e.stopPropagation();
console.log("click button");
}}
>
點擊
</button>
</section>
);
在瀏覽器中渲染:
const root = document.querySelector("#root");
ReactDOM.render(jsx, root);
點擊按鈕,會依次打印:
click button
click section
如果在button
的點擊回調中增加e.stopPropagation()
,點擊後會打印:
click button
我們的目標是將JSX
中的onClick
替換爲ONCLICK
,但是點擊後的效果不變。
也就是說,我們將基於React
自制一套事件系統,他的事件名的書寫規則是形如**「ONXXX」**的全大寫
形式。
實現 SyntheticEvent
首先,我們來實現SyntheticEvent
(合成事件)。
SyntheticEvent
是瀏覽器原生事件對象的一層封裝。兼容所有瀏覽器,同時擁有和瀏覽器原生事件相同的 API,如stopPropagation()
和preventDefault()
。
SyntheticEvent
存在的目的是抹平瀏覽器間在事件對象
間的差異,但是對於不支持某一事件的瀏覽器,SyntheticEvent
並不會提供polyfill
(因爲這會顯著增大ReactDOM
的體積)。
我們的實現很簡單:
class SyntheticEvent {
constructor(e) {
this.nativeEvent = e;
}
stopPropagation() {
this._stopPropagation = true;
if (this.nativeEvent.stopPropagation) {
this.nativeEvent.stopPropagation();
}
}
}
接收**「原生事件對象」**,返回一個包裝對象。原生事件對象
會保存在nativeEvent
屬性中。
同時,實現了stopPropagation
方法。
實際的 SyntheticEvent 會包含更多屬性和方法,這裏爲了演示目的簡化了
實現事件傳播機制
事件傳播機制的實現步驟如下:
-
在根節點綁定
事件類型
對應的事件回調,所有子孫節點觸發該類事件最終都會委託給**「根節點的事件回調」**處理。 -
尋找觸發事件的 DOM 節點,找到其對應的
FiberNode
(即虛擬 DOM 節點) -
收集從當前
FiberNode
到根FiberNode
之間所有註冊的**「該事件對應回調」** -
反向遍歷並執行一遍所有收集的回調(模擬捕獲階段的實現)
-
正向遍歷並執行一遍所有收集的回調(模擬冒泡階段的實現)
首先,實現第一步:
// 步驟1
const addEvent = (container, type) => {
container.addEventListener(type, (e) => {
// dispatchEvent是需要實現的“根節點的事件回調”
dispatchEvent(e, type.toUpperCase(), container);
});
};
在入口處註冊點擊回調
:
const root = document.querySelector("#root");
ReactDOM.render(jsx, root);
// 增加如下代碼
addEvent(root, "click");
接下來實現**「根節點的事件回調」**:
const dispatchEvent = (e, type) => {
// 包裝合成事件
const se = new SyntheticEvent(e);
const ele = e.target;
// 比較hack的方法,通過DOM節點找到對應的FiberNode
let fiber;
for (let prop in ele) {
if (prop.toLowerCase().includes("fiber")) {
fiber = ele[prop];
}
}
// 第三步:收集路徑中“該事件的所有回調函數”
const paths = collectPaths(type, fiber);
// 第四步:捕獲階段的實現
triggerEventFlow(paths, type + "CAPTURE", se);
// 第五步:冒泡階段的實現
if (!se._stopPropagation) {
triggerEventFlow(paths.reverse(), type, se);
}
};
接下來收集路徑中**「該事件的所有回調函數」**。
收集路徑中的事件回調函數
實現的思路是:從當前FiberNode
一直向上遍歷,直到根FiberNode
。收集遍歷過程中的FiberNode.memoizedProps
屬性內保存的**「對應事件回調」**:
const collectPaths = (type, begin) => {
const paths = [];
// 不是根FiberNode的話,就一直向上遍歷
while (begin.tag !== 3) {
const { memoizedProps, tag } = begin;
// 5代表DOM節點對應FiberNode
if (tag === 5) {
const eventName = ("on" + type).toUpperCase();
// 如果包含對應事件回調,保存在paths中
if (memoizedProps && Object.keys(memoizedProps).includes(eventName)) {
const pathNode = {};
pathNode[type.toUpperCase()] = memoizedProps[eventName];
paths.push(pathNode);
}
}
begin = begin.return;
}
return paths;
};
得到的paths
結構類似如下:
捕獲階段的實現
由於我們是從目標FiberNode
向上遍歷,所以收集到的回調的順序是:
[目標事件回調, 某個祖先事件回調, 某個更久遠的祖先回調 ...]
要模擬捕獲階段
的實現,需要從後向前遍歷數組並執行回調。
遍歷的方法如下:
const triggerEventFlow = (paths, type, se) => {
// 從後向前遍歷
for (let i = paths.length; i--; ) {
const pathNode = paths[i];
const callback = pathNode[type];
if (callback) {
// 存在回調函數,傳入合成事件,執行
callback.call(null, se);
}
if (se._stopPropagation) {
// 如果執行了se.stopPropagation(),取消接下來的遍歷
break;
}
}
};
注意,我們在SyntheticEvent
中實現的stopPropagation
方法,調用後會阻止遍歷的繼續。
冒泡階段的實現
有了捕獲階段
的實現經驗,冒泡階段很容易實現,只需將paths
反向後再遍歷一遍就行。
總結
React
事件系統的核心包括兩部分:
-
SyntheticEvent
-
事件傳播機制
事件傳播機制
由 5 個步驟實現。
總的來說,就是這麼簡單。
參考資料
[1]
在線 DEMO 地址: https://codesandbox.io/s/optimistic-torvalds-9ufc5?file=/src/index.js
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/UC022AC-O506ueFykZyWbQ