源碼看React 事件機制

對React熟悉的同窗都知道,React中的事件機制並非原生的那一套,事件沒有綁定在原生DOM上,發出的事件也是對原生事件的包裝。
那麼這一切是怎麼實現的呢?node

事件註冊

首先仍是看咱們熟悉的代碼react

<button onClick={this.autoFocus}>點擊聚焦</button>

這是咱們在React中綁定事件的常規寫法。經由JSX解析,button會被當作組件掛載。而onClick這時候也只是一個普通的props。
ReactDOMComponent在進行組件加載(mountComponent)、更新(updateComponent)的時候,須要對props進行處理(_updateDOMProperties):jquery

ReactDOMComponent.Mixin = {
  _updateDOMProperties: function (lastProps, nextProps, transaction) {
    ...
    for (propKey in nextProps) {
      // 判斷是否爲事件屬性
      if (registrationNameModules.hasOwnProperty(propKey)) {
        enqueuePutListener(this, propKey, nextProp, transaction);
      }
    }
  }
}
//這裏進行事件綁定
function enqueuePutListener(inst, registrationName, listener, transaction) {
  ...
  //注意這裏!!!!!!!!!
  //這裏獲取了當前組件(其實這時候就是button)所在的document
  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  listenTo(registrationName, doc);
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
  function putListener() {
    var listenerToPut = this;
    EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
  }
}

綁定的重點是這裏的listenTo方法。看源碼(ReactBrowerEventEmitter)數組

//registrationName:須要綁定的事件
//當前component所屬的document,即事件須要綁定的位置
listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    //獲取當前document上已經綁定的事件
    var isListening = getListeningForDocument(mountAt);
    ...
      if (...) {
      //冒泡處理  
      ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...);
      } else if (...) {
        //捕捉處理
        ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...);
      }
      ...
  },

最後處理(EventListener的listen和capture中)瀏覽器

//eventType:事件類型,target: document對象,
//callback:是固定的,始終是ReactEventListener的dispatch方法
if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    }

從事件註冊的機制中不難看出:函數

  • 全部事件綁定在document上
  • 因此事件觸發的都是ReactEventListener的dispatch方法

回調儲存

看到這邊你可能疑惑,全部回調都執行的ReactEventListener的dispatch方法,那我寫的回調幹嗎去了。別急,接着看:this

function enqueuePutListener(inst, registrationName, listener, transaction) {
  ...
  //注意這裏!!!!!!!!!
  //這裏獲取了當前組件(其實這時候就是button)所在的document
  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  //事件綁定
  listenTo(registrationName, doc);
 //這段代碼表示將putListener放入回調序列,當組件掛載完成是會依次執行序列中的回調。putListener也是在那時候執行的。
 //不明白的能夠看看本專欄中前兩篇關於transaction和掛載機制的講解
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
  //保存回調
  function putListener() {
    var listenerToPut = this;
    EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
  }
}

仍是這段代碼,事件綁定咱們介紹過,主要是listenTo方法。
當綁定完成之後會執行putListener。該方法會在ReactReconcileTransaction事務的close階段執行,具體由EventPluginHub來進行管理spa

//
var listenerBank = {};
var getDictionaryKey = function (inst) {
//inst爲組建的實例化對象
//_rootNodeID爲組件的惟一標識
  return '.' + inst._rootNodeID;
}
var EventPluginHub = {
//inst爲組建的實例化對象
//registrationName爲事件名稱
//listner爲咱們寫的回調函數,也就是列子中的this.autoFocus
  putListener: function (inst, registrationName, listener) {
    ...
    var key = getDictionaryKey(inst);
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    bankForRegistrationName[key] = listener;
    ...
  }
}

EventPluginHub在每一個項目中只實例化一次。也就是說,項目組全部事件的回調都會儲存在惟一的listenerBank中。設計

是否是有點暈,放上流程圖,仔細回憶一下
圖片描述code

事件觸發

註冊事件時咱們說過,全部的事件都是綁定在Document上。回調統一是ReactEventListener的dispatch方法。
因爲冒泡機制,不管咱們點擊哪一個DOM,最後都是由document響應(由於其餘DOM根本沒有事件監聽)。也便是說都會觸發dispatch

dispatchEvent: function(topLevelType, nativeEvent) {
    //實際觸發事件的DOM對象
    var nativeEventTarget = getEventTarget(nativeEvent);
    //nativeEventTarget對應的virtual DOM
    var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
      nativeEventTarget,
    );
    ...
    //建立bookKeeping實例,爲handleTopLevelImpl回調函數傳遞事件名和原生事件對象
    //其實就是把三個參數封裝成一個對象
    var bookKeeping = getTopLevelCallbackBookKeeping(
      topLevelType,
      nativeEvent,
      targetInst,
    );

    try {
    //這裏開啓一個transactIon,perform中執行了
    //handleTopLevelImpl(bookKeeping)
      ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      releaseTopLevelCallbackBookKeeping(bookKeeping);
    }
  },

這裏把節奏放慢點,咱們一步步跟。

function handleTopLevelImpl(bookKeeping) {
//觸發事件的真實DOM
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  //nativeEventTarget對應的ReactElement
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
  //bookKeeping.ancestors保存的是組件。
  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];
    //具體處理邏輯
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}
//這就是核心的處理了
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
//首先封裝event事件
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    //發送包裝好的event
    runEventQueueInBatch(events);
  }

事件封裝

首先是EventPluginHubextractEvents

extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    var events;
    var plugins = EventPluginRegistry.plugins;
    for (var i = 0; i < plugins.length; i++) {
      // Not every plugin in the ordering may be loaded at runtime.
      var possiblePlugin = plugins[i];
      if (possiblePlugin) {
      //主要看這邊
        var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
        ......
      }
    }
    return events;
  },

接着看SimpleEventPlugin的方法

extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    ......
    //這裏是對事件的封裝,可是不是咱們關注的重點
    var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
    //重點看這邊
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
}

接下來是方法中的各類引用,跳啊跳,轉啊轉,咱們來到了ReactDOMTraversal中的traverseTwoPhase方法

//inst是觸發事件的target的ReactElement
//fn:EventPropagator的accumulateDirectionalDispatches
//arg: 就是以前部分封裝好的event(之因此說是部分,是由於如今也是在處理Event,這邊處理完纔是封裝完成)
function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
   //注意path,這裏以ReactElement的形式冒泡着,
   //把觸發事件的父節點依次保存下來
    path.push(inst);
    //獲取父節點
    inst = inst._hostParent;
  }
  var i;
  //捕捉,依次處理
  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);
  if (listener) {
  //若是有回調,就把包含該類型事件監聽的DOM與對應的回調保存進Event。
  //accumulateInto能夠理解成_.assign
  //記住這兩個屬性,很重要。
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

listenerAtPhase裏面執行的是EventPluginHub的getListener函數

getListener: function (inst, registrationName) {
    //還記得以前保存回調的listenerBank吧?
    var bankForRegistrationName = listenerBank[registrationName];
    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
      return null;
    }
    //獲取inst的_rootNodeId
    var key = getDictionaryKey(inst);
    //獲取對應的回調
    return bankForRegistrationName && bankForRegistrationName[key];
  },

能夠發現,React在分裝原生nativeEvent時

  • 將有eventType屬性的ReactElement放入 event._dispatchInstances
  • 將對應的回調依次放入event._dispatchListeners

事件分發

runEventQueueInBatch主要進行了兩步操做

function runEventQueueInBatch(events) {
//將event事件加入processEventQueue序列
  EventPluginHub.enqueueEvents(events);
  //前一步保存好的processEventQueue依次執行
//executeDispatchesAndRelease
  EventPluginHub.processEventQueue(false);
}

  processEventQueue: function (simulated) {
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
    //重點看這裏
    //forEachAccumulated能夠當作forEach的封裝
    //那麼這裏就是processingEventQueue保存的event依次執行executeDispatchesAndReleaseTopLevel(event)
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
  },

executeDispatchesAndReleaseTopLevel(event)又是各類函數包裝,最後幹活的是

function executeDispatchesInOrder(event, simulated) {
  //對應的回調函數數組
  var dispatchListeners = event._dispatchListeners;
  //有eventType屬性的ReactElement數組
  var dispatchInstances = event._dispatchInstances;
  
  ......
  
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

OK,這裏總算出現了老熟人,在封裝nativeEvent時咱們保存在event裏的兩個屬性,dispatchListenersdispatchInstances,在這裏起做用。
代碼很簡單,若是有處理這個事件的回調函數,就一次進行處理。細節咱們稍後討論,先看看這裏是怎麼處理的吧

function executeDispatch(event, simulated, listener, inst) {
//type是事件類型
  var type = event.type || 'unknown-event';
  //這是觸發事件的真實DOM,也就是列子中的button
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
  //看這裏看這裏
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

終於來到最後了,代碼位於ReactErrorUtil中
(爲了幫助開發,React經過模擬真正的瀏覽器事件來得到更好的devtools集成。這段代碼在開發模式下運行)

//創造一個臨時DOM
    var fakeNode = document.createElement('react');
    ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
    //綁定回調函數的上下文
      var boundFunc = func.bind(null, 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);
    };

整體流程

圖片描述

不難發現,咱們經歷了從真實DOM到Virtual DOM的來回轉化。

常見問題的答案。

  1. e.stopPropagation不能阻止原生事件冒泡
    event是封裝好的事件。他是在document的回調裏進行封裝,並執行回調的。而原生的監聽,在document接收到冒泡時早就執行完了。
  2. e.nativeEvent.stopPropagation,回調沒法執行。
    很簡單,由於冒泡是從裏到外,執行了原生的阻止冒泡,document當如捕捉不到,document都沒捕捉到,React還玩個球啊,要知道,一切操做都放在docuemnt的回調裏了。
  3. 怎麼避免二者影響

    這個答案你們說了不少次,避免原生事件與React事件混用,或者經過target進行判斷。

爲何這麼設計

在網上看過一個列子說得很好,一個Ul下面有1000個li標籤。想在想爲每一個li都綁定一個事件,怎麼操做?總不可能一個個綁定吧?其實這個和jquery綁定事件差很少。經過最外層綁定事件,當操做是點擊任何一個li天然會冒泡到最外面的Ul,又能夠經過最外面的target獲取到具體操做的DOM。一次綁定,收益一羣啊。

相關文章
相關標籤/搜索