當咱們在組件上設置事件處理器時,React並不會在該DOM元素上直接綁定事件處理器. React內部自定義了一套事件系統,在這個系統上統一進行事件訂閱和分發.react
具體來說,React利用事件委託機制在Document上統一監聽DOM事件,再根據觸發的target將事件分發到具體的組件實例。另外上面e是一個合成事件對象(SyntheticEvent), 而不是原始的DOM事件對象.git
文章大綱github
截止本文寫做時,React版本是16.8.6segmentfault
若是瞭解過Preact(筆者以前寫過一篇文章解析Preact的源碼),Preact裁剪了不少React的東西,其中包括事件機制,Preact是直接在DOM元素上進行事件綁定的。瀏覽器
在研究一個事物以前,我首先要問爲何?瞭解它的動機,纔有利於你對它有本質的認識。網絡
React自定義一套事件系統的動機有如下幾個:架構
1. 抹平瀏覽器之間的兼容性差別。 這是估計最原始的動機,React根據W3C 規範來定義這些合成事件(SyntheticEvent), 意在抹平瀏覽器之間的差別。異步
另外React還會試圖經過其餘相關事件來模擬一些低版本不兼容的事件, 這纔是‘合成’的原本意思吧?。函數
2. 事件‘合成’, 即事件自定義。事件合成除了處理兼容性問題,還能夠用來自定義高級事件,比較典型的是React的onChange事件,它爲表單元素定義了統一的值變更事件。另外第三方也能夠經過React的事件插件機制來合成自定義事件,儘管不多人這麼作。post
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/*out
事件相比, 它們不支持事件冒泡, *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具體事件類型一一匹配:
SimpleEventPlugin將事件類型劃分紅了三類, 對應不一樣的優先級(優先級由低到高):
可能要先了解一下React調度(Schedule)的優先級,才能理解這三種事件類型的區別。截止到本文寫做時,React有5個優先級級別:
Immediate
- 這個優先級的任務會同步執行, 或者說要立刻執行且不能中斷UserBlocking
(250ms timeout) 這些任務通常是用戶交互的結果, 須要即時獲得反饋 .Normal
(5s timeout) 應對哪些不須要當即感覺到的任務,例如網絡請求Low
(10s timeout) 這些任務能夠放後,可是最終應該獲得執行. 例如分析通知Idle
(no timeout) 一些沒有必要作的任務 (e.g. 好比隱藏的內容).目前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
事件。也就是說*Enter/*Leave
事件在React中是經過*Over/*Out
事件來模擬的。這樣作的好處是能夠在document上面進行委託監聽,還有避免*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初始化和更新時會進行事件綁定。這裏先看一下流程圖,忽略雜亂的跳轉:
媒體類型
,媒體類型的事件是沒法在Document監聽的,因此會直接在元素上進行綁定onMouseEnter
依賴mouseover/mouseout
; 第二個是ReactBrowserEventEmitter維護的'已訂閱事件表'。事件處理器只需在Document訂閱一次,因此相比在每一個元素上訂閱事件會節省不少資源.代碼大概以下:
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); // 更新已訂閱表
}
}
}
複製代碼
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
主要作如下三個事情:
爲了不頻繁建立和釋放事件對象致使性能損耗(對象建立和垃圾回收),React使用一個事件池來負責管理事件對象,使用完的事件對象會放回池中,以備後續的複用。
這也意味着,在事件處理器同步執行完後,SyntheticEvent對象就會立刻被回收,全部屬性都會無效。因此通常不會在異步操做中訪問SyntheticEvent事件對象。你也能夠經過如下方法來保持事件對象的引用:
SyntheticEvent#persist()
方法,告訴React不要回收到對象池SyntheticEvent#nativeEvent
, nativeEvent是能夠持久引用的,不過爲了避免打破抽象,建議不要直接引用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
接口,還封裝了一些內置模塊, 實現跨平臺的、經常使用的高級事件封裝:
舉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), 並將它們轉換/合併成高級的事件:
咱們挑一個簡單的模塊來了解一些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的職責有了一些基本的瞭解,它主要作如下幾件事情:
targetEventTypes
onEvent
context.dispatchEvent
和上面的Keyboard模塊相比,現實中的不少高級事件,如longPress, 它們的實現則要複雜得多. 它們可能要維持必定的狀態、也可能要獨佔響應的全部權(即同一時間只能有一個Responder能夠對事件進行處理, 這個經常使用於移動端觸摸手勢,例如React Native的GestureResponderSystem)。
react-events目前都考慮了這些場景, 看一下API概覽:
詳細能夠看react-events官方倉庫
上文提到了React事件內部採用了插件機制,來實現事件處理和合成,比較典型的就是onChange事件。onChange事件其實就是所謂的‘高級事件’,它是經過表單組件的各類原生事件來模擬的。
也就是說,React經過插件機制本質上是能夠實現高級事件的封裝的。可是若是讀者看過源代碼,就會以爲裏面邏輯比較繞,並且依賴React的不少內部實現。因此這種內部的插件機制並非面向普通開發者的。
react-events
接口就簡單不少了,它屏蔽了不少內部細節,面向普通開發者。咱們能夠利用它來實現高性能的自定義事件分發,更大的意義是經過它能夠實現跨平臺/設備的事件處理方式.
目前react-events仍是實驗階段,特性是默認關閉,API可能會出現變動, 因此不建議在生產環境使用。能夠經過這個Issue來關注它的進展。
最後讚歎一下React團隊的創新能力!
完!