【React深入】React事件機制

ConardLi發表於2019-03-05

關於React事件的疑問

  • 1.為什麼要手動繫結this

  • 2.React事件和原生事件有什麼區別

  • 3.React事件和原生事件的執行順序,可以混用嗎

  • 4.React事件如何解決跨瀏覽器相容

  • 5.什麼是合成事件

下面是我閱讀過原始碼後,將所有的執行流程總結出來的流程圖,不會貼程式碼,如果你想閱讀程式碼看看具體是如何實現的,可以根據流程圖去原始碼裡尋找。

事件註冊

image

  • 元件裝載 / 更新。
  • 通過lastPropsnextProps判斷是否新增、刪除事件分別呼叫事件註冊、解除安裝方法。
  • 呼叫EventPluginHubenqueuePutListener進行事件儲存
  • 獲取document物件。
  • 根據事件名稱(如onClickonCaptureClick)判斷是進行冒泡還是捕獲。
  • 判斷是否存在addEventListener方法,否則使用attachEvent(相容IE)。
  • document註冊原生事件回撥為dispatchEvent(統一的事件分發機制)。

事件儲存

image

  • EventPluginHub負責管理React合成事件的callback,它將callback儲存在listenerBank中,另外還儲存了負責合成事件的Plugin
  • EventPluginHubputListener方法是向儲存容器中增加一個listener。
  • 獲取繫結事件的元素的唯一標識key
  • callback根據事件型別,元素的唯一標識key儲存在listenerBank中。
  • listenerBank的結構是:listenerBank[registrationName][key]

例如:

{
    onClick:{
        nodeid1:()=>{...}
        nodeid2:()=>{...}
    },
    onChange:{
        nodeid3:()=>{...}
        nodeid4:()=>{...}
    }
}
複製程式碼

事件觸發 / 執行

image

這裡的事件執行利用了React的批處理機制,在前一篇的【React深入】setState的執行機制中已經分析過,這裡不再多加分析。

  • 觸發document註冊原生事件的回撥dispatchEvent
  • 獲取到觸發這個事件最深一級的元素

例如下面的程式碼:首先會獲取到this.child

      <div onClick={this.parentClick} ref={ref => this.parent = ref}>
        <div onClick={this.childClick} ref={ref => this.child = ref}>
          test
        </div>
      </div>
複製程式碼
  • 遍歷這個元素的所有父元素,依次對每一級元素進行處理。
  • 構造合成事件。
  • 將每一級的合成事件儲存在eventQueue事件佇列中。
  • 遍歷eventQueue
  • 通過isPropagationStopped判斷當前事件是否執行了阻止冒泡方法。
  • 如果阻止了冒泡,停止遍歷,否則通過executeDispatch執行合成事件。
  • 釋放處理完成的事件。

react在自己的合成事件中重寫了stopPropagation方法,將isPropagationStopped設定為true,然後在遍歷每一級事件的過程中根據此遍歷判斷是否繼續執行。這就是react自己實現的冒泡機制。

合成事件

image

  • 呼叫EventPluginHubextractEvents方法。
  • 迴圈所有型別的EventPlugin(用來處理不同事件的工具方法)。
  • 在每個EventPlugin中根據不同的事件型別,返回不同的事件池。
  • 在事件池中取出合成事件,如果事件池是空的,那麼建立一個新的。
  • 根據元素nodeid(唯一標識key)和事件型別從listenerBink中取出回撥函式
  • 返回帶有合成事件引數的回撥函式

總流程

將上面的四個流程串聯起來。

image

為什麼要手動繫結this

通過事件觸發過程的分析,dispatchEvent呼叫了invokeGuardedCallback方法。

function invokeGuardedCallback(name, func, a) {
  try {
    func(a);
  } catch (x) {
    if (caughtError === null) {
      caughtError = x;
    }
  }
}
複製程式碼

可見,回撥函式是直接呼叫呼叫的,並沒有指定呼叫的元件,所以不進行手動繫結的情況下直接獲取到的thisundefined

這裡可以使用實驗性的屬性初始化語法 ,也就是直接在元件宣告箭頭函式。箭頭函式不會建立自己的this,它只會從自己的作用域鏈的上一層繼承this。因此這樣我們在React事件中獲取到的就是元件本身了。

和原生事件有什麼區別

  • React 事件使用駝峰命名,而不是全部小寫。

  • 通過 JSX , 你傳遞一個函式作為事件處理程式,而不是一個字串。

例如,HTML

<button onclick="activateLasers()">
  Activate Lasers
</button>
複製程式碼

React 中略有不同:

<button onClick={activateLasers}>
  Activate Lasers
</button>
複製程式碼

另一個區別是,在 React 中你不能通過返回false 來阻止預設行為。必須明確呼叫 preventDefault

由上面執行機制我們可以得出:React自己實現了一套事件機制,自己模擬了事件冒泡和捕獲的過程,採用了事件代理,批量更新等方法,並且抹平了各個瀏覽器的相容性問題。

React事件和原生事件的執行順序

  componentDidMount() {
    this.parent.addEventListener('click', (e) => {
      console.log('dom parent');
    })
    this.child.addEventListener('click', (e) => {
      console.log('dom child');
    })
    document.addEventListener('click', (e) => {
      console.log('document');
    })
  }

  childClick = (e) => {
    console.log('react child');
  }

  parentClick = (e) => {
    console.log('react parent');
  }

  render() {
    return (
      <div onClick={this.parentClick} ref={ref => this.parent = ref}>
        <div onClick={this.childClick} ref={ref => this.child = ref}>
          test
        </div>
      </div>)
  }
複製程式碼

執行結果:

image

由上面的流程我們可以理解:

  • react的所有事件都掛載在document
  • 當真實dom觸發後冒泡到document後才會對react事件進行處理
  • 所以原生的事件會先執行
  • 然後執行react合成事件
  • 最後執行真正在document上掛載的事件

react事件和原生事件可以混用嗎?

react事件和原生事件最好不要混用。

原生事件中如果執行了stopPropagation方法,則會導致其他react事件失效。因為所有元素的事件將無法冒泡到document上。

由上面的執行機制不難得出,所有的react事件都將無法被註冊。

合成事件、瀏覽器相容

  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }
複製程式碼

這裡, e 是一個合成的事件。 React 根據 W3C 規範 定義了這個合成事件,所以你不需要擔心跨瀏覽器的相容性問題。

事件處理程式將傳遞 SyntheticEvent 的例項,這是一個跨瀏覽器原生事件包裝器。 它具有與瀏覽器原生事件相同的介面,包括stopPropagation()preventDefault() ,在所有瀏覽器中他們工作方式都相同。

每個SyntheticEvent物件都具有以下屬性:

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
DOMEventTarget target
number timeStamp
string type
複製程式碼

React合成的SyntheticEvent採用了事件池,這樣做可以大大節省記憶體,而不會頻繁的建立和銷燬事件物件。

另外,不管在什麼瀏覽器環境下,瀏覽器會將該事件型別統一建立為合成事件,從而達到了瀏覽器相容的目的。

推薦閱讀

相關文章