React事件機制-事件分發

来源:https://www.cnblogs.com/raion/archive/2019/03/26/10598473.html
-Advertisement-
Play Games

事件分發 之前講述了事件如何綁定在 上,那麼具體事件觸發的時候是如何分發到具體的監聽者呢?我們接著上次註冊的事件代理看。當我點擊 按鈕時,觸發註冊的 事件代理。 為`click nativeEvent dispatchEvent(topLevelType, nativeEvent) _interac ...


事件分發

之前講述了事件如何綁定在document上,那麼具體事件觸發的時候是如何分發到具體的監聽者呢?我們接著上次註冊的事件代理看。當我點擊update counter按鈕時,觸發註冊的click事件代理。

function dispatchInteractiveEvent(topLevelType, nativeEvent) {
  interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}
function interactiveUpdates(fn, a, b) {
  return _interactiveUpdatesImpl(fn, a, b);
}
var _interactiveUpdatesImpl = function (fn, a, b) {
  return fn(a, b);
};

topLevelTypeclicknativeEvent為真實dom事件對象。看似很多,其實就做了一件事: 執行dispatchEvent(topLevelType, nativeEvent)。其實不然,_interactiveUpdatesImpl在後面被重新賦值為interactiveUpdates$1,完成了一次自我蛻變。

function setBatchingImplementation(batchedUpdatesImpl, interactiveUpdatesImpl, flushInteractiveUpdatesImpl) {
  _batchedUpdatesImpl = batchedUpdatesImpl;
  _interactiveUpdatesImpl = interactiveUpdatesImpl;
  _flushInteractiveUpdatesImpl = flushInteractiveUpdatesImpl;
}

function interactiveUpdates$1(fn, a, b) {
  if (!isBatchingUpdates && !isRendering && lowestPriorityPendingInteractiveExpirationTime !== NoWork) {
    performWork(lowestPriorityPendingInteractiveExpirationTime, false);
    lowestPriorityPendingInteractiveExpirationTime = NoWork;
  }
  var previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return scheduler.unstable_runWithPriority(scheduler.unstable_UserBlockingPriority, function () {
      return fn(a, b);
    });
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

setBatchingImplementation(batchedUpdates$1, interactiveUpdates$1, flushInteractiveUpdates$1);

如果有任何等待的交互更新,條件滿足的情況下會先同步更新,然後設置isBatchingUpdates,進行scheduler調度。最後同步更新。scheduler的各類優先順序如下:

unstable_ImmediatePriority: 1
unstable_UserBlockingPriority: 2
unstable_NormalPriority: 3
unstable_LowPriority: 4
unstable_IdlePriority: 5

進入scheduler調度,根據優先順序計算時間,開始執行傳入的回調函數。然後調用dispatchEvent,最後更新immediate workflushImmediateWork里的調用關係很複雜,最終會調用requestAnimationFrame進行更新,這裡不進行過多討論。

function unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  var previousPriorityLevel = currentPriorityLevel;
  var previousEventStartTime = currentEventStartTime;
  currentPriorityLevel = priorityLevel;
  currentEventStartTime = exports.unstable_now();

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
    currentEventStartTime = previousEventStartTime;
    flushImmediateWork();
  }
}

下麵看看dispatchEvent的具體執行過程。

function dispatchEvent(topLevelType, nativeEvent) {
  if (!_enabled) {
    return;
  }
  // 獲取事件觸發的原始節點
  var nativeEventTarget = getEventTarget(nativeEvent);
  // 獲取原始節點最近的fiber對象(通過緩存在dom上的internalInstanceKey屬性來尋找),如果沒找到會往父節點繼續尋找。
  var targetInst = getClosestInstanceFromNode(nativeEventTarget);

  if (targetInst !== null && typeof targetInst.tag === 'number' && !isFiberMounted(targetInst)) {
    targetInst = null;
  }
  // 創建對象,包含事件名稱,原始事件,目標fiber對象和ancestor(空數組);如果緩存池有則直接取出並根據參數初始化屬性。
  var bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst);

  try {
    // 批處理事件
    batchedUpdates(handleTopLevel, bookKeeping);
  } finally {
    // 釋放bookKeeping對象記憶體,並放入對象池緩存
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

接著看batchedUpdates,其實就是設置isBatching變數然後調用handleTopLevel(bookkeeping)

function batchedUpdates(fn, bookkeeping) {
  if (isBatching) {
    return fn(bookkeeping);
  }
  isBatching = true;
  try {
    // _batchedUpdatesImpl其實指向batchedUpdates$1函數,具體細節這裡不再贅述
    return _batchedUpdatesImpl(fn, bookkeeping);
  } finally {
    isBatching = false;
    var controlledComponentsHavePendingUpdates = needsStateRestore();
    if (controlledComponentsHavePendingUpdates) {
      _flushInteractiveUpdatesImpl();
      restoreStateIfNeeded();
    }
  }
}

所以將原始節點對應最近的fiber緩存在bookKeeping.ancestors中。

function handleTopLevel(bookKeeping) {
  var targetInst = bookKeeping.targetInst;
  var ancestor = targetInst;
  do {
    if (!ancestor) {
      bookKeeping.ancestors.push(ancestor);
      break;
    }
    var root = findRootContainerNode(ancestor);
    if (!root) {
      break;
    }
    bookKeeping.ancestors.push(ancestor);
    ancestor = getClosestInstanceFromNode(root);
  } while (ancestor);

  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    runExtractedEventsInBatch(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}

runExtractedEventsInBatch中調用了兩個方法: extractEventsrunEventsInBatch。前者構造合成事件,後者批處理合成事件。

function runExtractedEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
  var events = extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
  runEventsInBatch(events);
}

事件合成

 function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
  var events = null;

  for (var i = 0; i < plugins.length; i++) {
    var possiblePlugin = plugins[i];
    if (possiblePlugin) {
      var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);

      if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
      }
    }
  }

  return events;
}

plugins是所有合成事件集合的數組,EventPluginHub初始化的時候完成註入。遍歷所有plugins,調用其extractEvents方法,返回構造的合成事件。accumulateInto函數則把合成事件放入events。本例click事件合適的pluginSimpleEventPlugin,其他plugin得到的extractedEvents都不滿足if (extractedEvents)條件。

EventPluginHubInjection.injectEventPluginsByName({
  SimpleEventPlugin: SimpleEventPlugin,
  EnterLeaveEventPlugin: EnterLeaveEventPlugin,
  ChangeEventPlugin: ChangeEventPlugin,
  SelectEventPlugin: SelectEventPlugin,
  BeforeInputEventPlugin: BeforeInputEventPlugin,
});

接下來看看構造合成事件的具體過程,這裡針對SimpleEventPlugin,其他plugin就不一一分析了,來看下其extractEvents:

extractEvents: function(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if (!dispatchConfig) {
      return null;
    }
    var EventConstructor = void 0;
    switch (topLevelType) {
      ...
      case TOP_CLICK:
      ...
        EventConstructor = SyntheticMouseEvent;
        break;
      ...    
    }
    var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
    accumulateTwoPhaseDispatches(event);
    return event;
  }

topLevelEventsToDispatchConfig是一個map對象,存儲著各類事件對應的配置信息。這裡獲取到click的配置信息,然後根據topLevelType選擇對應的合成構造函數,這裡為SyntheticMouseEvent。接著從SyntheticMouseEvent合成事件對象池中獲取合成事件。調用EventConstructor.getPooled,最終調用的是getPooledEvent

註意: SyntheticEvent.extend方法中明確寫有addEventPoolingTo(Class);所以,SyntheticMouseEvent有eventPool、getPooled和release屬性。後面會詳細介紹SyntheticEvent.extend

function addEventPoolingTo(EventConstructor) {
  EventConstructor.eventPool = [];
  EventConstructor.getPooled = getPooledEvent;
  EventConstructor.release = releasePooledEvent;
}

首次觸發事件,對象池為空,所以這裡需要新創建。如果不為空,則取出一個並初始化。

function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
  var EventConstructor = this;
  if (EventConstructor.eventPool.length) {
    var instance = EventConstructor.eventPool.pop();
    EventConstructor.call(instance, dispatchConfig, targetInst, nativeEvent, nativeInst);
    return instance;
  }
  return new EventConstructor(dispatchConfig, targetInst, nativeEvent, nativeInst);
}

合成事件的屬性是由React主動生成的,一些屬性和原生事件的屬性名完全一致,使其完全符合W3C標準,因此在事件層面上具有跨瀏覽器相容性。如果要訪問原生對象,通過nativeEvent屬性即可獲取。這裡SyntheticMouseEventSyntheticUIEvent擴展而來,而SyntheticUIEventSyntheticEvent擴展而來。

var SyntheticMouseEvent = SyntheticUIEvent.extend({
  ...
});

var SyntheticUIEvent = SyntheticEvent.extend({
  ...
});

SyntheticEvent.extend = function (Interface) {
  var Super = this;
  // 原型繼承
  var E = function () {};
  E.prototype = Super.prototype;
  var prototype = new E();
  // 構造繼承
  function Class() {
    return Super.apply(this, arguments);
  }
  _assign(prototype, Class.prototype);
  Class.prototype = prototype;
  Class.prototype.constructor = Class;

  Class.Interface = _assign({}, Super.Interface, Interface);
  Class.extend = Super.extend;
  addEventPoolingTo(Class);

  return Class;
};

當被new創建時,會調用父類SyntheticEvent進行構造。主要是將原生事件上的屬性掛載到合成事件上,還配置了一些額外屬性。

function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) {
  this.dispatchConfig = dispatchConfig;
  this._targetInst = targetInst;
  this.nativeEvent = nativeEvent;
  ...
}

合成事件構造完成後,調用accumulateTwoPhaseDispatches

function accumulateTwoPhaseDispatches(events) {
  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}

// 迴圈處理所有的合成事件
function forEachAccumulated(arr, cb, scope) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}

// 檢測事件是否具有捕獲階段和冒泡階段
function accumulateTwoPhaseDispatchesSingle(event) {
  if (event && event.dispatchConfig.phasedRegistrationNames) {
    traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
  }
}

function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  // 迴圈遍歷當前元素及父元素,緩存至path
  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }
  var i = void 0;
  // 捕獲階段
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg);
  }
  // 冒泡階段
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

function accumulateDirectionalDispatches(inst, phase, event) {
  // 獲取當前階段對應的事件處理函數
  var listener = listenerAtPhase(inst, event, phase);
  // 將相關listener和目標fiber掛載到event對應的屬性上
  if (listener) {
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

事件執行(批處理合成事件)

首先將events合併到事件隊列,之前沒有處理完畢的隊列也一同合併。如果新的事件隊列為空,則退出。反之開始迴圈處理事件隊列中每一個eventforEachAccumulated前面有提到過,這裡不再贅述。

function runEventsInBatch(events) {
  if (events !== null) {
    eventQueue = accumulateInto(eventQueue, events);
  }
  var processingEventQueue = eventQueue;
  eventQueue = null;

  if (!processingEventQueue) {
    return;
  }

  forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
  rethrowCaughtError();
}

接下來看看事件處理,executeDispatchesAndRelease方法將事件執行和事件清理分開。

var executeDispatchesAndReleaseTopLevel = function (e) {
  return executeDispatchesAndRelease(e);
};

var executeDispatchesAndRelease = function (event) {
  if (event) {
    // 執行事件
    executeDispatchesInOrder(event);

    if (!event.isPersistent()) {
      // 事件清理,將合成事件放入對象池
      event.constructor.release(event);
    }
  }
};

提取事件的處理函數和對應的fiber,調用executeDispatch

function executeDispatchesInOrder(event) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

獲取真實dom掛載到event對象上,然後開始執行事件。

function executeDispatch(event, listener, inst) {
  var type = event.type || 'unknown-event';
  // 獲取真實dom
  event.currentTarget = getNodeFromInstance(inst);
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}

invokeGuardedCallbackAndCatchFirstError下麵調用的方法很多,最終會來到invokeGuardedCallbackImpl,關鍵就在func.apply(context, funcArgs);這裡的func就是listener(本例中是handleClick),而funcArgs就是合成事件對象。至此,事件執行完畢。

var invokeGuardedCallbackImpl = function (name, func, context, a, b, c, d, e, f) {
  var funcArgs = Array.prototype.slice.call(arguments, 3);
  try {
    func.apply(context, funcArgs);
  } catch (error) {
    this.onError(error);
  }
};

事件清理

事件執行完之後,剩下就是一些清理操作。event.constructor.release(event)相當於releasePooledEvent(event)。由於click對應的是SyntheticMouseEvent,所以會放入SyntheticMouseEvent.eventPool中。EVENT_POOL_SIZE固定為10。

function releasePooledEvent(event) {
  var EventConstructor = this;
  event.destructor();
  if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
    EventConstructor.eventPool.push(event);
  }
}

這裡做了兩件事,第一手動釋放event屬性上的記憶體(將屬性置為null),第二將event放入對象池。至此,清理工作完畢。

destructor: function () {
    ...
    this.dispatchConfig = null;
    this._targetInst = null;
    this.nativeEvent = null;
    this.isDefaultPrevented = functionThatReturnsFalse;
    this.isPropagationStopped = functionThatReturnsFalse;
    this._dispatchListeners = null;
    this._dispatchInstances = null;
    ...
}    

event清理完後,還會清理bookKeeping,同樣也會放入對象池進行緩存。同樣CALLBACK_BOOKKEEPING_POOL_SIZE也固定為10。

// callbackBookkeepingPool是react-dom中的全局變數
function releaseTopLevelCallbackBookKeeping(instance) {
  instance.topLevelType = null;
  instance.nativeEvent = null;
  instance.targetInst = null;
  instance.ancestors.length = 0;

  if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) {
    callbackBookkeepingPool.push(instance);
  }
}

總結

最後執行performSyncWork。如果執行的事件內調用了this.setState,會進行reconciliationcommit。由於事件流的執行是批處理過程,同步調用this.setState不會立馬更新,需等待所有事件執行完成,即scheduler調度完後才開始performSyncWork,最終才能拿到新的state。如果是setTimeout或者是在dom上另外addEventListener的回調函數中調用this.setState則會立馬更新。因為執行回調函數的時候不經過React事件流。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、獲取元素方法(JS選擇器) 1.1概述 得到id元素的方法 document.getElementById() 得到一個元素。事實上,還有一個方法可以得到標簽元素,並且得到的是多個元素: document.getElementsByTagName(); 全線瀏覽器相容的,得到元素的方法,就這兩個 ...
  • 1.二維數組中的查找 在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。 時間限制:1秒 空間限制:32768K 分析:由於每一行都按照從左到右遞增的順序排序 ...
  • 前段時間在使用jQuery的animate() 函數時候用到Alternate方式。主要是要讓數字自增到指定大小,且能看見數字增加過程。 一般使用如下方式: 嗯...能運行,不報錯,但是問題來了。多刷新試試,會發現有時動畫過程會在未達到指定數字的時候就停下了。可想而知,不管是數字動畫還是其他動畫都可 ...
  • 2019年3月21日開通博客準備開始前端學習的徵程! ...
  • js導出word文檔所需要的兩個插件: 使用jquery.wordexport.js這個插件導出的word文檔的排版方式; 編輯器打開jquery.wordexport.js,找到 var styles = 在後面添加樣式即可: ...
  • 代碼如下: <div id="menu"> <ul> <li><a href="#">首頁</a></li> <li class="menuDiv"></li> <li><a href="#">博客</a></li> <li class="menuDiv"></li> & ...
  • 初識 javascript 1、JS組成:ECMA BOM DOM (1)是一種基於 對象模型 和 事件 的腳本語言 (2)組成: ECMAScript由ECMA-262定義,提供核心語言功能; 文檔對象模型(DOM),提供訪問和操作網頁內容的方法和介面; 瀏覽器對象模型(BOM),提供與瀏覽器交互 ...
  • //動畫函數---任意一個元素移動到指定的目標位置 //element為元素 target為位置 function carToon(element, target) { //設置一個定時器讓他迴圈去增加 element.timeid = setInterval(function () { //拿到當... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...