React事件源碼淺析

一、概覽

React實現本身封裝了一套事件系統,基本原理爲將全部的事件都代理到頂層元素上(如documen元素)上進行處理,帶來的好處有:javascript

  • 抹平各平臺的兼容性問題,其中不只包括不一樣瀏覽器之間的差別,並且在RN上也能帶來一致的開發體驗。
  • 更好的性能。事件代理是開發中常見的優化手段,React更進一步,包括複用合成事件類、事件池、批量更新等進一步提升性能。
本文基於React 16.8.1

二、幾個小問題

在詳細講解以前,先思考幾個問題,能夠幫助咱們更好理解React的事件系統。html

  • React事件系統與原生事件混用的執行順序問題html5

    class App extends React.Component {
      handleWrapperCaptureClick() {
        console.log('wrapper capture click')
      }
    
      handleButtonClick() {
        console.log('button click')
      }
    
      componentDidMount() {
        const buttonEle = document.querySelector('#btn')
        buttonEle.addEventListener('click', () => {
          console.log('button native click')
        })
    
        window.addEventListener('click', () => {
          console.log('window native click')
        })
      }
    
      render() {
        <div className="wrapper" onClickCapture={this.handleWrapperCaptureClick}>
          <button id="btn" onClick={this.handleButtonClick}>
            click me
          </button>
        </div>
      }
    }
  • 異步回調中獲取事件對象失敗問題java

    handleClick(e) {
        fetch('/a/b/c').then(() => {
            console.log(e)
        })
    }
  • React事件系統中與瀏覽器原生change事件有哪些差異

若是看完本文後,能清晰的回答出這幾個問題,說明你對React事件系統已經有比較清楚的理解了。下面就正式進入正文了。node

三、事件的綁定

事件綁定在/packages/react-dom/src/client/ReactDOMComponent.js文件中react

} else if (registrationNameModules.hasOwnProperty(propKey)) {
        if (nextProp != null) {
            ensureListeningTo(rootContainerElement, propKey);
        }
    }

若是propkey是registrationNameModules中的一個事件名,則經過ensureListeningTo方法綁定,其中registrationNameModules爲包含React全部事件一個的map,在事件plugin部分中會再提到。數組

function ensureListeningTo(rootContainerElement, registrationName) {
  const isDocumentOrFragment =
    rootContainerElement.nodeType === DOCUMENT_NODE ||
    rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
  const doc = isDocumentOrFragment
    ? rootContainerElement
    : rootContainerElement.ownerDocument;
  listenTo(registrationName, doc);
}

從ensureListeningTo方法中能夠看出,React事件掛載在document節點或者DocumentFragment上,listenTo方法則是真正將事件註冊的入口,截取部分代碼以下:瀏覽器

case TOP_FOCUS:
        case TOP_BLUR:
          trapCapturedEvent(TOP_FOCUS, mountAt);
          trapCapturedEvent(TOP_BLUR, mountAt);
          // We set the flag for a single dependency later in this function,
          // but this ensures we mark both as attached rather than just one.
          isListening[TOP_BLUR] = true;
          isListening[TOP_FOCUS] = true;
          break;
        case TOP_CANCEL:
        case TOP_CLOSE:
          if (isEventSupported(getRawEventName(dependency))) {
            trapCapturedEvent(dependency, mountAt);
          }
          break;
        case TOP_INVALID:
        case TOP_SUBMIT:
        case TOP_RESET:
          // We listen to them on the target DOM elements.
          // Some of them bubble so we don't want them to fire twice.
          break;
        default:
          // By default, listen on the top level to all non-media events.
          // Media events don't bubble so adding the listener wouldn't do anything.
          const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
          if (!isMediaEvent) {
            trapBubbledEvent(dependency, mountAt);
          }
          break;

部分特殊事件作單獨處理,默認將事件經過trapBubbledEvent放到綁定,trapBubbledEvent根據字面意思可知就是綁定到冒泡事件上。其中注意的是blur等事件是經過trapCapturedEvent綁定的,這是由於blur等方法不支持冒泡事件,可是支持捕獲事件,因此須要使用trapCapturedEvent綁定。緩存

接下來咱們看下trapBubbledEvent方法。app

function trapBubbledEvent(
  topLevelType: DOMTopLevelEventType,
  element: Document | Element,
) {
  if (!element) {
    return null;
  }
  const dispatch = isInteractiveTopLevelEventType(topLevelType)
    ? dispatchInteractiveEvent
    : dispatchEvent;

  addEventBubbleListener(
    element,
    getRawEventName(topLevelType),
    // Check if interactive and wrap in interactiveUpdates
    dispatch.bind(null, topLevelType),
  );
}

trapBubbledEvent就是將事件經過addEventBubbleListener綁定到document上的。dispatch則是事件的回調函數。dispatchInteractiveEvent和dispatchEvent的區別爲,dispatchInteractiveEvent在執行前會確保以前全部的任務都已執行,具體見/packages/react-reconciler/src/ReactFiberScheduler.js中的interactiveUpdates方法,該模塊不是本文討論的重點,感興趣能夠本身看看。

事件的綁定已經介紹完畢,下面介紹事件的合成及觸發,該部分爲React事件系統的核心。

四、事件的合成

事件在dispatch方法中將事件的相關信息保存到bookKeeping中,其中bookKeeping也有個bookKeeping池,從而避免了反覆建立銷燬變量致使瀏覽器頻繁GC。
建立完bookkeeping後就傳入handleTopLevel處理了,handleTopLevel主要是緩存祖先元素,避免事件觸發後找不到祖先元素報錯。接下來就進入runExtractedEventsInBatch方法了。

function runExtractedEventsInBatch(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: EventTarget,
) {
  const events = extractEvents(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  );
  runEventsInBatch(events);
}

runExtractedEventsInBatch代碼很短,可是很是重要,其中extractEvents經過不一樣插件合成事件,runEventsInBatch則是完成事件的觸發,事件觸發放到下一小節中再講,接下來先講事件的合成。

function extractEvents(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
  let events = null;

  for (let i = 0; i < plugins.length; i++) {
    // Not every plugin in the ordering may be loaded at runtime.
    const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
    if (possiblePlugin) {
      const extractedEvents = possiblePlugin.extractEvents(
        topLevelType,
        targetInst,
        nativeEvent,
        nativeEventTarget,
      );
      if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
      }
    }
  }
  return events;
}

能夠看到extractEvents經過遍歷全部插件的extractEvents方法合成事件,若是一個插件適用該事件,則返回一個events,不然返回爲null,意味着最後產生的events有多是個數組。每一個插件至少有兩部分組成:eventTypes和extractEvents,eventTypes會在初始化的時候生成前文提到的registrationNameModules,extractEvents用於合成事件。下面介紹SimpleEventPlugin和ChangeEventPlugin兩個插件。

插件是在初始化的時候經過EventPluginHubInjection插入的,並對其進行排序等初始化工做,不一樣的平臺會注入不一樣的插件。

SimpleEventPlugin

const SimpleEventPlugin: PluginModule<MouseEvent> & {
  isInteractiveTopLevelEventType: (topLevelType: TopLevelType) => boolean,
} = {
  eventTypes: eventTypes,

  isInteractiveTopLevelEventType(topLevelType: TopLevelType): boolean {
    const config = topLevelEventsToDispatchConfig[topLevelType];
    return config !== undefined && config.isInteractive === true;
  },

  extractEvents: function(
    topLevelType: TopLevelType,
    targetInst: null | Fiber,
    nativeEvent: MouseEvent,
    nativeEventTarget: EventTarget,
  ): null | ReactSyntheticEvent {
    const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if (!dispatchConfig) {
      return null;
    }
    let EventConstructor;
    switch (topLevelType) {
      case DOMTopLevelEventTypes.TOP_KEY_PRESS:
        // Firefox creates a keypress event for function keys too. This removes
        // the unwanted keypress events. Enter is however both printable and
        // non-printable. One would expect Tab to be as well (but it isn't).
        if (getEventCharCode(nativeEvent) === 0) {
          return null;
        }
      /* falls through */
      case DOMTopLevelEventTypes.TOP_KEY_DOWN:
      case DOMTopLevelEventTypes.TOP_KEY_UP:
        EventConstructor = SyntheticKeyboardEvent;
        break;
      case DOMTopLevelEventTypes.TOP_BLUR:
      case DOMTopLevelEventTypes.TOP_FOCUS:
        EventConstructor = SyntheticFocusEvent;
        break;
      case DOMTopLevelEventTypes.TOP_CLICK:
        // Firefox creates a click event on right mouse clicks. This removes the
        // unwanted click events.
        if (nativeEvent.button === 2) {
          return null;
        }
      /* falls through */
      case DOMTopLevelEventTypes.TOP_AUX_CLICK:
      case DOMTopLevelEventTypes.TOP_DOUBLE_CLICK:
      case DOMTopLevelEventTypes.TOP_MOUSE_DOWN:
      case DOMTopLevelEventTypes.TOP_MOUSE_MOVE:
      case DOMTopLevelEventTypes.TOP_MOUSE_UP:
      /* falls through */
      case DOMTopLevelEventTypes.TOP_MOUSE_OUT:
      case DOMTopLevelEventTypes.TOP_MOUSE_OVER:
      case DOMTopLevelEventTypes.TOP_CONTEXT_MENU:
        EventConstructor = SyntheticMouseEvent;
        break;
      case DOMTopLevelEventTypes.TOP_DRAG:
      case DOMTopLevelEventTypes.TOP_DRAG_END:
      case DOMTopLevelEventTypes.TOP_DRAG_ENTER:
      case DOMTopLevelEventTypes.TOP_DRAG_EXIT:
      case DOMTopLevelEventTypes.TOP_DRAG_LEAVE:
      case DOMTopLevelEventTypes.TOP_DRAG_OVER:
      case DOMTopLevelEventTypes.TOP_DRAG_START:
      case DOMTopLevelEventTypes.TOP_DROP:
        EventConstructor = SyntheticDragEvent;
        break;
      case DOMTopLevelEventTypes.TOP_TOUCH_CANCEL:
      case DOMTopLevelEventTypes.TOP_TOUCH_END:
      case DOMTopLevelEventTypes.TOP_TOUCH_MOVE:
      case DOMTopLevelEventTypes.TOP_TOUCH_START:
        EventConstructor = SyntheticTouchEvent;
        break;
      case DOMTopLevelEventTypes.TOP_ANIMATION_END:
      case DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION:
      case DOMTopLevelEventTypes.TOP_ANIMATION_START:
        EventConstructor = SyntheticAnimationEvent;
        break;
      case DOMTopLevelEventTypes.TOP_TRANSITION_END:
        EventConstructor = SyntheticTransitionEvent;
        break;
      case DOMTopLevelEventTypes.TOP_SCROLL:
        EventConstructor = SyntheticUIEvent;
        break;
      case DOMTopLevelEventTypes.TOP_WHEEL:
        EventConstructor = SyntheticWheelEvent;
        break;
      case DOMTopLevelEventTypes.TOP_COPY:
      case DOMTopLevelEventTypes.TOP_CUT:
      case DOMTopLevelEventTypes.TOP_PASTE:
        EventConstructor = SyntheticClipboardEvent;
        break;
      case DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE:
      case DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE:
      case DOMTopLevelEventTypes.TOP_POINTER_CANCEL:
      case DOMTopLevelEventTypes.TOP_POINTER_DOWN:
      case DOMTopLevelEventTypes.TOP_POINTER_MOVE:
      case DOMTopLevelEventTypes.TOP_POINTER_OUT:
      case DOMTopLevelEventTypes.TOP_POINTER_OVER:
      case DOMTopLevelEventTypes.TOP_POINTER_UP:
        EventConstructor = SyntheticPointerEvent;
        break;
      default:
        // HTML Events
        // @see http://www.w3.org/TR/html5/index.html#events-0
        EventConstructor = SyntheticEvent;
        break;
    }
    const event = EventConstructor.getPooled(
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    accumulateTwoPhaseDispatches(event);
    return event;
  },
};

能夠看到不一樣的事件類型會有不一樣的合成事件基類,而後再經過EventConstructor.getPooled生成事件。在default中的SyntheticEvent咱們能夠看到熟悉的preventDefault、stopPropagation、persist等方法,其中有個persist須要說明下,由上文可知事件對象會循環使用,因此一個事件完成後事件就會被回收,所以在異步回調中是拿不到事件的,而調用persist方法後會保持事件的引用不被回收。preventDefault則調用原生事件的preventDefault方法,並標記isDefaultPrevented,該屬性下一節會再繼續講。

合成事件以後,會經過accumulateTwoPhaseDispatches收集父級事件監聽並儲存到_dispatchListeners中,這裏是React事件系統模擬冒泡的關鍵。

export function traverseTwoPhase(inst, fn, arg) {
  const path = [];
  // 遍歷父級元素
  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }
  let i;
  // 分別放入捕獲和冒泡隊列中
  // fn爲accumulateDirectionalDispatches方法
  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) {
  // 提取綁定的監聽事件
  const listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    // 將提取到的綁定添加到_dispatchListeners中
    event._dispatchListeners = accumulateInto(
      event._dispatchListeners,
      listener,
    );
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

ChangeEventPlugin

const ChangeEventPlugin = {
  eventTypes: eventTypes,

  _isInputEventSupported: isInputEventSupported,

  extractEvents: function(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  ) {
    const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;

    let getTargetInstFunc, handleEventFunc;
    if (shouldUseChangeEvent(targetNode)) {
      getTargetInstFunc = getTargetInstForChangeEvent;
    } else if (isTextInputElement(targetNode)) {
      if (isInputEventSupported) {
        getTargetInstFunc = getTargetInstForInputOrChangeEvent;
      } else {
        getTargetInstFunc = getTargetInstForInputEventPolyfill;
        handleEventFunc = handleEventsForInputEventPolyfill;
      }
    } else if (shouldUseClickEvent(targetNode)) {
      getTargetInstFunc = getTargetInstForClickEvent;
    }

    if (getTargetInstFunc) {
      const inst = getTargetInstFunc(topLevelType, targetInst);
      if (inst) {
        const event = createAndAccumulateChangeEvent(
          inst,
          nativeEvent,
          nativeEventTarget,
        );
        return event;
      }
    }

    if (handleEventFunc) {
      handleEventFunc(topLevelType, targetNode, targetInst);
    }

    // When blurring, set the value attribute for number inputs
    if (topLevelType === TOP_BLUR) {
      handleControlledInputBlur(targetNode);
    }
  },
};

MDN中對change事件有如下描述:

事件觸發取決於表單元素的類型(type)和用戶對標籤的操做:

  • 當元素被:checked時(經過點擊或者使用鍵盤):<input type="radio"> 和 <input type="checkbox">;
  • 當用戶完成提交動做時(例如:點擊了 <select>中的一個選項,從 <input type="date">標籤選擇了一個日期,經過<input type="file">標籤上傳了一個文件,等);
  • 當標籤的值被修改而且失焦後,但並未進行提交(例如:對<textarea> 或者<input type="text">的值進行編輯後。)。

ChangeEventPlugin中shouldUseChangeEvent對應的<input type="date">與<input type="file">元素,監聽change事件;isTextInputElement對應普通input元素,監聽input事件;shouldUseClickEvent對應<input type="radio">與<input type="checkbox">元素,監聽click事件。

因此普通input元素中當時區焦點後纔會觸發change事件,而React的change事件在每次輸入的時候都會觸發,由於監聽的是input事件。

五、事件的觸發

截止到目前已經完成了事件的綁定與合成,接下來就是最後一步事件的觸發了。事件觸發的入口爲前文提到的runEventsInBatch方法,該方法中會遍歷觸發合成的事件。

function executeDispatchesInOrder(event) {
  const dispatchListeners = event._dispatchListeners;
  const dispatchInstances = event._dispatchInstances;
  // 遍歷觸發dispatchListeners中收集的事件
  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, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

其中event.isPropagationStopped()爲判斷是否須要阻止冒泡,須要注意的是由於是代理到document上的,原生事件早已冒泡到了document上,因此stopPropagation是沒法阻止原生事件的冒泡,只能阻止React事件的冒泡。
executeDispatch就是最終觸發回調事件的地方,並捕獲錯誤。至此React事件的綁定、合成與觸發都已經結束了。

六、結束

React事件系統初看比較複雜,其實理解後也並無那麼難。在解決跨平臺和兼容性的問題時,保持了高性能,有不少值得學習的地方。在看源代碼的時候,一開始也沒有頭緒,多打斷點,一點點調試,也就慢慢理解。文中若有不正確的地方,還望不吝指正。

相關文章
相關標籤/搜索