![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5CNkBTNlNTYjhDO5MDZzMzMhRGN3MjN3EjNhljYycjMk9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
當我們在元件上設定事件處理器時,React并不會在該DOM元素上直接綁定事件處理器. React内部自定義了一套事件系統,在這個系統上統一進行事件訂閱和分發.
具體來講,React利用事件委托機制在Document上統一監聽DOM事件,再根據觸發的target将事件分發到具體的元件執行個體。另外上面e是一個合成事件對象(SyntheticEvent), 而不是原始的DOM事件對象.
文章大綱- 那為什麼要自定義一套事件系統?
- 基本概念
- 整體的架構
- 事件分類與優先級
- 實作細節
- 事件是如何綁定的?
- 事件是如何分發的?
- 事件觸發排程
- 插件是如何處理事件?
- 批量執行
- 未來
- 初探Responder的建立
- react-events意義何在?
- 擴充閱讀
截止本文寫作時,React版本是16.8.6
那為什麼要自定義一套事件系統?
如果了解過Preact(筆者之前寫過一篇文章解析Preact的源碼),Preact裁剪了很多React的東西,其中包括事件機制,Preact是直接在DOM元素上進行事件綁定的。
在研究一個事物之前,我首先要問為什麼?了解它的動機,才有利于你對它有本質的認識。
React自定義一套事件系統的動機有以下幾個:
- 1. 抹平浏覽器之間的相容性差異 。 這是估計最原始的動機,React根據W3C 規範來定義這些合成事件(SyntheticEvent), 意在抹平浏覽器之間的差異。
另外React還會試圖通過其他相關事件來模拟一些低版本不相容的事件, 這才是‘合成’的本來意思吧?。
- 2. 事件‘合成’, 即事件自定義 。事件合成除了處理相容性問題,還可以用來自定義進階事件,比較典型的是React的onChange事件,它為表單元素定義了統一的值變動事件。另外第三方也可以通過React的事件插件機制來合成自定義事件,盡管很少人這麼做。
- 3. 抽象跨平台事件機制 。 和VirtualDOM的意義差不多,VirtualDOM抽象了跨平台的渲染方式,那麼對應的SyntheticEvent目的也是想提供一個抽象的跨平台事件機制。
- 4. React打算做更多優化 。比如利用事件委托機制,大部分事件最終綁定到了Document,而不是DOM節點本身. 這樣簡化了DOM事件處理邏輯,減少了記憶體開銷. 但這也意味着, React需要自己模拟一套事件冒泡的機制 。
- 5. React打算幹預事件的分發 。v16引入Fiber架構,React為了優化使用者的互動體驗,會幹預事件的分發。不同類型的事件有不同的優先級,比如高優先級的事件可以中斷渲染,讓使用者代碼可以及時響應使用者互動。
Ok, 後面我們會深入了解React的事件實作,我會盡量不貼代碼,用流程圖說話。
基本概念
整體的架構
- ReactEventListener - 事件處理器. 在這裡進行事件處理器的綁定。當DOM觸發事件時,會從這裡開始排程分發到React元件樹
- ReactEventEmitter - 暴露接口給React元件層用于添加事件訂閱
- EventPluginHub - 如其名,這是一個‘插件插槽’,負責管理和注冊各種插件。在事件分發時,調用插件來生成合成事件
- Plugin - React事件系統使用了插件機制來管理不同行為的事件。這些插件會處理自己感興趣的事件類型,并生成合成事件對象。目前ReactDOM有以下幾種插件類型:
- SimpleEventPlugin - 簡單事件, 處理一些比較通用的事件類型,例如click、input、keyDown、mouseOver、mouseOut、pointerOver、pointerOut
- EnterLeaveEventPlugin - mouseEnter/mouseLeave和pointerEnter/pointerLeave這兩類事件比較特殊, 和
事件相比, 它們不支援事件冒泡,*over/*leave
會給所有進入的元素發送事件, 行為有點類似于*enter
; 而:hover
*over
在進入元素後,還會冒泡通知其上級. 可以通過這個執行個體觀察enter和over的差別.
如果樹層次比較深,大量的mouseenter觸發可能導緻性能問題。另外其不支援冒泡,無法在Document完美的監聽和分發, 是以ReactDOM使用
事件來模拟這些*over/*out
。*enter/*leave
- ChangeEventPlugin
- change事件是React的一個自定義事件,旨在規範化表單元素的變動事件。
它支援這些表單元素: input, textarea, select
- SelectEventPlugin - 和change事件一樣,React為表單元素規範化了select(選擇範圍變動)事件,适用于input、textarea、contentEditable元素.
- BeforeInputEventPlugin - beforeinput事件以及composition事件處理。
本文主要會關注
SimpleEventPlugin
的實作,有興趣的讀者可以自己閱讀React的源代碼.
- EventPropagators 按照DOM事件傳播的兩個階段,周遊React元件樹,并收集所有元件的事件處理器.
- EventBatching 負責批量執行事件隊列和事件處理器,處理事件冒泡。
- SyntheticEvent 這是‘合成’事件的基類,可以對應DOM的Event對象。隻不過React為了減低記憶體損耗和垃圾回收,使用一個對象池來建構和釋放事件對象, 也就是說SyntheticEvent不能用于異步引用,它在同步執行完事件處理器後就會被釋放。
SyntheticEvent也有子類,和DOM具體事件類型一一比對:
- SyntheticAnimationEvent
- SyntheticClipboardEvent
- SyntheticCompositionEvent
- SyntheticDragEvent
- SyntheticFocusEvent
- SyntheticInputEvent
- SyntheticKeyboardEvent
- SyntheticMouseEvent
- SyntheticPointerEvent
- SyntheticTouchEvent
- ....
事件分類與優先級
SimpleEventPlugin将事件類型劃分成了三類, 對應不同的優先級(
優先級由低到高):
- DiscreteEvent 離散事件. 例如blur、focus、 click、 submit、 touchStart. 這些事件都是離散觸發的
- UserBlockingEvent 使用者阻塞事件. 例如touchMove、mouseMove、scroll、drag、dragOver等等。這些事件會'阻塞'使用者的互動。
- ContinuousEvent 可連續事件。例如load、error、loadStart、abort、animationEnd. 這個優先級最高,也就是說它們應該是立即同步執行的,這就是Continuous的意義,即可連續的執行,不被打斷.
可能要先了解一下React排程(Schedule)的優先級,才能了解這三種事件類型的差別。截止到本文寫作時,React有5個優先級級别:
-
- 這個優先級的任務會同步執行, 或者說要馬上執行且不能中斷Immediate
-
(250ms timeout) 這些任務一般是使用者互動的結果, 需要即時得到回報 .UserBlocking
-
(5s timeout) 應對哪些不需要立即感受到的任務,例如網絡請求Normal
-
(10s timeout) 這些任務可以放後,但是最終應該得到執行. 例如分析通知Low
-
(no timeout) 一些沒有必要做的任務 (e.g. 比如隐藏的内容).Idle
目前ContinuousEvent對應的是Immediate優先級; UserBlockingEvent對應的是UserBlocking(需要手動開啟); 而DiscreteEvent對應的也是UserBlocking, 隻不過它在執行之前,先會執行完其他Discrete任務。
本文不會深入React Fiber架構的細節,有興趣的讀者可以閱讀文末的擴充閱讀清單.
實作細節
現在開始進入文章正題,React是怎麼實作事件機制?主要分為兩個部分:
綁定和
分發.
事件是如何綁定的?
為了避免後面繞暈了,有必要先了解一下React事件機制中的插件協定。 每個插件的結構如下:
export type EventTypes = {[key: string]: DispatchConfig};
// 插件接口
export type PluginModule<NativeEvent> = {
eventTypes: EventTypes, // 聲明插件支援的事件類型
extractEvents: ( // 對事件進行處理,并傳回合成事件對象
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: NativeEvent,
nativeEventTarget: EventTarget,
) => ?ReactSyntheticEvent,
tapMoveThreshold?: number,
};
eventTypes 聲明該插件負責的事件類型, 它通過
DispatchConfig
來描述:
export type DispatchConfig = {
dependencies: Array<TopLevelType>, // 依賴的原生事件,表示關聯這些事件的觸發. ‘簡單事件’一般隻有一個,複雜事件如onChange會監聽多個, 如下圖
phasedRegistrationNames?: { // 兩階段props事件注冊名稱, React會根據這些名稱在元件執行個體中查找對應的props事件處理器
bubbled: string, // 冒泡階段, 如onClick
captured: string, // 捕獲階段,如onClickCapture
},
registrationName?: string // props事件注冊名稱, 比如onMouseEnter這些不支援冒泡的事件類型,隻會定義 registrationName,不會定義phasedRegistrationNames
eventPriority: EventPriority, // 事件的優先級,上文已經介紹過了
};
看一下執行個體:
上面列舉了三個典型的EventPlugin:
- SimpleEventPlugin - 簡單事件最好了解,它們的行為都比較通用,沒有什麼Trick, 例如不支援事件冒泡、不支援在Document上綁定等等. 和原生DOM事件是一一對應的關系,比較好處理.
- EnterLeaveEventPlugin - 從上圖可以看出來,
和mouseEnter
依賴的是mouseLeave
和mouseout
事件。也就是說mouseover
事件在React中是通過*Enter/*Leave
事件來模拟的。這樣做的好處是可以在document上面進行委托監聽,還有避免*Over/*Out
一些奇怪而不實用的行為。*Enter/*Leave
- ChangeEventPlugin - onChange是React的一個自定義事件,可以看出它依賴了多種原生DOM事件類型來模拟onChange事件.
另外每個插件還會定義
extractEvents
方法,這個方法接受事件名稱、原生DOM事件對象、事件觸發的DOM元素以及React元件執行個體, 傳回一個合成事件對象,如果傳回空則表示不作處理. 關于extractEvents的細節會在下一節闡述.
在ReactDOM啟動時就會向
EventPluginHub
注冊這些插件:
EventPluginHubInjection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
Ok, 回到正題,事件是怎麼綁定的呢? 打個斷點看一下調用棧:
前面調用棧關于React樹如何更新和渲染就不在本文的範圍内了,通過調用棧可以看出React在props初始化和更新時會進行事件綁定。這裡先看一下流程圖,忽略雜亂的跳轉:
- 1. 在props初始化和更新時會進行事件綁定 。首先React會判斷元素是否是
, 媒體類型的事件是無法在Document監聽的,是以會直接在元素上進行綁定媒體類型
- 2. 反之就在Document上綁定 . 這裡面需要兩個資訊,一個就是上文提到的'事件依賴清單', 比如
依賴onMouseEnter
; 第二個是ReactBrowserEventEmitter維護的'已訂閱事件表'。 事件處理器隻需在Document訂閱一次,是以相比在每個元素上訂閱事件會節省很多資源 .mouseover/mouseout
代碼大概如下:
export function listenTo(
registrationName: string, // 注冊名稱,如onClick
mountAt: Document | Element | Node, // 元件樹容器,一般是Document
): void {
const listeningSet = getListeningSetForElement(mountAt); // 已訂閱事件表
const dependencies = registrationNameDependencies[registrationName]; // 事件依賴
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!listeningSet.has(dependency)) { // 未訂閱
switch (dependency) {
// ... 特殊的事件監聽處理
default:
const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(dependency, mountAt); // 設定事件處理器
}
break;
}
listeningSet.add(dependency); // 更新已訂閱表
}
}
}
- 接下來就是根據事件的'優先級'和'捕獲階段'(是否是capture)來設定事件處理器 :
function trapEventForPluginEventSystem(
element: Document | Element | Node, // 綁定到元素,一般是Document
topLevelType: DOMTopLevelEventType, // 事件名稱
capture: boolean,
): void {
let listener;
switch (getEventPriority(topLevelType)) {
// 不同優先級的事件類型,有不同的事件處理器進行分發, 下文會詳細介紹
case DiscreteEvent: // ⚛️離散事件
listener = dispatchDiscreteEvent.bind(
null,
topLevelType,
PLUGIN_EVENT_SYSTEM,
);
break;
case UserBlockingEvent: // ⚛️使用者阻塞事件
listener = dispatchUserBlockingUpdate.bind(
null,
topLevelType,
PLUGIN_EVENT_SYSTEM,
);
break;
case ContinuousEvent: // ⚛️可連續事件
default:
listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
break;
}
const rawEventName = getRawEventName(topLevelType);
if (capture) { // 綁定事件處理器到元素
addEventCaptureListener(element, rawEventName, listener);
} else {
addEventBubbleListener(element, rawEventName, listener);
}
}
事件綁定的過程還比較簡單, 接下來看看事件是如何分發的。
事件是如何分發的?
按慣例還是先上流程圖:
事件觸發排程
通過上面的
trapEventForPluginEventSystem
函數可以知道,不同的事件類型有不同的事件處理器, 它們的差別是排程的優先級不一樣:
// 離散事件
// discrentUpdates 在UserBlocking優先級中執行
function dispatchDiscreteEvent(topLevelType, eventSystemFlags, nativeEvent) {
flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, nativeEvent);
}
// 阻塞事件
function dispatchUserBlockingUpdate(
topLevelType,
eventSystemFlags,
nativeEvent,
) {
// 如果開啟了enableUserBlockingEvents, 則在UserBlocking優先級中排程,
// 開啟enableUserBlockingEvents可以防止饑餓問題,因為阻塞事件中有scroll、mouseMove這類頻繁觸發的事件
// 否則同步執行
if (enableUserBlockingEvents) {
runWithPriority(
UserBlockingPriority,
dispatchEvent.bind(null, topLevelType, eventSystemFlags, nativeEvent),
);
} else {
dispatchEvent(topLevelType, eventSystemFlags, nativeEvent);
}
}
// 可連續事件則直接同步調用dispatchEvent
最終不同的事件類型都會調用
dispatchEvent
函數.
dispatchEvent
中會從DOM原生事件對象擷取事件觸發的target,再根據這個target擷取關聯的React節點執行個體.
export function dispatchEvent(topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent): void {
// 擷取事件觸發的目标DOM
const nativeEventTarget = getEventTarget(nativeEvent);
// 擷取離該DOM最近的元件執行個體(隻能是DOM元素元件)
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
// ....
dispatchEventForPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst);
}
接着(中間還有一些步驟,這裡忽略)會調用
EventPluginHub
的
runExtractedPluginEventsInBatch
,這個方法周遊插件清單來處理事件,生成一個SyntheticEvent清單:
export function runExtractedPluginEventsInBatch(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
) {
// 周遊插件清單, 調用插件的extractEvents,生成SyntheticEvent清單
const events = extractPluginEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
// 事件處理器執行, 見後文批量執行
runEventsInBatch(events);
}
插件是如何處理事件?
現在來看看插件是如何處理事件的,我們以
SimpleEventPlugin
為例:
const SimpleEventPlugin: PluginModule<MouseEvent> & {
getEventPriority: (topLevelType: TopLevelType) => EventPriority,
} = {
eventTypes: eventTypes,
// 抽取事件對象
extractEvents: function(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: MouseEvent,
nativeEventTarget: EventTarget,
): null | ReactSyntheticEvent {
// 事件配置
const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
// 1️⃣ 根據事件類型擷取SyntheticEvent子類事件構造器
let EventConstructor;
switch (topLevelType) {
// ...
case DOMTopLevelEventTypes.TOP_KEY_DOWN:
case DOMTopLevelEventTypes.TOP_KEY_UP:
EventConstructor = SyntheticKeyboardEvent;
break;
case DOMTopLevelEventTypes.TOP_BLUR:
case DOMTopLevelEventTypes.TOP_FOCUS:
EventConstructor = SyntheticFocusEvent;
break;
// ... 省略
case DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE:
// ...
case DOMTopLevelEventTypes.TOP_POINTER_UP:
EventConstructor = SyntheticPointerEvent;
break;
default:
EventConstructor = SyntheticEvent;
break;
}
// 2️⃣ 構造事件對象, 從對象池中擷取
const event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
);
// 3️⃣ 根據DOM事件傳播的順序擷取使用者事件處理器
accumulateTwoPhaseDispatches(event);
return event;
},
};
SimpleEventPlugin
的
extractEvents
主要做以下三個事情:
- 1️⃣ 根據事件的類型确定SyntheticEvent構造器
- 2️⃣ 構造SyntheticEvent對象。
- 3️⃣ 根據DOM事件傳播的順序擷取使用者事件處理器清單
。
這也意味着,
在事件處理器同步執行完後,SyntheticEvent對象就會馬上被回收,所有屬性都會無效。是以一般不會在異步操作中通路SyntheticEvent事件對象。你也可以通過以下方法來保持事件對象的引用:
- 調用
方法,告訴React不要回收到對象池SyntheticEvent#persist()
- 直接引用
, nativeEvent是可以持久引用的,不過為了不打破抽象,建議不要直接引用nativeEventSyntheticEvent#nativeEvent
建構完SyntheticEvent對象後,就需要
周遊元件樹來擷取訂閱該事件的使用者事件處理器了:
function accumulateTwoPhaseDispatchesSingle(event) {
// 以_targetInst為基點, 按照DOM事件傳播的順序周遊元件樹
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
周遊方法其實很簡單:
export function traverseTwoPhase(inst, fn, arg) {
const path = [];
while (inst) { // 從inst開始,向上級回溯
path.push(inst);
inst = getParent(inst);
}
let i;
// 捕獲階段,先從最頂層的父元件開始, 向下級傳播
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
// 冒泡階段,從inst,即事件觸發點開始, 向上級傳播
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
accumulateDirectionalDispatches
函數則是簡單查找目前節點是否有對應的事件處理器:
function accumulateDirectionalDispatches(inst, phase, event) {
// 檢查是否存在事件處理器
const listener = listenerAtPhase(inst, event, phase);
// 所有處理器都放入到_dispatchListeners隊列中,後續批量執行這個隊列
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
例如下面的元件樹, 周遊過程是這樣的:
最終計算出來的
_dispatchListeners
隊列是這樣的:
[handleB, handleC, handleA]
批量執行
周遊執行插件後,會得到一個SyntheticEvent清單,
runEventsInBatch
就是批量執行這些事件中的
_dispatchListeners
事件隊列
export function runEventsInBatch(
events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
) {
// ...
forEachAccumulated(processingEventQueue, executeDispatchesAndRelease);
}
//
const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
if (event) {
// 按順序執行_dispatchListeners
//
executeDispatchesInOrder(event);
// 如果沒有調用persist()方法則直接回收
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
export function executeDispatchesInOrder(event) {
// 周遊dispatchListeners
for (let i = 0; i < dispatchListeners.length; i++) {
// 通過調用 stopPropagation 方法可以禁止執行下一個事件處理器
if (event.isPropagationStopped()) {
break;
}
// 執行事件處理器
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
}
}
OK, 到這裡React的事件機制就基本介紹完了,這裡隻是簡單了介紹了一下
SimpleEventPlugin
, 實際代碼中還有很多事件處理的細節,限于篇幅,本文就不展開去講了。有興趣的讀者可以親自去觀摩React的源代碼.
未來
React内部有一個實驗性的事件API,React内部稱為
React Flare
、正式名稱是
react-events
,
通過這個API可以實作跨平台、跨裝置的進階事件封裝.
react-events定義了一個
事件響應器(Event Responders)的概念,這個事件響應器可以捕獲子元件樹或應用根節點的事件,然後轉換為自定義事件.
比較典型的進階事件是press、longPress、swipe這些手勢。通常我們需要自己或者利用第三方庫來實作這一套手勢識别, 例如
import Gesture from 'rc-gesture';
ReactDOM.render(
<Gesture
onTap={handleTap}
onSwipe={onSwipe}
onPinch={handlePinch}
>
<div>container</div>
</Gesture>,
container);
那麼react-events的目的就是
提供一套通用的事件機制給開發者來實作'進階事件'的封裝, 甚至實作事件的跨平台、跨裝置, 現在你可以通過react-events來封裝這些手勢事件.
react-events除了核心的
Responder
接口,還封裝了一些内置子產品, 實作跨平台的、常用的進階事件封裝:
- Focus module
- Hover module
- Press module
- FocusScope module
- Input module
- KeyBoard module
- Drag module
- Pan module
- Scroll module
- Swipe module
舉
Press
子產品作為例子, Press子產品會響應它包裹的元素的press事件。press事件包括onContextMenu、onLongPress、onPress、onPressEnd、onPressMove、onPressStart等等. 其底層通過mouse、pen、touch、trackpad等事件來轉換.
看看使用示例:
import { PressResponder, usePressListener } from 'react-events/press';
const Button = (props) => (
const listener = usePressListener({ // ⚛️通過hooks建立Responder
onPressStart,
onPress,
onPressEnd,
})
return (
<div listeners={listener}>
{subtrees}
</div>
);
);
react-events的運作流程圖如下,
事件響應器(Event Responders)會挂載到host節點,它會在host節點監聽host或子節點分發的原生事件(DOM或React Native), 并将它們轉換/合并成進階的事件:
你可以通過這個Codesanbox玩一下
react-events
:
Edit react-events-playground
初探Responder的建立
我們挑一個簡單的子產品來了解一些react-events的核心API, 目前最簡單的是Keyboard子產品. Keyboard子產品的目的就是規範化keydown和keyup事件對象的key屬性(部分浏覽器key屬性的行為不一樣),它的實作如下:
/**
* 定義Responder的實作
*/
const keyboardResponderImpl = {
/**
* 1️⃣定義Responder需要監聽的子樹的DOM事件,對于Keyboard來說是['keydown', 'keyup';]
*/
targetEventTypes,
/**
* 2️⃣監聽子樹觸發的事件
*/
onEvent(
event: ReactDOMResponderEvent, // 包含了目前觸發事件的相關資訊,如原生事件對象,事件觸發的節點,事件類型等等
context: ReactDOMResponderContext, // Responder的上下文,給Responder提供了一些方法來驅動事件分發
props: KeyboardResponderProps, // 傳遞給Responder的props
): void {
const {responderTarget, type} = event;
if (props.disabled) {
return;
}
if (type === 'keydown') {
dispatchKeyboardEvent(
'onKeyDown',
event,
context,
'keydown',
((responderTarget: any): Element | Document),
);
} else if (type === 'keyup') {
dispatchKeyboardEvent(
'onKeyUp',
event,
context,
'keyup',
((responderTarget: any): Element | Document),
);
}
},
};
再來看看dispatchKeyboardEvent:
function dispatchKeyboardEvent(
eventPropName: string,
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
type: KeyboardEventType,
target: Element | Document,
): void {
// ⚛️建立合成事件對象,在這個函數中會規範化事件的key屬性
const syntheticEvent = createKeyboardEvent(event, context, type, target);
// ⚛️通過Responder上下文分發事件
context.dispatchEvent(eventPropName, syntheticEvent, DiscreteEvent);
}
導出Responder:
// ⚛️createResponder把keyboardResponderImpl轉換為元件形式
export const KeyboardResponder = React.unstable_createResponder(
'Keyboard',
keyboardResponderImpl,
);
// ⚛️建立hooks形式
export function useKeyboardListener(props: KeyboardListenerProps): void {
React.unstable_useListener(KeyboardResponder, props);
}
現在讀者應該對
Responder的職責有了一些基本的了解,它主要做以下幾件事情:
- 聲明要監聽的原生事件(如DOM), 如上面的
targetEventTypes
- 處理和轉換合成事件,如上面的
onEvent
- 建立并分發自定義事件。如上面的
context.dispatchEvent
和上面的Keyboard子產品相比,現實中的很多進階事件,如longPress, 它們的實作則要複雜得多. 它們可能要維持一定的
狀态、也可能要獨占響應的
所有權(即同一時間隻能有一個Responder可以對事件進行處理, 這個常用于移動端觸摸手勢,例如React Native的GestureResponderSystem)。
react-events目前都考慮了這些場景, 看一下API概覽:
詳細可以看react-events官方倉庫
react-events意義何在?
上文提到了React事件内部采用了插件機制,來實作事件處理和合成,比較典型的就是onChange事件。onChange事件其實就是所謂的‘進階事件’,它是通過表單元件的各種原生事件來模拟的。
也就是說,React通過插件機制本質上是可以實作進階事件的封裝的。但是如果讀者看過源代碼,就會覺得裡面邏輯比較繞,而且依賴React的很多内部實作。
是以這種内部的插件機制并不是面向普通開發者的。
react-events
接口就簡單很多了,它屏蔽了很多内部細節,面向普通開發者。我們可以利用它來實作高性能的自定義事件分發,更大的意義是通過它可以實作跨平台/裝置的事件處理方式.
目前react-events還是實驗階段,特性是預設關閉,API可能會出現變更, 是以不建議在生産環境使用。可以通過這個Issue來關注它的進展。
最後贊歎一下React團隊的創新能力!
完!
擴充閱讀
- input事件中文觸發多次問題研究
- 完全了解React Fiber
- Lin Clark – A Cartoon Intro to Fiber – React Conf 2017
- Scheduling in React
- [Umbrella] React Flare
- react-events