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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...