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

清夜發表於2018-10-28

上篇文件 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方法

// 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,否則說明佇列中只有一個事件,則無需遍歷直接呼叫即可

所以來看下 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,這個方法是事件處理的核心所在:

// 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相同的效果

如果迴圈沒有被中斷,則繼續執行 executeDispatch方法,這個方法接下來又一層一層地調了很多方法,最終來到 invokeGuardedCallbackImpl

// 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事件機制 - 原始碼概覽(下)

事件清理

事件執行完畢之後,接下來就是一些清理工作了,因為 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這種,比較複雜,程式碼方法很多,很容易看著看著就亂了,那麼就不要再幹看著了,直接寫個簡單的例子,然後在瀏覽器上打斷點,對著例子和原始碼一步步除錯,弄明白每一步的邏輯和目的,多除錯幾次後,基本上就能抓到關鍵點了,後續再通讀原始碼的時候,就會流暢很多了

相關文章