React事件機制 - 源碼概覽(下)

上篇文檔 React事件機制 - 源碼概覽(上)說到了事件執行階段的構造合成事件部分,本文接着繼續往下分析react

批處理合成事件

入口是 runEventsInBatch數組

// runEventsInBatch
// packages/events/EventPluginHub.js
export function runEventsInBatch( events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null, simulated: boolean, ) {
  if (events !== null) {
    eventQueue = accumulateInto(eventQueue, events);
  }
  const processingEventQueue = eventQueue;
  eventQueue = null;
  if (!processingEventQueue) {
    return;
  }
  if (simulated) {
    // react-test 纔會執行的代碼
    // ...
  } else {
    forEachAccumulated(
      processingEventQueue,
      executeDispatchesAndReleaseTopLevel,
    );
  }
  // This would be a good time to rethrow if any of the event handlers threw.
  rethrowCaughtError();
}
複製代碼

這個方法首先會將當前須要處理的 events事件,與以前沒有處理完畢的隊列調用 accumulateInto方法按照順序進行合併,組合成一個新的隊列,由於以前可能就存在還沒處理完的合成事件,這裏就又有獲得執行的機會了瀏覽器

若是合併後的隊列爲 null,即沒有須要處理的事件,則退出,不然根據 simulated來進行分支判斷調用對應的方法,這裏的 simulated標誌位,字面意思是 仿造的、僞裝的,其實這個字段跟 react-test,即測試用例有關,只有測試用例調用 runEventsInBatch方法的時候, simulated標誌位的值才爲true,除了這個地方之外,React源碼中還有其餘的不少地方都會出現 simulated,都是跟測試用例有關,看到了不用管直接走 else邏輯便可,因此咱們這裏就走 else的邏輯,調用 forEachAccumulated方法app

// packages/events/forEachAccumulated.js
function forEachAccumulated<T>( arr: ?(Array<T> | T), cb: (elem: T) => void, scope: ?any, ) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}
複製代碼

這個方法就是先看下事件隊列processingEventQueue是否是個數組,若是是數組,說明隊列中不止一個事件,則遍歷隊列,調用 executeDispatchesAndReleaseTopLevel,不然說明隊列中只有一個事件,則無需遍歷直接調用便可dom

因此來看下 executeDispatchesAndReleaseTopLevel這個方法:函數

// packages/events/EventPluginHub.js
const executeDispatchesAndReleaseTopLevel = function(e) {
  return executeDispatchesAndRelease(e, false);
};
// ...
const executeDispatchesAndRelease = function( event: ReactSyntheticEvent, simulated: boolean, ) {
  if (event) {
    executeDispatchesInOrder(event, simulated);

    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};
複製代碼

executeDispatchesAndReleaseTopLevel又調用了 executeDispatchesAndRelease,而後 executeDispatchesAndRelease這個方法先調用了 executeDispatchesInOrder,這個方法是事件處理的核心所在:post

// packages/events/EventPluginUtils.js
// executeDispatchesInOrder
export function executeDispatchesInOrder(event, simulated) {
  const dispatchListeners = event._dispatchListeners;
  const dispatchInstances = event._dispatchInstances;
  if (__DEV__) {
    validateEventDispatches(event);
  }
  if (Array.isArray(dispatchListeners)) {
    for (let 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;
}
複製代碼

首先對拿到的事件上掛在的 dispatchListeners,也就是以前拿到的當前元素以及其全部父元素上註冊的事件回調函數的集合,遍歷這個集合,若是發現遍歷到的事件的 event.isPropagationStopped()true,則遍歷的循環直接 break掉,這裏的 isPropagationStopped在前面已經說過了,它是用於標識當前 React Node上觸發的事件是否執行了 e.stopPropagation()這個方法,若是執行了,則說明在此以前觸發的事件已經調用 event.stopPropagation()isPropagationStopped的值被置爲 functionThatReturnsTrue,即執行後爲 true,當前事件以及後面的事件做爲父級事件就不該該再被執行了測試

這裏當 event.isPropagationStopped()true時,中斷合成事件的向上遍歷執行,也就起到了和原生事件調用 stopPropagation相同的效果ui

若是循環沒有被中斷,則繼續執行 executeDispatch方法,這個方法接下來又一層一層地調了不少方法,最終來到 invokeGuardedCallbackImplthis

// packages/shared/invokeGuardedCallbackImpl.js
let invokeGuardedCallbackImpl = function<A, B, C, D, E, F, Context>( name: string | null, func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed, context: Context, a: A, b: B, c: C, d: D, e: E, f: F, ) {
  const funcArgs = Array.prototype.slice.call(arguments, 3);
  try {
    func.apply(context, funcArgs);
  } catch (error) {
    this.onError(error);
  }
};
複製代碼

關鍵在於這一句:

func.apply(context, funcArgs);
複製代碼

funcArgs是什麼呢?其實就是合成事件對象,包括原生瀏覽器事件對象的基本上全部屬性和方法,除此以外還另外掛載了額外其餘一些跟 React合成事件相關的屬性和方法,而 func則就是傳入的事件回調函數,對於本示例來講,就等於clickHandler這個回調方法:

// func === clickHandler
clickHandler(e) {
  console.log('click callback', e)
}
複製代碼

funcArgs做爲參數傳入 func,也便是傳入 clickHandler,因此咱們就可以在 clickHandler這個函數體內拿到 e這個回調參數,也就能經過這個回調參數拿到其上面掛載的任何屬性和方法,例如一些跟原生瀏覽器對象相關的屬性和方法,以及原生事件對象自己(nativeEvent)

至此,事件執行完畢

這個過程流程圖以下:

事件清理

事件執行完畢以後,接下來就是一些清理工做了,由於 React採用了對象池的方式來管理合成事件,因此當事件執行完畢以後就要清理釋放掉,減小內存佔用,主要是執行了上面提到過的位於 executeDispatchesAndRelease方法中的 event.constructor.release(event);這一句代碼

這裏面的 release就是以下方法:

// packages/events/SyntheticEvent.js
function releasePooledEvent(event) {
  const EventConstructor = this;
  invariant(
    event instanceof EventConstructor,
    'Trying to release an event instance into a pool of a different type.',
  );
  event.destructor();
  if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
    EventConstructor.eventPool.push(event);
  }
}
複製代碼

這個方法主要作了兩件事,首先釋放掉 event上屬性佔用的內存,而後把清理後的 event對象再放入對象池中,能夠被後續事件對象二次利用

event.destructor();這句就是用於釋放內存的,destructor這個方法的字面意思是 析構,也就表示它是一個析構函數,瞭解 C/C++的人應該對這個名詞很熟悉,它通常都是用於 清理善後的工做,例如釋放掉構造函數申請的內存空間以釋放內存,這裏的 destructor方法一樣是有着這個做用

destructorSyntheticEvent上的方法,因此全部的合成事件都能拿到這個方法:

// packages/events/SyntheticEvent.js
destructor: function() {
  const Interface = this.constructor.Interface;
  for (const propName in Interface) {
    if (__DEV__) {
      Object.defineProperty(
        this,
        propName,
        getPooledWarningPropertyDefinition(propName, Interface[propName]),
      );
    } else {
      this[propName] = null;
    }
  }
  this.dispatchConfig = null;
  this._targetInst = null;
  this.nativeEvent = null;
  this.isDefaultPrevented = functionThatReturnsFalse;
  this.isPropagationStopped = functionThatReturnsFalse;
  this._dispatchListeners = null;
  this._dispatchInstances = null;
  // 如下省略部分代碼
  // ...
}
複製代碼

JavaScript引擎有本身的垃圾回收機制,通常來講不須要開發者親自去回收內存空間,但這並非說開發者就徹底沒法影響這個過程了,常見的手動釋放內存的方法就是將對象置爲 nulldestructor這個方法主要就是作這件事情,遍歷事件對象上全部屬性,並將全部屬性的值置爲 null

總結

React的事件機制看起來仍是比較複雜的,我本身看了幾遍源碼又對着調試了幾遍,如今又寫了分析文章,回頭再想一想其實主線仍是比較明確的,過完了源碼以後,再去看 react-dom/src/events/ReactBrowserEventEmitter.js這個源碼文件開頭的那一段圖形化註釋,整個流程就更加清晰了

順便分享一個看源碼的技巧,若是某份源碼,好比 React這種,比較複雜,代碼方法不少,很容易看着看着就亂了,那麼就不要再幹看着了,直接寫個簡單的例子,而後在瀏覽器上打斷點,對着例子和源碼一步步調試,弄明白每一步的邏輯和目的,多調試幾回後,基本上就能抓到關鍵點了,後續再通讀源碼的時候,就會流暢不少了

相關文章
相關標籤/搜索