react源碼閱讀筆記(8)react事件的設(shè)計(jì)

本文使用的是react 15.6.1的代碼

我們先看一段代碼

import React, {Component} from "react";
import "./App.css";

class App extends Component {
    componentDidMount() {
        this.refs.div.addEventListener('click', function () {
            alert('dom event');
        })
    }

    componentClick(event) {
        alert('react event');
        event.stopPropagation();
    }

    render() {
        return (
            <div ref="div">
                <button ref="button" onClick={this.componentClick.bind(this)}>
                    button
                </button>
            </div>
        );
    }
}

export default App;

當(dāng)我們點(diǎn)擊按鈕的時(shí)候,會(huì)彈出哪一些框?
答案是分別彈出 dom event以及react event,如果只了解dom事件的童鞋也許會(huì)問,在componentClick中明明寫了stopPropagation,事件應(yīng)該不會(huì)冒泡到父元素了?。繉?duì)此有疑惑的童鞋可以看看下面的分析,幫助你更進(jìn)一步了解react的事件系統(tǒng)。

事件的注冊(cè)

熟悉react的同學(xué)應(yīng)該知道,在react中,幾乎所有的事件都被代理到了document之上,以達(dá)到優(yōu)化的目的,試想,如果react組件又1000個(gè)列表,每個(gè)列表都給他綁定一個(gè)click事件是多么費(fèi)性能的事情,即便不用react,我們也會(huì)使用代理來處理這樣綁定事件過多對(duì)性能的開銷。既然事件大部分都綁定到了document上,所以stopPropagation沒有起到相應(yīng)的作用,我們來看看是如何綁定的;
在ReactDomComponent.js中有這樣一段代碼,react組件渲染props的時(shí)候,發(fā)現(xiàn)如果是事件屬性,則會(huì)調(diào)用這樣一個(gè)函數(shù)enqueuePutListener

function enqueuePutListener(inst, registrationName, listener, transaction) {
  if (transaction instanceof ReactServerRenderingTransaction) {
    return;
  }
  //實(shí)體對(duì)象中dom節(jié)點(diǎn)信息
  var containerInfo = inst._hostContainerInfo;
  var isDocumentFragment =
    containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
  /**
   * ownerDocument:該節(jié)點(diǎn)的頂層document
   * [ownerDocument](https://developer.mozilla.org/zh-CN/docs/Web/API/Node/ownerDocument)
   */
  var doc = isDocumentFragment
    ? containerInfo._node
    : containerInfo._ownerDocument;
  //注冊(cè)事件
  listenTo(registrationName, doc);
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener,
  });
}

在這段代碼中,簡(jiǎn)單做了這樣一件事情,首先獲取到真實(shí)節(jié)點(diǎn),判斷節(jié)點(diǎn)是否是document,如果不是,則將其節(jié)點(diǎn)的頂層document以及事件名傳入listenTo方法,對(duì)應(yīng)的listenToReactBrowserEventEmitter.js中,如下

listenTo: function(registrationName, contentDocumentHandle) {
    // 事件掛載節(jié)點(diǎn),大部分是document
    var mountAt = contentDocumentHandle;
    var isListening = getListeningForDocument(mountAt);
    var dependencies =
      EventPluginRegistry.registrationNameDependencies[registrationName];

    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (
        !(isListening.hasOwnProperty(dependency) && isListening[dependency])
      ) {
        if (dependency === 'topWheel') {
          //.....
        } else if (dependency === 'topScroll') {
          //.....
        } else if (dependency === 'topFocus' || dependency === 'topBlur') {
          //.....
        } else if (topEventMapping.hasOwnProperty(dependency)) {

          //注冊(cè)冒泡事件
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
            dependency,
            topEventMapping[dependency],
            mountAt,
          );
        }

        isListening[dependency] = true;
      }
    }
  },

在這個(gè)方法中,著重對(duì)不同瀏覽器捕獲與冒泡不同進(jìn)行兼容處理,同時(shí),對(duì)wheel scroll等事件進(jìn)行了特殊處理,調(diào)用trapCapturedEventtrapBubbledEvent來注冊(cè)捕獲和冒泡事件,我們來看看trapBubbledEvent的實(shí)現(xiàn)

return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
      topLevelType,
      handlerBaseName,
      handle,
    );

代碼很簡(jiǎn)單,調(diào)用了ReactEventListenertrapBubbledEvent方法,可是ReactEventListener對(duì)象是從哪里來的呢?在【react源碼閱讀筆記(3)batchedUpdates與Transaction】介紹過React在啟動(dòng)后會(huì)調(diào)用inject方法注入一些對(duì)象,在ReactDefaultInjection.js中執(zhí)行了

ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);

因此,在默認(rèn)情況下 ReactBrowserEventEmitter.ReactEventListener即為注入的ReactEventListener對(duì)象

//ReactEventListener.trapBubbledEvent
trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    return EventListener.listen(
      element,
      handlerBaseName,
      //事件回調(diào),注意
      ReactEventListener.dispatchEvent.bind(null, topLevelType),
    );
  }

這個(gè)方法只是簡(jiǎn)單判斷了一下元素是否存在,隨后調(diào)用EventListener.listen注冊(cè)事件,EventListener.js位于node_modules/fbjs/lib

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);
        }
      };
    }
  }

代碼更加簡(jiǎn)單了,就是對(duì)不同瀏覽器的addEventListener做兼容處理,到這里我們終于找到了事件注冊(cè)的源頭

但是我們?cè)?strong>ReactEventListener.trapBubbledEvent發(fā)現(xiàn)了,綁定到dom上的事件回調(diào),并不是我們?cè)趓eact寫的對(duì)應(yīng)的回調(diào)函數(shù),而是ReactEventListener.dispatchEvent.bind(null, topLevelType),這個(gè)又是什么呢?

// topLevelType:帶top的事件名,如topClick。不用糾結(jié)為什么帶一個(gè)top字段,知道它是事件名就OK了
// nativeEvent: 用戶觸發(fā)click等事件時(shí),瀏覽器傳遞的原生事件
dispatchEvent: function(topLevelType, nativeEvent) {
    if (!ReactEventListener._enabled) {
      return;
    }

    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(
      topLevelType,
      nativeEvent,
    );
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  }

簡(jiǎn)單總結(jié)就是

  1. 對(duì)dom組件進(jìn)行遍歷,如果是事件的話調(diào)用 enqueuePutListener
  2. 對(duì)該dom找到其document,綁定對(duì)應(yīng)的事件名
  3. document不管注冊(cè)的是什么事件,具有統(tǒng)一的回調(diào)函數(shù)handleTopLevelImpl

事件存儲(chǔ)

那么,真正的事件回調(diào)在哪里呢?
回到 enqueuePutListener方法,在listenTo后面執(zhí)行了方法enqueue,把真正的事件綁定回調(diào)listener最為參數(shù)傳入putListener函數(shù)中,也就是說,在ReactReconcileTransaction事務(wù)的close階段執(zhí)行notifyAll的時(shí)候就會(huì)調(diào)用putListener方法,

transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener,
  });
  
function putListener() {
  var listenerToPut = this;
  EventPluginHub.putListener(
    listenerToPut.inst,
    listenerToPut.registrationName,
    listenerToPut.listener,
  );
}

在這里,調(diào)用了 EventPluginHub.putListener方法,將對(duì)應(yīng)的組件對(duì)象,事件名以及回調(diào)作為參數(shù)傳遞

putListener: function(inst, registrationName, listener) {

    // return '.' + inst._rootNodeID;
    var key = getDictionaryKey(inst);
    // 將所綁定的事件存入 listenerBank中,
    var bankForRegistrationName =
      listenerBank[registrationName] || (listenerBank[registrationName] = {});

    // 將對(duì)應(yīng)的事件回調(diào)放入 bankForRegistrationName中,通過id區(qū)分不同組件當(dāng)時(shí)相同的事件,如組件A和B都有click事件,通過id進(jìn)行區(qū)分
    bankForRegistrationName[key] = listener;

    var PluginModule =
      EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  }

在這里,react將不同時(shí)間存入listenerBank數(shù)組中,然后通過NodeId區(qū)分不同節(jié)點(diǎn)具體所綁定的事件

事件執(zhí)行

上面說到,react中會(huì)將真正的事件回調(diào)函數(shù)通過不同id,塞入listenerBank中,大部分事件都是綁定到document上,并使用dispatchEvent進(jìn)行分發(fā)事件,最后調(diào)用了handleTopLevelImpl,那么來看看handleTopLevelImpl事件做了些什么

function handleTopLevelImpl(bookKeeping) {
  // 獲取這個(gè)event的真是dom元素,如果是text節(jié)點(diǎn),則返回父容器
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  // 獲取事件節(jié)點(diǎn)所對(duì)應(yīng)的react實(shí)例
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
    nativeEventTarget,
  );

  var ancestor = targetInst;
  // 將該組件所有父節(jié)點(diǎn)放入ancestors,因?yàn)槭录赡軙?huì)改變父節(jié)點(diǎn)結(jié)構(gòu),因此在執(zhí)行事件回調(diào)之前緩存當(dāng)前parent
  do {
    bookKeeping.ancestors.push(ancestor);
    ancestor = ancestor && findParent(ancestor);
  } while (ancestor);


  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    // 從當(dāng)前組件開始執(zhí)行注冊(cè)的事件,模擬冒泡操作
    ReactEventListener._handleTopLevel(
      bookKeeping.topLevelType,
      targetInst,
      bookKeeping.nativeEvent,
      getEventTarget(bookKeeping.nativeEvent),
    );
  }
}

從源碼中看出handleTopLevelImpl主要是模擬了事件的冒泡操作,拿到事件的注冊(cè)對(duì)象,然后依次找到其父級(jí)組件,調(diào)用ReactEventListener._handleTopLeve

ReactEventListener._handleTopLeve本質(zhì)上是調(diào)用ReactBrowserEventEmitter.handleTopLevel的方法,該代碼位于ReactEventEmitterMixin中

handleTopLevel: function(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  ) {
    //合成對(duì)應(yīng)的事件
    var events = EventPluginHub.extractEvents(
      topLevelType,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    //將事件放入隊(duì)列中
    runEventQueueInBatch(events);
  },

在這里,我們發(fā)現(xiàn),這里會(huì)將組建實(shí)例,具體事件對(duì)象等參數(shù)調(diào)用extractEvents合成react的事件對(duì)象,來看看react將事件是如何合成的

事件合成

EventPluginHub.extractEvents方法

extractEvents: function(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  ) {
    var events;
    // 獲取事件插件,
    var plugins = EventPluginRegistry.plugins;
    for (var i = 0; i < plugins.length; i++) {
      // 通過各個(gè)插件返回事件數(shù)組,最后將事件數(shù)組合并返回
      var possiblePlugin = plugins[i];
      if (possiblePlugin) {
        var extractedEvents = possiblePlugin.extractEvents(
          topLevelType,
          targetInst,
          nativeEvent,
          nativeEventTarget,
        );
        if (extractedEvents) {
          //該方法比較簡(jiǎn)單,就是,簡(jiǎn)單來說就是數(shù)組合并,
          // 如果param1和2都是數(shù)組,那么調(diào)用concat合并
          // 如果其中有一個(gè)是對(duì)象,將其按順序插入新的數(shù)組之中,
          // 最后返回的一定是數(shù)組
          events = accumulateInto(events, extractedEvents);
        }
      }
    }
    return events;
  }

這段代碼依然相對(duì)簡(jiǎn)單,合成事件的時(shí)候,會(huì)依次通過EventPluginRegistry.plugins插件列表來生成對(duì)應(yīng)的事件數(shù)組,最后將這個(gè)生成的事件合并為一個(gè)數(shù)組返回,
我們來看看具體對(duì)應(yīng)的插件是什么

在我們的老朋友ReactDefaultInjection中,會(huì)自動(dòng)注冊(cè)5個(gè)插件

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

我們來分析其中一個(gè)SimpleEventPlugin插件的extractEvents方法

extractEvents: function(
    topLevelType: TopLevelTypes,
    targetInst: ReactInstance,
    nativeEvent: MouseEvent,
    nativeEventTarget: EventTarget,
  ): null | ReactSyntheticEvent {
    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if (!dispatchConfig) {
      return null;
    }
    var EventConstructor;
    switch (topLevelType) {
    //.....
      case 'topCopy':
      case 'topCut':
      case 'topPaste':
        EventConstructor = SyntheticClipboardEvent;
        break;
    }
    // 從緩存池中獲取對(duì)應(yīng)的event
    var event = EventConstructor.getPooled(
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    //最后實(shí)際調(diào)用了ReactDOMTraversal中的traverseTwoPhase方法
    mao.accumulateTwoPhaseDispatches(event);
    return event;
  },

function traverseTwoPhase(inst, fn, arg) {
//這里的fn是EventPropagators中的accumulateDirectionalDispatches方法
  var path = [];
  // 依次尋找父節(jié)點(diǎn)
  while (inst) {
    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);
  }
}

在這里對(duì)模擬進(jìn)行了冒泡與捕獲,同時(shí)調(diào)用了accumulateDirectionalDispatches方法,來看看fn中處理了什么

function accumulateDispatches(inst, ignoredDirection, event) {
  if (event && event.dispatchConfig.registrationName) {
    var registrationName = event.dispatchConfig.registrationName;
    var listener = getListener(inst, registrationName);
    if (listener) {
      // 將具體的event以及實(shí)例放入event下面的數(shù)組之中
      event._dispatchListeners = accumulateInto(
        event._dispatchListeners,
        listener,
      );
      event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
    }
  }
}

參考上文,也就是說,合成的event保存了綁定事件節(jié)點(diǎn)到其父節(jié)點(diǎn)/實(shí)例的所有l(wèi)istener以及inst,但是我們還沒有看到具體的作用,我們繼續(xù)分析。

事件分發(fā)

事件已經(jīng)由插件合成完畢,我們回到 handleTopLevel方法,看看 runEventQueueInBatch(events)具體做了些什么?

function runEventQueueInBatch(events) {
  // 先將events事件放入隊(duì)列中,不做分析,類似push
  EventPluginHub.enqueueEvents(events);
  //觸發(fā)該事件隊(duì)列中的所有事件
  EventPluginHub.processEventQueue(false);
}

來看看processEventQueue方法

processEventQueue: function(simulated) {
    // 獲取已合成事件隊(duì)列
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(
        processingEventQueue,
        executeDispatchesAndReleaseSimulated,
      );
    } else {
      // 讓所有事件執(zhí)行executeDispatchesAndReleaseTopLevel方法,
      forEachAccumulated(
        processingEventQueue,
        executeDispatchesAndReleaseTopLevel,
      );
    }
    // This would be a good time to rethrow if any of the event handlers threw.
    ReactErrorUtils.rethrowCaughtError();
  }

往下分析

/**
 *
 * @param e 事件隊(duì)列中的某個(gè)
 */
var executeDispatchesAndReleaseTopLevel = function(e) {
  return executeDispatchesAndRelease(e, false);
};
var executeDispatchesAndRelease = function(event, simulated) {
  if (event) {
    EventPluginUtils.executeDispatchesInOrder(event, simulated);

    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};

executeDispatchesAndReleaseTopLevel最后本質(zhì)是調(diào)用了EventPluginUtils.executeDispatchesInOrder(event, simulated)方法

function executeDispatchesInOrder(event, simulated) {
  // 合成事件中的對(duì)象以及事件回調(diào)
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      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;
}

這段代碼相對(duì)簡(jiǎn)單了,如果有處理這個(gè)事件的回調(diào)函數(shù),就調(diào)用executeDispatch進(jìn)行處理

function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || 'unknown-event';
  // 獲取具體dom
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  
  if (simulated) {
    // 在15.6版本invokeGuardedCallbackWithCatch和下面的invokeGuardedCallback代碼是相同的,具體的代碼就是執(zhí)行l(wèi)istener,并將event作為參數(shù)
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    //
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

到這里,拿到真實(shí)的dom,將合成的event賦予currentTarget,是不是和原生了event有點(diǎn)神似,最后,執(zhí)行了所綁定的回調(diào)函數(shù),直到這里我們發(fā)現(xiàn)了最開始的問題,為什么我們?cè)?strong>componentClick中調(diào)用的stopPropagation并沒有干擾到dom的事件,因?yàn)檫@個(gè)時(shí)候我們拿到的event并不是真實(shí)dom的事件,是完全由react封裝實(shí)現(xiàn)了標(biāo)準(zhǔn)dom的虛擬事件。

總結(jié)

react為什么要做這么一套復(fù)雜的事件系統(tǒng)?用原生的不好嗎?
我覺得這么設(shè)計(jì)至少可以有(還有什么好處歡迎告訴我)

  1. 自己實(shí)現(xiàn)事件,可以有效的規(guī)避不同瀏覽器對(duì)事件的兼容性錯(cuò)誤。
  2. 全局事件代理,對(duì)系統(tǒng)優(yōu)化無疑的巨大的性能提升。

參考資料

[React源碼分析7 — React合成事件系統(tǒng)](http://blog.csdn.net/u013510838/article/details/612247600

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容