一看就暈的React事件機制

前言

上篇文章咱們瞭解了React合成事件跟原生綁定事件是有區別的,本篇文章從源碼來深挖一下React的事件機制。html

TL;DR :node

  • react事件機制分爲兩個部分:一、事件註冊 二、事件分發
  • 事件註冊部分,全部的事件都會註冊到document上,擁有統一的回調函數dispatchEvent來執行事件分發
  • 事件分發部分,首先生成合成事件,注意同一種事件類型只能生成一個合成事件Event,如onclick這個類型的事件,dom上全部帶有經過jsx綁定的onClick的回調函數都會按順序(冒泡或者捕獲)會放到Event._dispatchListeners 這個數組裏,後面依次執行它。

仍是使用上次的栗子:react

class ExampleApplication extends React.Component {
    componentDidMount() {
        document.addEventListener('click', () => {
            alert('document click');
        })
    }

    outClick(e) {
        console.log(e.currentTarget);
        alert('outClick');
    }
    
    onClick(e) {
        console.log(e.currentTarget);
        alert('onClick');
        e.stopPropagation();
    }
    render() {
        return <div onClick={this.outClick}>
            <button onClick={this.onClick}> 測試click事件 </button>
        </div>
    }
}

分析源碼以前,有些工做和知識要提早準備,普及一下:git

  1. 請各位準備好一個編輯器,自行用react-starter-kit建一個react項目,複製上面的代碼,渲染上面的組件,而後打開控制檯
  2. 下圖是整個事件機制的流程圖,後面會分部分解析
    https://www.processon.com/dia...
  3. 普及幾個功能函數,提早了解它的做用
// 做用:若是隻是單個next,則直接返回,若是有數組,返回合成的數組,裏面有個
//current.push.apply(current, next)能夠學習一下,我查了一下[資料][3]https://jsperf.com/array-prototype-push-apply-vs-concat/2,這樣組合數組比concat效率更高

// 栗子:input accumulateInto([],[])

function accumulateInto(current, next) {
  if (current == null) {
    return next;
  }

  // Both are not empty. Warning: Never call x.concat(y) when you are not
  // certain that x is an Array (x could be a string with concat method).
  if (Array.isArray(current)) {
    if (Array.isArray(next)) {
      current.push.apply(current, next);
      return current;
    }
    current.push(next);
    return current;
  }

  if (Array.isArray(next)) {
    // A bit too dangerous to mutate `next`.
    return [current].concat(next);
  }

  return [current, next];
}

// 這個其實就是用來執行函數的,當arr時數組的時候,arr裏的每個項都做爲回調函數cb的參數執行;
// 若是不是數組,直接執行回調函數cb,參數爲arr
// 例如:
// arr爲數組:forEachAccumulated([1,2,3], (item) => {console.log(item), this})
// 此時會打印出 1,2,3
// arr不爲數組,forEachAccumulated(1, (item) => {console.log(item), this})
// 此時會打印出 1

function forEachAccumulated(arr, cb, scope) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}

React事件機制

React事件機制分爲兩塊:github

  • 事件註冊
  • 事件分發

咱們一步步來看:web

事件註冊

整個過程從ReactDomComponent開始,重點在enqueuePutListener,這個函數作了三件事情,詳細請參考下面源碼:segmentfault

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代碼
    ...
    // 一、*重要:在這裏取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    // 二、在document上註冊事件,同一個事件類型只會被註冊一次
      listenTo(registrationName, doc);
    // 三、mountReady以後將回調函數存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

接下來看看第二步:在document上註冊事件 的過程,流程圖以下:
數組

接着咱們抽出每一個文件的重點函數出來分析:瀏覽器

ReactBrowserEventEmitter.js

listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    // 檢測document上是否已經監聽onClick事件,因此前面說同一類型事件只會綁定一次
    var isListening = getListeningForDocument(mountAt);
    // 得到dependency,將onClick 轉成topClick,這只是一種處理方式不用糾結
    var dependencies = 
   EventPluginRegistry.registrationNameDependencies[registrationName];
   // 中間是對各類事件類型給document綁定捕獲事件或者冒泡事件,大部分都是冒泡,
   ...
   // 這裏咱們的topClick,綁定的是冒泡事件
   else if (topEventMapping.hasOwnProperty(dependency)) {  
   // trapBubbledEvent會在下面分析
 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
   }
   // 最後把topClick標記爲已註冊過,防止重複註冊
   isListening[dependency] = true;
}

因爲onclick綁定的是冒泡事件,因此咱們來看看trapBubbledEventapp

ReactEventListener.js

// 輸入: topClick, click, doc
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    //  EventListener 要作的事情就是把事件綁定到document上,注意這裏不管是註冊冒泡仍是捕獲事件,最終的回調函數都是dispatchEvent
    
    return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
  },
  
// EventListener.js
// 輸入doc, click, dispatchEvent
// 這個函數其實就是咱們熟悉的兼容瀏IE瀏覽器事件綁定的方法
listen: function listen(target, eventType, callback) {
    if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    } else if (target.attachEvent) {
      target.attachEvent('on' + eventType, callback);
      return {
        remove: function remove() {
          target.detachEvent('on' + eventType, callback);
        }
      };
    }
  },

注意這裏不管是註冊冒泡仍是捕獲事件,最終的回調函數都是dispatchEvent,因此咱們來看看dispatchEvent怎麼處理事件分發。

dispatchEvent

看到這裏你們會奇怪,全部的事件的回調函數都是dispatchEvent來處理,那事件onClick原來的回調函數存到哪裏去了呢?

再回來看事件註冊的第三步:mountReady以後將回調函數存在ListernBank中

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代碼
    ...
    // 一、*重要:在這裏取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    // 二、在document上註冊事件,同一個事件類型只會被註冊一次
      listenTo(registrationName, doc);
    // 三、mountReady以後將回調函數存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

document上註冊完全部的事件以後,還須要把listener 放到listenerBank中以listenerBank[registrationName][key] 這樣的形式存起來,而後在dispatchEvent裏面使用。

將listener放到listenerBank中儲存的過程以下:

ReactDomComponent.js

// 在putListener裏存入listener
function putListener() {
  var listenerToPut = this;
  // 先put的是外層的listener - outClick,因此這裏的inst是外層div
  // registrationName是onclick,listener是outClick
  EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}

EventPluginHub.js

/**
   * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent.
   *
   * @param {object} inst The instance, which is the source of events.
   * @param {string} registrationName Name of listener (e.g. `onClick`).
   * @param {function} listener The callback to store.
   */
  putListener: function (inst, registrationName, listener) {
    var key = getDictionaryKey(inst); // 先根據inst獲得惟一的key
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    // 能夠看到最終listener 在 listenerBank裏,最終以listenerBank[registrationName][key] 存在

    bankForRegistrationName[key] = listener; 
   
    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      // 這裏的didPutListener只是爲了兼容手機safari對non-interactive元素
      // 雙擊響應不正確,詳情能夠參考這篇[文章][7]
      //https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  },

以上就是事件註冊的過程,接下來在看dispatchEvent如何處理事件分發。

事件分發

在介紹事件分發以前,有必要先介紹一下生成合成事件的過程,連接是https://segmentfault.com/a/11...

瞭解合成事件生成的過程以後,咱們須要get一個點:合成事件收集了一波同類型(例如click)的回調函數存在了合成事件event._dispatchListeners這個數組裏,而後將它們事件對應的虛擬dom節點放到_dispatchInstances 就本例來講,_dispatchListeners= [onClick, outClick],以後在一塊兒執行。

接下來看看事件分發的過程:

EventListener.js

dispatchEvent: function (topLevelType, nativeEvent) {
    if (!ReactEventListener._enabled) {
      return;
    }
    // 這裏獲得TopLevelCallbackBookKeeping的實例對象,本例中第一次觸發dispatchEvent時
    // bookKeeping = {ancestors: [],nativeEvent,‘topClick’}
    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      // 接着執行handleTopLevelImpl(bookKeeping)
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  }

function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  // 獲取當前事件的虛擬dom元素
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);

  var ancestor = targetInst;
  do {
    bookKeeping.ancestors.push(ancestor);
    ancestor = ancestor && findParent(ancestor);
  } while (ancestor);

  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    // 這裏的_handleTopLevel 對應的就是ReactEventEmitterMixin.js裏的handleTopLevel
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}

// 這裏的findParent曾經給我帶來誤導,我覺得去找當前元素全部的父節點,但其實不是的,
// 咱們知道通常狀況下,咱們的組件最後會被包裹在<div id='root'></div>的標籤裏
// 通常是沒有組件再去嵌套它的,因此一般返回null
/**
 * Find the deepest React component completely containing the root of the
 * passed-in instance (for use when entire React trees are nested within each
 * other). If React trees are not nested, returns null.
 */
function findParent(inst) {
  while (inst._hostParent) {
    inst = inst._hostParent;
  }
  var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
  var container = rootNode.parentNode;
  return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}

上面這段代碼的重點就是_handleTopLevel,它能夠獲取合成事件,而且去執行它。

下面看看具體是如何執行:

ReactEventEmitterMixin.js

function runEventQueueInBatch(events) {
  // 一、先將事件放進隊列裏
  EventPluginHub.enqueueEvents(events);
  // 二、執行它
  EventPluginHub.processEventQueue(false);
}

var ReactEventEmitterMixin = {
  /**
   * Streams a fired top-level event to `EventPluginHub` where plugins have the
   * opportunity to create `ReactEvent`s to be dispatched.
   */
  handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
     // 用EventPluginHub生成合成事件
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    //  執行合成事件
    runEventQueueInBatch(events);
  }
};

執行的過程分紅兩步:

  1. 將事件放進隊列
  2. 執行

執行的細節以下:

EventPluginHub.js

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

  var executeDispatchesAndRelease = function (event, simulated) {
      if (event) {
         // 在這裏dispatch事件
        EventPluginUtils.executeDispatchesInOrder(event, simulated);
         // 釋放事件
        if (!event.isPersistent()) {
          event.constructor.release(event);
        }
      }
  };

  enqueueEvents: function (events) {
    if (events) {
      eventQueue = accumulateInto(eventQueue, events);
    }
  },

  /**
   * Dispatches all synthetic events on the event queue.
   *
   * @internal
   */
  processEventQueue: function (simulated) {
    // Set `eventQueue` to null before processing it so that we can tell if more
    // events get enqueued while processing.
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
    // This would be a good time to rethrow if any of the event fexers threw.
    ReactErrorUtils.rethrowCaughtError();
  },

上段代碼裏,咱們最終會走到

forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);

forEachAccumulated這個函數咱們以前講過,就是對數組processingEventQueue的每個合成事件都使用executeDispatchesAndReleaseTopLevel來dispatch 事件。

因此各位同窗們,注意到這裏咱們已經走到最核心的部分,dispatch 合成事件了,下面看看dispatch的詳細過程:

EventPluginUtils.js

/**
 * Standard/simple iteration through an event's collected dispatches.
 */
function executeDispatchesInOrder(event, simulated) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;

  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
     // 由這裏能夠看出,合成事件的stopPropagation只能阻止react合成事件的冒泡,
     // 由於event._dispatchListeners 只記錄了由jsx綁定的綁定的事件,對於原生綁定的是沒有記錄的
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

由上面的函數可知,dispatch 合成事件分爲兩個步驟:

  1. 經過_dispatchListeners裏獲得全部綁定的回調函數,在經過_dispatchInstances的綁定回調函數的虛擬dom元素
  2. 循環執行_dispatchListeners裏全部的回調函數,這裏有一個特殊狀況,也是react阻止冒泡的原理

當回調函數裏使用了stopPropagation會使得數組後面的回調函數不能執行,這樣就作到了阻止事件冒泡

目前仍是還有看到執行事件的代碼,在接着看:

EventPluginHub.js

function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || 'unknown-event';
  // 注意這裏將事件對應的dom元素綁定到了currentTarget上
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    // 通常都是非模擬的狀況,執行invokeGuardedCallback
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

上面這個函數最重要的功能就是將事件對應的dom元素綁定到了currentTarget上,
這樣咱們經過e.currentTarget就能夠找到綁定事件的原生dom元素。

下面就是整個執行過程的尾聲了:

ReactErrorUtils.js

var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
  var boundFunc = function () {
    func(a);
  };
  var evtType = 'react-' + name;
  fakeNode.addEventListener(evtType, boundFunc, false);
  var evt = document.createEvent('Event');
  evt.initEvent(evtType, false, false);
  fakeNode.dispatchEvent(evt);
  fakeNode.removeEventListener(evtType, boundFunc, false);
};

invokeGuardedCallback可知,最後react調用了faked元素的dispatchEvent方法來觸發事件,而且觸發完畢以後當即移除監聽事件。

總的來講,整個click事件被分發的過程就是:
一、用EventPluginHub生成合成事件,這裏注意同一事件類型只會生成一個合成事件,裏面的_dispatchListeners裏儲存了同一事件類型的全部回調函數

二、按順序去執行它

就辣麼簡單!

本文比較長,有不理解的歡迎提問~ 或者有理解錯誤的也請你們指正。
最後附上整個流程圖文件:

clipboard.png

s://segmentfault.com/a/1190000013343819

相關文章
相關標籤/搜索