React Events

引言

官方文檔對React事件的介紹包含如下幾點css

  • React事件是合成事件
  • 有stopPropagation和preventDefault
  • 有nativeEvent上的全部屬性
  • 能夠經過nativeEvent獲取到原生事件
  • 跨瀏覽器兼容

那麼在看源碼以前,有如下疑問:html

  • 如何監聽?監聽的什麼元素?
  • 如何模擬捕獲和冒泡?
  • 如何實現stopPropagation?
  • 爲何要使用合成事件?
React 版本號 16.9.0

源碼閱讀說明

瞭解源碼最好的方式是單步調試,找一個最簡單的例子,在源碼中打斷點進行調試。本文采用create-react-app建立了最簡單的demo,只含有click事件。頁面內容以下react

import React from 'react';
import './App.css';

class App extends React.Component {
  spanClickEvent = null;
  headerClickEvent = null;

  componentDidMount () {
    // document.addEventListener('click', () => {
    //   console.log('document click');
    // })
  }

  spanClick (event) {
    event.stopPropagation();
    console.log('spanClick');
    console.log(event);
    // this.spanClickEvent = event;
  }
  headerClick (event) {
    console.log('headerClick');
    console.log(event);
    // this.headerClickEvent = event;
    // console.log(this.headerClickEvent === this.spanClickEvent);
  }
  inputChange (event) {
    console.log('inputChange');
    console.log(event);
  }
  render () {
    return (
    <div className="App">
      <header className="App-header" onClick={(event) => this.headerClick(event)}>
        <div className="btn-wrapper">
          <span className="btn" onClick={(event) => this.spanClick(event)}>
            <span>點擊</span>
          </span>
          {/* <input onChange={(event) => this.inputChange(event)}/> */}
        </div>
      </header>
    </div>
    )};
}

export default App;

事件註冊

首先刷新頁面,在render過程當中會走到以下邏輯瀏覽器

function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
      var parentNamespace = void 0;
      {
        // 此處省略代碼...
      }
      var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
      // 將internalInstanceHandle和props掛載在真實的DOM上,後面會用到
      precacheFiberNode(internalInstanceHandle, domElement);
      updateFiberProps(domElement, props);
      return domElement;
    }

updateFiberProps會走到setInitialDOMProperties裏面app

function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
    // 此處省略代碼
    else if (registrationNameModules.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        if (true && typeof nextProp !== 'function') {
          warnForInvalidEventListener(propKey, nextProp);
        }
        // 若是props含有事件相關的屬性,則去監聽對應的事件
        ensureListeningTo(rootContainerElement, propKey);
      }
    }
}

注意此處使用的registrationNameModules存放了全部React事件。dom

clipboard.png

ensureListeningTo會判斷當前是否在iframe裏面,以決定監聽哪裏的事件。最後走到listenTo邏輯裏面。異步

function listenTo(registrationName, mountAt) {
      //listeningSet存放了已經監聽過的事件,避免重複去監聽。
      var listeningSet = getListeningSetForElement(mountAt);
      var dependencies = registrationNameDependencies[registrationName];

      for (var i = 0; i < dependencies.length; i++) {
        var dependency = dependencies[i];

        if (!listeningSet.has(dependency)) {
          // 初始化span標籤的時候會走到這個邏輯裏面,header的時候就不會再重複去監聽click了
          switch (dependency) {
            // 此處省略代碼

            default:
              var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;

              if (!isMediaEvent) {
                trapBubbledEvent(dependency, mountAt);
              }

              break;
          }

          listeningSet.add(dependency);
        }
      }
    }

registrationNameDependencies存放了React事件與原生事件須要監聽的對應關係。以下圖中,若是使用onBlur則會監聽window的blur事件,若是使用onChange則會監聽blur/change/..等事件this

clipboard.png

接下來講trapBubbledEventspa

function addEventBubbleListener(element, eventType, listener) {
  // 注意此處element是document,第三個參數是false 
  element.addEventListener(eventType, listener, false);
}

此時的listener爲dispatchDiscreteEvent調試

至此,事件註冊完成。值得注意的是,React在生成的真實DOM中加入了兩個React屬性,一個放了元素的props,一個放了元素對應的FiberNode。
原生DOM和FiberNode的一個雙向關係。
clipboard.png
傳中...]

事件觸發

點擊span元素後,會走到dispatchDiscreteEvent邏輯裏面,會帶着nativeEvent調dispatchEvent方法。

clipboard.png

經過getEventTarget(nativeEvent)拿到當前的nativeEvent.target<span>點擊元素</span>,而後拿到DOM上含有__reactInternalInstance*的最近的元素,此處爲<span>點擊元素</span>。調用dispatchEventForPluginEventSystem,調用batchedEventUpdates,中間會調用runExtractedPluginEventsInBatch處理原生事件,將原生事件合成爲合成事件。最後會調用到traverseTwoPhase。這個方法主要是找到當前的path鏈

溫習currentTarget和target currentTarget表示事件處理程序當前正在處理事件的那個元素
target 事件的目標

clipboard.png

function traverseTwoPhase(inst, fn, arg) {
  var path = [];

  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }

  var i = void 0;

  for (i = path.length; i-- > 0;) {
    // 從外層到裏層遍歷元素,模擬捕獲
    fn(path[i], 'captured', arg);
  }

  for (i = 0; i < path.length; i++) {
    // 從裏層到外層遍歷元素,模擬冒泡
    fn(path[i], 'bubbled', arg);
  }
}

調用對應的fn也就是accumulateDirectionalDispatches

function accumulateDirectionalDispatches(inst, phase, event) {
      // 省略代碼
      // 在'bubble'階段的onClick對應onClick,而captured的onClick對應onClickCaptured。所以咱們在捕獲階段沒有事件能夠觸發。感興趣的能夠將demo中的onClick更改成onClickCaptured模擬捕獲觸發
      var listener = listenerAtPhase(inst, event, phase);

      if (listener) {
        // 依次拿到span.btn和header上的onClick,而且放進event._dispatchListeners
        event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
        event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
      }
    }

最後調用executeDispatchesInOrder,遍歷_dispatchListeners依次觸發。觸發的時候會判斷event.isPropagationStopped()是true仍是false

clipboard.png

function executeDispatch(event, listener, inst) {
    var type = event.type || 'unknown-event';
    // 賦值給currentTarget
    event.currentTarget = getNodeFromInstance(inst);
    invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
    event.currentTarget = null;
  }

最後調用到fakeNode.dispatchEvent觸發callCallback真正的onClick事件。此處採用fakeNode.dispatchEvent是爲了讓事件仍然是瀏覽器發起的。

clipboard.png

調用完畢後,會將event初始化爲最初的狀態

整個過程,能夠發現如下問題

  • 在事件冒泡階段,使用的是同一個事件,只不過調用時的currentTarget不同。即spanClickEvent ==== headerClickEvent
  • 所以在處理事件時,必定提早把值取出來。不然像有異步操做的時候,是取不到currentTarget的值的
  • onClick對應有onClickCaptured
  • 由於整個過程使用的是同一個event,因此stopPropagation在調用的時候會將event上的isPropagationStopped置爲true,則後面的事件將再也不觸發,以此來模擬冒泡中止

clipboard.png

總結

回到最初的問題提問

  • 如何監聽?監聽的什麼元素?(使用到的根據registrationNameDependencies對應關係纔會去監聽,且使用一個set避免重複監聽。監聽了document元素)
  • 如何模擬捕獲和冒泡?(找到元素的path鏈,按不一樣順序依次取出對應的事件)
  • 如何實現stopPropagation?(同一個event,利用其屬性stopPropagation是否返回false標記)
  • 爲何要使用合成事件?(抹平瀏覽器差別)

課後題

例子中的span調用了stopPropagation,那麼如下代碼會觸發嗎

componentDidMount () {
    document.addEventListener('click', () => {
      // 依然會觸發。爲何?
      console.log('document click');
    })
    window.addEventListener('click', () => {
        // 不會觸發。爲何?
      console.log('document click');
    })
  }

另外一個問題,React是何時removeEventListner的?目前的出來的結論是並無。以下例子,在點擊header時會觸發React的DispatchEvent沒有問題。可是在isShow爲false後,點擊span元素,仍然會觸發DispatchEvent。所以目前的結論是,React並無去移除無用的EventListner。這個問題歡迎在評論區交流

class App extends React.Component {

  constructor () {
      super();
      this.state = {
          isShow: true
      };
  }

  headerClick (event) {
    this.setState({
        isShow: false
    });
  }
  render () {
    return (
    <div className="App">
        {
            this.state.isShow ?
            <header className="App-header" onClick={(event) => this.headerClick(event)}>
            </header>
            : <span>點擊</span>
        }
    </div>
    )};
}
相關文章
相關標籤/搜索