<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
今天嘗試學習 React 事件的原始碼實現。
React 版本為 18.2.0
React 中的事件,是對原生事件的封裝,叫做合成事件。抽象出一層合成事件,是為了做相容,抹平不同瀏覽器之間的差異。
下面會從兩個方面進行原始碼的解讀:
首先是 React 專案過程啟動時,呼叫 listenToAllSupportedEvents 方法,做合成事件的繫結。
// 對應方法 `ReactDOM.createRoot() function createRoot(container, CreateRootOptions) { const rootContainerElement = container.nodeType === COMMENT_NODE ? (container.parentNode: any) : container; // 繫結支援的事件 listenToAllSupportedEvents(rootContainerElement); }
listenToAllSupportedEvents 方法核心實現為:
function listenToAllSupportedEvents(rootContainerElement) { allNativeEvents.forEach(domEventName => { // 是否為會冒泡的事件 if (!nonDelegatedEvents.has(domEventName)) { // 第二個引數是 isCapturePhaseListener // false 表示繫結在 「冒泡階段」 listenToNativeEvent(domEventName, false, rootContainerElement); } listenToNativeEvent(domEventName, true, rootContainerElement); } }
allNativeEvents 是一個集合(Set),儲存所有的原生 DOM 事件名。會 呼叫不同事件外掛的初始化。
事件外掛有:
這些事件外掛物件暴露有 registerEvents 和 extractEvents 方法。registerEvents 用於初始化原生事件和對應 React 事件,其中一個操作就是往 allNativeEvents 加原生事件名。
nonDelegatedEvents 也是一個集合(Set),儲存不支援冒泡的事件,即不能進行事件委託,比如 cancel、scroll、媒體事件等。
對於支援冒泡的事件,捕獲階段和繫結階段都繫結;對於不支援冒泡的事件,只繫結捕獲階段,且會 在目標元素上繫結事件。
listenToNativeEvent 裡面呼叫了一層又一層的函數,人已經麻了。為了方便理解,這裡將這些巢狀的函數拍平,丟掉一些次要的分支,得到下面核心程式碼:
function listenToNativeEvent() { const listener = dispatchEvent.bind( null, domEventName, eventSystemFlags, targetContainer, ); if (isCapturePhaseListener) { // 捕獲階段的事件委託 targetContainer.addEventListener(domEventName, listener, true); } else { // 冒泡階段的事件委託 targetContainer.addEventListener(domEventName, listener, false); } }
現在我們給一個按鈕繫結 React 的 mousedown 事件,然後觸發。
呼叫棧為:
鏈路很長,只說核心的。
首先 執行 dispatchEventForPluginEventSystem,這裡用 batchedUpdates 批次更新策略執行了 dispatchEventsForPlugins。
function dispatchEventForPluginEventSystem( domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer, ) { batchedUpdates(() => dispatchEventsForPlugins( domEventName, eventSystemFlags, nativeEvent, ancestorInst, targetContainer, ), ); }
dispatchEventsForPlugins 方法下,做的事情:
function dispatchEventsForPlugins( domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer, ) { const nativeEventTarget = getEventTarget(nativeEvent); const dispatchQueue = []; // 1. 找到所有需要執行的事件 extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); // 2. 按一定順序執行 processDispatchQueue(dispatchQueue, eventSystemFlags); }
首先是 呼叫 extractEvents,該方法會呼叫不同的事件外掛物件的 extractEvents 方法。
function extractEvents(...) { SimpleEventPlugin.extractEvents(...); if (shouldProcessPolyfillPlugins) { EnterLeaveEventPlugin.extractEvents(...); ChangeEventPlugin.extractEvents(...); SelectEventPlugin.extractEvents(...); BeforeInputEventPlugin.extractEvents(...); } }
這裡以 SimpleEventPlugin 事件外掛物件的 extractEvents 方法為例,做了下面事情:
// 這個 extractEvents 是 SimpleEventPlugin 下的方法 function extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ) { // 先預設一個合成物件建構函式 let SyntheticEventCtor = SyntheticEvent; switch (domEventName) { // ... case 'mousedown': // 建構函式替換為 合成滑鼠事件建構函式 SyntheticEventCtor = SyntheticMouseEvent; break; } // 是否為捕獲階段 const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0; // 遞迴找到所有的 listeners const listeners = accumulateSinglePhaseListeners( targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly, nativeEvent, ); // 範例化合成事件物件,後面要傳到 listener 裡 const event = new SyntheticEventCtor( reactName, reactEventType, null, nativeEvent, nativeEventTarget, ); // 給佇列加上 合成物件範例,和 listeners dispatchQueue.push({event, listeners}); }
dispatchQueue 佇列的元素是一個物件,有 event 和 listeners 兩個屬性。event 是合成物件範例。listeners 是一個物件陣列,由多個 listener 組成。
dispatchQueue 長下面這個樣子:
然後就是將佇列裡的函數拿去執行。
processDispatchQueue 方法會將佇列裡的不同事件的 listeners 陣列拿去執行。
export function processDispatchQueue(dispatchQueue, eventSystemFlags) { const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0; for (let i = 0; i < dispatchQueue.length; i++) { // 取出不同的事件 const {event, listeners} = dispatchQueue[i]; // 每個事件的 listeners 按一定順序執行 processDispatchQueueItemsInOrder(event, listeners, inCapturePhase); } }
processDispatchQueueItemsInOrder 是按一定順序執行的,因為事件捕獲和冒泡階段的順序是相反的,所以程式碼邏輯中會有兩種執行方向。
一條鏈路中的 listener 拿到的都是同一個 event。執行 event.stopPropagation()
後,event.isPropagationStopped()
就一定會返回 true,然後鏈路就會被中斷。
function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) { let previousInstance; if (inCapturePhase) { // 捕獲階段為逆序 for (let i = dispatchListeners.length - 1; i >= 0; i--) { const { instance, currentTarget, listener } = dispatchListeners[i]; if (instance !== previousInstance && event.isPropagationStopped()) { return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; } } else { // 冒泡階段為順序 for (let i = 0; i < dispatchListeners.length; i++) { const { instance, currentTarget, listener } = dispatchListeners[i]; // 是否中斷冒泡 if (instance !== previousInstance && event.isPropagationStopped()) { return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; } } }
把原始碼細節丟掉,總結一下原理。
在 React 專案啟動時,React 會在 ReactDOM 掛載的根節點上繫結事件,做事件委託,自己模擬瀏覽器的事件流,實現一套 React 事件流。
根節點繫結的通常是兩個事件,一個用於模擬捕獲階段,一個模擬冒泡階段。(但有些事件比較特別,是不能捕獲冒和泡的,比如 scroll 事件,這種事件只會繫結一個事件模擬捕獲階段,且不支援事件委託)
使用者觸發了 React 事件,這裡假設為 mousedown 的冒泡階段。前面繫結的函數通過事件委託拿到了原生事件名,以及目標元素。基於它們,先建立一個合成事件物件,再從 fiber 樹中不停往根節點找,將這些 fiberNode 的 props 的 onMousedown 放到一個佇列中。
收集完畢後,我們根據當前是事件捕獲還是冒泡階段,選擇方向去遍歷同步執行。捕獲階段是從根節點到目標節點,冒泡則相反。另外,可以通過 event.stopPropagation()
阻止事件冒泡。
到此這篇關於React實現合成事件的原始碼分析的文章就介紹到這了,更多相關React合成事件內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45