4 運算符 4.1 算術運算符 4.1.1 概述 JavaScript 提供的算術運算符如下所示: | 類型 | 符號 | 示例| | | | | |加法運算符| + | a+b | |減法運算符| - | a-b | |乘法運算符| * | a*b | |除法運算符| / | a/b | |餘數運 ...
事件系統
react v17
事件綁定
事件綁定在函數 setInitialDOMProperties
setInitialDOMProperties 將在 complete 階段執行
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean
): void {
// *遍歷 props
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) { continue; }
const nextProp = nextProps[propKey];
if (...) { ... }
// *registrationNameDependencies 包含 react 支持的所有的事件,如果當前的 propKey 是 react支持的事件就進入該 if
else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
// !註意,這裡與 react v16 有所不同,v16 這裡直接執行 ensureListeningTo 函數,但是 v17 這裡不會執行。因為 enableEagerRootListeners 是一個常量,值一直為 true,if (false) 自然不會執行,並且在 react-dom.development.js 中直接沒有這個 if,只剩下 onScroll 的判斷。這樣更能說明問題了
if (!enableEagerRootListeners) {
// *忽略這個函數,並沒有執行
ensureListeningTo(rootContainerElement, propKey, domElement);
} else if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
那麼在 react v17 中遍歷到 onClick 這種事件的時候貌似並沒有做什麼。那麼事件綁定是什麼時候綁定的呢?其實在最開始的 createRootImpl
也就是創建 HostRootFiber
時就通過 listenToAllSupportedEvents
將所有支持的事件都綁定到了 rootContainerElement
(這裡也對應了 react v17 就將事件統統綁定到 rootContainer 而不是 document)
那麼事件處理函數又是多久綁定的呢?
通過對綁定的事件處理函數進行 debugger 可以發現,其實根本沒有將事件處理函數直接綁定到 rootContainerElement 上,而是直接使用的上面 listenToAllSupportedEvents
中綁定的事件。大概的流程為:
listenToAllSupportedEvents
為rootContainerElement
綁定所有的事件- 點擊子組件,其實就相當於在點擊
rootContainerElement
所以會觸發對應的點擊事件。 - 綁定事件的時候會根據事件優先順序綁定不同的處理函數,但是最終其實都是執行 dispatchEvent
dispatchEvent
內部將將進入其他函數,獲取觸發事件的元素,然後根據對應的 Fiber 然後在根據很多層函數,最終執行事件處理函數。
事件觸發
使用的案例
import React from 'react'
import './index.css'
class EventDemo extends React.Component{
state = {
count: 0,
}
onDemoClick = e => {this.setState({ count: this.state.count + 1 })}
onParentClick = () => {console.log('父級元素的點擊事件被觸發了');}
onParentClickCapture = () => {console.log('父級元素捕獲到點擊事件');}
onSubCounterClick = () => {console.log('子元素點擊事件');}
onSubCounterClickCapture = () => {console.log('子元素點擊事件 capture')}
render() {
const { count } = this.state
return <div className={'counter-parent'} onClick={this.onParentClick} onClickCapture={this.onParentClickCapture}>
counter-parent
<div onClick={this.onDemoClick} className={'counter'}>
counter:{count}
<div className={'sub-counter'} onClick={this.onSubCounterClick} onClickCapture={this.onSubCounterClickCapture}>
子組件
</div>
</div>
</div>
}
}
export default EventDemo
- 點擊子元素後,自然會執行 dispatchEvent
- 然後會進入 attemptToDispatchEvent
(如果沒有正在進行的事件?因為在進入 attemptToDispatchEvent 之前會進行 hasQueuedDiscreteEvents
hasQueuedDiscreteEvents 判斷 具體可以看 dispatchEvent)
然後在attemptToDispatchEvent
中會通過原生的事件參數(event)獲取到觸發事件的 DOM,然後通過該 DOM 獲取到對應的 Fiber
然後正常情況下會進入 dispatchEventForPluginEventSystem. dispatchEventForPluginEventSystem
一般會進入批量更新,也就是batchEventUpdates
,與 render 時的一樣,也會傳入一個匿名函數,不過該匿名函數內部執行的是:dispatchEventsForPlugins
.dispatchEventsForPlugins
內部又執行extractEvents
函數extractEvents
函數內部又會使用EventPlugin
創建 react合成事件 的 Event 參數,並且會遍歷 Fiber 鏈表,將將會觸發的事件統統放到 dispatchQueue 中(具體遍歷 Fiber 的函數是在 accumulateSinglePhaseListeners )。
accumulateSinglePhaseListeners
具體流程如下- 首先會判斷當前是 捕獲階段 還是 冒泡階段 根據階段的不同,使用不同的 reactEventName (例如:onClick 還是 onClickCapture)
- 然後會進入 while 迴圈,迴圈中會通過
reactEventName
獲取instance
的事件處理函數,即listener
如果listener
不為 null 那麼就會將{ currentTarget, instance, listener }
放到listeners
中(currentTarget
是當前的 dom 元素,instance
是當前的 Fiber,listener
是當前的事件處理函數),接著將instance
指向instance.return
繼續 while 迴圈。 - while 迴圈結束後,會將
listeners
返回出去。
extractEvents
執行完成後,就會開始執行 dispatchQueue 中的內容了。
針對我們的案例,分析一下具體的流程
- 點擊子元素,那麼就會直接觸發 root 的 clickCapture 事件
- 進入 dispatchEvent
- 進入 attemptToDispatchEvent, 獲取真實觸發事件的 dom 和對應的 Fiber
- 進入 dispatchEventForPluginEventSystem
- 進入 batchEventUpdates
- 進入 dispatchEventsForPlugins
- 進入 extractEvent 獲取 react合成事件參數
- 進入 accumulateSinglePhaseListeners 獲取 listeners 數組因為當前是捕獲階段(在代碼中會判斷是什麼階段),所以就只會收集捕獲階段的事件處理函數(直接 push 到 listeners 並不會像小冊中說的那樣 遇到捕獲事件就 unshift 可能是版本問題),經過測試可以得知,無論案例中是否綁定了 clickCapture 都會去試圖收集捕獲階段的事件處理函數, 只是收集不到而已
- 返回 extractEvent 將 listeners 放到 dispatchQueue 中去
- 返回 dispatchEventsForPlugins 進入 processDispatchQueue 內部會判斷當前到底是什麼階段,接著迴圈 dispatchQueue
- 進入 processDispatchQueueItemsInOrder ,根據階段不同,按照不同的順序執行 listeners,比如捕獲階段的話,就是從後往前,冒泡階段的話就是從前往後。
- 再經過一系列函數的包裹,最終順利執行函數。
- capture 階段完成後,直接進入 bubble 階段,再次按照上面的順序執行,最終 bubble 階段也完成。就是這樣。
註意:listeners 的結構應該是 { currentTarget, instance, listener }, dispatchQueue 的結構應該是 [ { event, listeners } ] 此時的 event 應該是 React合成事件 event
註意:在此案例中,無論是捕獲階段,還是冒泡階段,因為 listeners 是一個數組(該階段將要觸發的所有 listener 數組),所以 dispatchQueue 中都只有一個元素,不清楚在上面情況下 dispatchQueue 才有多個元素。