做者: 凹凸曼-風魔小次郎javascript
誰都沒有看見過風,更不用說你和我了。可是當紙幣在飄的時候,咱們知道那是風在數錢。
React 影響着咱們工做的方方面面,咱們天天都在使用它,只窺其表卻難以窺其裏。正所謂看不如寫,本篇文章的目的就是從原理層面探究 React 是如何工做的。css
在寫文章以前,爲了方便理解,我準備了一個懶人調試倉庫 simple_react ,這個倉庫將 benchmark 用例(只有兩個 ^ ^)和 React 源碼共同放在 src 文件夾中,經過 snowpack 進行熱更新,能夠直接在源碼中加入 log 和 debuger 進行調試。固然這裏的「源碼」並非真的源碼,由於 React 源碼中充斥着巨量的 dev 代碼和不明確的功能函數,因此我對源碼進行了整理,用 typescript 對類型進行了規範,刪除了大量和核心流程無關的代碼(固然也誤刪了一些有關的 ^ ^)。html
若是你只是但願瞭解 React 的運行流程而不是寫一個能夠用的框架的話,那麼這個倉庫徹底能夠知足你學習的須要。固然,這個倉庫基於 React16.8 ,雖然這個版本並不包括當前的航道模型 Lane 等新特性,可是是我我的認爲比較穩定且更適合閱讀的一個版本。前端
(若是但願調試完整的源碼,也能夠參考 拉取源碼 經過 yarn link 來進行 debug)java
在瞭解 React 是如何工做以前,咱們應該確保瞭解幾點有關 React 的基礎知識。node
首先,咱們須要知道使用框架對於開發的意義是什麼。若是咱們還處於遠古時期使用純 JS 的階段,每次數據的改變都會引起組件的展現狀態改變,所以咱們須要去手動的操做 DOM 。若是在某一秒內,數據異步的連續改變了幾十次,根據展現邏輯咱們也須要連續對 DOM 進行幾十次修改。頻繁的 DOM 操做對網頁性能的影響是很大的,固然,建立 DOM 元素和修改 DOM 元素的屬性都不過度消耗性能,主要在於每次將新的 DOM 插入 document 都會致使瀏覽器從新計算佈局屬性,以及各個視圖層、合併、渲染。因此,這樣的代碼性能是十分低下的。react
能夠試想這樣一個場景。對於一個前端列表組件而言,當存在 3 條數據的時候展現 3 條,當存在 5 條數據的時候展現 5 條。也就是說 UI 的呈如今某種程度上必然會和數據存在某種邏輯關係。若是 JS 可以感知到關鍵數據的改變,使用一種高效的方式將 DOM 改寫成與數據相對應的狀態。那麼於開發者而言,就能夠專一於業務邏輯和數據的改變,工做效率也會大幅提升。git
因此, 框架 最核心的功能之一就是 高效地 達成 UI 層和數據層的統一。github
React 自己並非框架, React 只是一個 JavaScript 庫,他的做用是經過組件構建用戶界面,屬於 MVC 應用中的 View 視圖層。 React 經過 props 和 state 來簡化關鍵數據的存儲,對於一個 react 組件函數而言,在 1 秒內可能被執行不少次。而每一次被執行,數據被注入 JSX , JSX 並非真實的 DOM ,在 React 中會被轉換成 React.createElement(type, props, children)
函數,執行的結果就是 ReactElement 元素 ,也便是 虛擬 DOM ,用來描述在瀏覽器的某一幀中,組件應該被呈現爲何樣子。typescript
VirtualDom 並不是 React 專屬,就像 redux 也能夠在非 React 環境下使用同樣,它們只是一種設計的思路。
事實上, React 在使用 fiber 架構以前的 Virtual Dom 和 diff 過程要相對直觀一些。可是在引入了 fiber 架構以後整個流程變得冗長,若是單純想了解 VirtualDom 和 diff 過程的原理也能夠經過 simple-virtual-dom 這個倉庫來學習。
VirtualDom 的本質是利用 JS 變量 對真實 DOM 進行抽象,既然每一次操做 DOM 均可能觸發瀏覽器的重排消耗性能,那麼就可使用 VirtualDom 來緩存當前組件狀態,對用戶交互和數據的變更進行批次處理,直接計算出每一幀頁面應該呈現的最終狀態,而這個狀態是以 JS 變量 的形式存在於內存中的。因此經過 VirtualDom 既可以保證用戶看到的每一幀都響應了數據的變化,又能節約性能保證瀏覽器不出現卡頓。
首先咱們應該注意到 React(瀏覽器環境) 代碼的入口 render 函數
ReactDOM.render(<App />, domContainer)
這個 render 過程當中, React 須要作到的是根據用戶創造的 JSX 語法,構建出一個虛擬的樹結構(也就是 ReactElement 和 Fiber )來表示用戶 指望中 頁面中的元素結構。固然對於這個過程相對並不複雜(誤),由於此時的 document 內仍是一片虛無。就思路上而言,只須要根據虛擬 DOM 節點生成真實的 DOM 元素而後插入 document ,第一次渲染就算圓滿完成。
一般咱們會經過 Babel 將 JSX 轉換爲一個 JS 執行函數。例如咱們在 React 環境下用 JSX 中寫了一個標題組件
<h1 className='title'> <div>Class Component</div> </h1>
那麼這個組件被 Babel 轉換以後將會是
React.createElement('h1', { className: 'title' }, [ React.createElement('div', null, [ 'Class Component' ] ])
傳統編譯講究一個 JSON 化,固然 JSX 和 React 也沒有什麼關係, JSX 只是 React 推薦的一種拓展語法。固然你也能夠不用 JSX 直接使用 React.createElement 函數,可是對比上面的兩種寫法你就也能知道,使用純 JS 的心智成本會比簡明可見的 JSX 高多少。咱們能夠看出, React.createElement 須要接收 3 個參數,分別是 DOM 元素的標籤名,屬性對象以及一個子元素數組,返回值則是一個 ReactElement 對象。
事實上, JSX 編譯後的 json 結構自己就是一個對象,即便不執行 React.createElement 函數也已經初步可使用了。那麼在這個函數中咱們作了什麼呢。
一個 ReactElement 元素主要有 5 個關鍵屬性,咱們都知道要構建成一個頁面須要經過 html 描述元素的類型和結構,經過 style 和 class 去描述元素的樣式呈現,經過 js 和綁定事件來觸發交互事件和頁面更新。
因此最重要的是第一個屬性,元素類型 type
。若是這個元素是一個純 html 標籤元素,例如 div ,那麼 type 將會是字符串 div ,若是是一個 React 組件,例如
function App() { return ( <div>Hello, World!</div> ) }
那麼 type
的值將會指向 App 函數,固然 Class 組件 也同樣(衆所周知 ES6 的 Class 語法自己就是函數以及原型鏈構成的語法糖)
第二個屬性是 props
,咱們在 html 標籤中寫入的大部分屬性都會被收集在 props
中,例如 id 、 className 、 style 、 children 、點擊事件等等。
第三個第四個屬性分別是 key
和 ref
,其中 key
在數組的處理和 diff 過程當中有重要做用,而 ref
則是引用標識,在這裏就先不作過多介紹。
最後一個屬性是 $$typeof
,這個屬性會指向 Symbol(React.element)
。做爲 React 元素的惟一標識的同時,這個標籤也承擔了安全方面的功能。咱們已經知道了所謂的 ReactElement 其實就是一個 JS 對象。那麼若是有用戶惡意的向服務端數據庫中存入了某個有侵入性功能的 僞 React 對象,在實際渲染過程當中被當作頁面元素渲染,那麼將有可能威脅到用戶的安全。而 Symbol
是沒法在數據庫中被存儲的,換句話說, React 所渲染的全部元素,都必須是由 JSX 編譯的擁有 Symbol
標識的元素。(若是在低版本不支持 Symbol 的瀏覽器中,將會使用字符串替代,也就沒有這層安排保護了)
ok,接下來回到 render 函數。在這個函數中到底發生了什麼呢,簡單來講就是建立 Root
結構。
從設計者的角度,根據 單一職責原則 和 開閉口原則 須要有與函數體解耦的數據結構來告訴 React 應該怎麼操做 fiber 。而不是初次渲染寫一套邏輯,第二次渲染寫一套邏輯。所以, fiber 上有了更新隊列 UpdateQueue
和 更新鏈表 Update
結構
若是查看一下相關的定義就會發現,更新隊列 updateQueue
是多個更新組成的鏈表結構,而 update
的更新也是一個鏈表,至於爲何是這樣設計,試想在一個 Class Component 的更新函數中連續執行了 3 次 setState ,與其將其做爲 3 個更新掛載到組件上,不如提供一種更小粒度的控制方式。一句話歸納就是, setState 級別的小更新合併成一個狀態更新,組件中的多個狀態更新在組件的更新隊列中合併,就可以計算出組件的新狀態 newState
。
對於初次渲染而言,只須要在第一個 fiber 上,掛載一個 update
標識這是一個初次渲染的 fiber 便可。
// 更新根節點 export function ScheduleRootUpdate ( current: Fiber, element: ReactElement, expirationTime: number, suspenseConfig: SuspenseConfig | null, callback?: Function ) { // 建立一個update實例 const update = createUpdate(expirationTime, suspenseConfig) // 對於做用在根節點上的 react element update.payload = { element } // 將 update 掛載到根 fiber 的 updateQueue 屬性上 enqueueUpdate( current, update ) ScheduleWork( current, expirationTime ) }
做爲整個 Fiber 架構 中最核心的設計, Fiber 被設計成了鏈表結構。
若是是 React16 以前的樹狀結構,就須要經過 DFS 深度遍從來查找每個節點。而如今只須要將指針按照 child → sibling → return 的優先級移動,就能夠處理全部的節點
這樣設計還有一個好處就是在 React 工做的時候只須要使用一個全局變量做爲指針在鏈表中不斷移動,若是出現用戶輸入或其餘優先級更高的任務就能夠 暫停 當前工做,其餘任務結束後只須要根據指針的位置繼續向下移動就能夠繼續以前的工做。指針移動的規律能夠概括爲 自頂向下,從左到右 。
康康 fiber 的基本結構
其中
stateNode 表明這個 fiber 節點對應的真實狀態
接下來是初次渲染的幾個核心步驟,由於是初次渲染,核心任務就是將首屏元素渲染到頁面上,因此這個過程將會是同步的。
由於筆者是土貨沒學過英語,百度了下發現是 準備乾淨的棧 的意思。結合了下流程,能夠看出這一步的做用是在真正工做以前作一些準備,例如初始化一些變量,放棄以前未完成的工做,以及最重要的—— 建立雙向緩衝變量 WorkInProgress
let workInProgress: Fiber | null = null ... export function prepareFreshStack ( root: FiberRoot, expirationTime: number ) { // 重置根節點的finishWork root.finishedWork = null root.finishedExpirationTime = ExpirationTime.NoWork ... if (workInProgress !== null) { // 若是已經存在了WIP,說明存在未完成的任務 // 向上找到它的root fiber let interruptedWork = workInProgress.return while (interruptedWork !== null) { // unwindInterruptedWork // 抹去未完成的任務 unwindInterruptedWork(interruptedWork) interruptedWork = interruptedWork.return } } workInProgressRoot = root // 建立雙向緩衝對象 workInProgress = createWorkInProgress(root.current, null, expirationTime) renderExpirationTime = expirationTime workInProgressRootExitStatus = RootExitStatus.RootImcomplete }
這裏簡稱 WIP 好了,與之對應的是 current , current 表明的是當前頁面上呈現的組件對應的 fiber 節點,你能夠將其類比爲 git 中的 master 分支,它表明的是已經對外的狀態。而 WIP 則表明了一個 pending 的狀態,也就是下一幀屏幕將要呈現的狀態,就像是從 master 拉出來的一個 feature 分支,咱們能夠在這個分支上作任意的更改。最終協調完畢,將 WIP 的結果渲染到了頁面上,按照頁面內容對應 current 的原則, current 將會指向 WIP ,也就是說, WIP 取代了以前的 current ( git 的 master 分支)。
在這以前 current 和 WIP 的 alternate 字段分別指向彼此。
那麼 WIP 是如何被創造出來的呢:
// 根據已有 fiber 生成一個 workInProgress 節點 export function createWorkInProgress ( current: Fiber, pendingProps: any, expirationTime ): Fiber { let workInProgress = current.alternate if (workInProgress === null) { // 若是當前fiber沒有alternate // tip: 這裏使用的是「雙緩衝池技術」,由於咱們最多須要一棵樹的兩個實例。 // tip: 咱們能夠自由的複用未使用的節點 // tip: 這是異步建立的,避免使用額外的對象 // tip: 這一樣支持咱們釋放額外的內存(若是須要的話 workInProgress = createFiber( current.tag, pendingProps, current.key, current.mode ) workInProgress.elementType = current.elementType workInProgress.type = current.type workInProgress.stateNode = current.stateNode workInProgress.alternate = current current.alternate = workInProgress } else { // 咱們已經有了一個 WIP workInProgress.pendingProps = pendingProps // 重置 effectTag workInProgress.effectTag = EffectTag.NoEffect // 重置 effect 鏈表 workInProgress.nextEffect = null workInProgress.firstEffect = null workInProgress.lastEffect = null }
能夠看出 WIP 其實就是繼承了 current 的核心屬性,可是去除了一些反作用和工做記錄的 乾淨 的 fiber。
在工做循環中,將會執行一個 while
語句,每執行一次循環,都會完成對一個 fiber
節點的處理。在 workLoop 模塊中有一個指針 workInProgress 指向當前正在處理的 fiber ,它會不斷向鏈表的尾部移動,直到指向的值爲 null ,就中止這部分工做, workLoop 的部分也就結束了。
每處理一個 fiber 節點都是一個工做單元,結束了一個工做單元后 React 會進行一次判斷,是否須要暫停工做檢查有沒有更高優先級的用戶交互進來。
function workLoopConcurrent() { // 執行工做直到 Scheduler 要求咱們 yield while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); } }
跳出條件只有:
可是咱們如今討論的是第一次渲染,觸屏渲染的優先級高於一切,因此並不存在第二個限制條件。
function workLoopSync () { // 只要沒有完成reconcile就一直執行 while(workInProgress !== null) { workInProgress = performUnitOfWork(workInProgress as Fiber) } }
單元工做 performUnitOfWork 的主要工做是經過 beginWork 來完成。 beginWork 的核心工做是經過判斷 fiber.tag 判斷當前的 fiber 表明的是一個類組件、函數組件仍是原生組件,而且針對它們作一些特殊處理。這一切都是爲了最終步驟:操做真實 DOM 作準備,即經過改變 fiber.effectTag 和 pendingProps 告訴後面的 commitRoot 函數應該對真實 DOM 進行怎樣的改寫。
switch (workInProgress.tag) { // RootFiber case WorkTag.HostRoot: return updateHostRoot(current as Fiber, workInProgress, renderExpirationTime) // class 組件 case WorkTag.ClassComponent: { const Component = workInProgress.type const resolvedProps = workInProgress.pendingProps return updateClassComponent( current, workInProgress, Component, resolvedProps, renderExpirationTime ) } ... }
此處就以 Class 組件爲例,查看一下具體是如何構建的。
以前有提過,對於類組件而言, fiber.stateNode 會指向這個類以前構造過的實例。
// 更新Class組件 function updateClassComponent ( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpiration: number ) { // 若是這個 class 組件被渲染過,stateNode 會指向類實例 // 不然 stateNode 指向 null const instance = workInProgress.stateNode if (instance === null) { // 若是沒有構造過類實例 ... } else { // 若是構造過類實例 ... } // 完成 render 的構建,將獲得的 react 元素和已有元素進行調和 const nextUnitOfWork = finishClassComponent( current, workInProgress, Component, shouldUpdate, false, renderExpiration ) return nextUnitOfWork
若是這個 fiber 並無構建過類實例的話,就會調用它的構建函數,而且將更新器 updater 掛載到這個類實例上。(處理 setState 邏輯用的,事實上全部的類組件實例上的更新器都是同一個對象,後面會提到)
if (instance === null) { // 這個 class 第一次渲染 if (current !== null) { // 刪除 current 和 WIP 之間的指針 current.alternate = null workInProgress.alternate = null // 插入操做 workInProgress.effectTag |= EffectTag.Placement } // 調用構造函數,創造新的類實例 // 給予類實例的某個指針指向更新器 updater constructClassInstance( workInProgress, Component, nextProps, renderExpiration ) // 將屬性掛載到類實例上,而且觸發多個生命週期 mountClassInstance( workInProgress, Component, nextProps, renderExpiration ) }
若是實例已經存在,就須要對比新舊 props 和 state ,判斷是否須要更新組件(萬一寫了 shouldComponentUpdate 呢)。而且觸發一些更新時的生命週期鉤子,例如 getDerivedStateFromProps 等等。
else { // 已經 render 過了,更新 shouldUpdate = updateClassInstance( current, workInProgress, Component, nextProps, renderExpiration ) }
屬性計算完畢後,調用類的 render
函數獲取最終的 ReactElement ,打上 Performed 標記,表明這個類在本次渲染中已經執行過了。
// 完成Class組件的構建 function finishClassComponent ( current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderExpiration: number ) { // 錯誤 邊界捕獲 const didCaptureError = false if (!shouldUpdate && !didCaptureError) { if (hasContext) { // 拋出問題 return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpiration ) } } // 實例 const instance = workInProgress.stateNode let nextChildren nextChildren = instance.render() // 標記爲已完成 workInProgress.effectTag |= EffectTag.PerformedWork // 開始調和 reconcile reconcileChildren( current, workInProgress, nextChildren, renderExpiration ) return workInProgress.child }
調和過程
若是還記得以前的內容的話,咱們在一切工做開始以前只是構建了第一個根節點 fiberRoot 和第一個無心義的空 root ,而在單個元素的調和過程 reconcileSingleElement 中會根據以前 render 獲得的 ReactElement 元素構建出對應的 fiber 而且插入到整個 fiber 鏈表中去。
而且經過 placeSingleChild 給這個 fiber 的 effectTag 打上 Placement 的標籤,擁有 Placement 標記後這裏的工做就完成了,能夠將 fiber 指針移動到下一個節點了。
// 處理對象類型(單個節點) const isObjectType = isObject(newChild) && !isNull(newChild) // 對象 if (isObjectType) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { // 在遞歸調和結束,向上回溯的過程當中 // 給這個 fiber 節點打上 Placement 的 Tag return placeSingleChild( reconcileSingleElement( returnFiber, currentFirstChild, newChild, expirationTime ) ) } // 還有 Fragment 等類型 } } // 若是這時子元素是字符串或者數字,按照文字節點來處理 // 值得一提的是,若是元素的子元素是純文字節點 // 那麼這些文字不會被轉換成 fiber // 而是做爲父元素的 prop 來處理 if (isString(newChild) || isNumber(newChild)) { return placeSingleChild( reconcileSingleTextNode( returnFiber, currentFirstChild, '' + newChild, expirationTime ) ) } // 數組 if (isArray(newChild)) { return reconcileChildrenArray( returnFiber, currentFirstChild, newChild, expirationTime ) }
文章篇幅有限,對於函數組件和原生組件這裏就不作過多介紹。假設咱們已經完成了對於全部 WIP 的構建和調和過程,對於第一次構建而言,咱們須要插入大量的 DOM 結構,可是到如今咱們獲得的仍然是一些虛擬的 fiber 節點。
因此,在最後一次單元工做 performUnitOfWork 中將會執行 completeWork
,在此以前,咱們的單元工做是一步步向尾部的 fiber 節點移動。而在 completeWork
中,咱們的工做將是自底向上,根據 fiber 生成真實的 dom 結構,而且在向上的過程當中將這些結構拼接成一棵 dom 樹。
export function completeWork ( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: number ): Fiber | null { // 最新的 props const newProps = workInProgress.pendingProps switch (workInProgress.tag) { ... case WorkTag.HostComponent: { // pop 該 fiber 對應的上下文 popHostContext(workInProgress) // 獲取 stack 中的當前 dom const rootContainerInstance = getRootHostContainer() // 原生組件類型 const type = workInProgress.type if (current !== null && workInProgress.stateNode !== null) { // 若是不是初次渲染了,能夠嘗試對已有的 dom 節點進行更新複用 updateHostComponent( current, workInProgress, type as string, newProps, rootContainerInstance ) } else { if (!newProps) { throw new Error('若是沒有newProps,是不合法的') } const currentHostContext = getHostContext() // 建立原生組件 let instance = createInstance( type as string, newProps, rootContainerInstance, currentHostContext, workInProgress ) // 將以前全部已經生成的子 dom 元素裝載到 instance 實例中 // 逐步拼接成一顆 dom 樹 appendAllChildren(instance, workInProgress, false, false) // fiber 的 stateNode 指向這個 dom 結構 workInProgress.stateNode = instance // feat: 這個函數真的藏得很隱蔽,我不知道這些人是怎麼能註釋都不提一句的呢→_→ // finalizeInitialChildren 做用是將props中的屬性掛載到真實的dom元素中去,結果做爲一個判斷條件被調用 // 返回一個bool值,表明是否須要auto focus(input, textarea...) if (finalizeInitialChildren(instance, type as string, newProps, rootContainerInstance, currentHostContext)) { markUpdate(workInProgress) } } } } return null }
構建完畢後,咱們獲得了形以下圖,虛擬 dom 和 真實 dom,父元素和子元素之間的關係結構
截止到當前,調和 reconcile 工做已經完成,咱們已經進入了準備提交到文檔 ready to commit 的狀態。其實從進入 completeUnitOfWork 構建開始,後面的過程就已經和時間片,任務調度系統沒有關係了,此時一切事件、交互、異步任務都將屏氣凝神,聆聽接下來 dom 的改變。
// 提交根實例(dom)到瀏覽器真實容器root中 function commitRootImpl ( root: FiberRoot, renderPriorityLevel: ReactPriorityLevel ) { ... // 由於此次是整個組件樹被掛載,因此根 fiber 節點將會做爲 fiberRoot 的 finishedWork const finishedWork = root.finishedWork ... // effect 鏈表,即那些將要被插入的原生組件 fiber let firstEffect = finishedWork.firstEffect ... let nextEffect = firstEffect while (nextEffect !== null) { try { commitMutationEffects(root, renderPriorityLevel) } catch(err) { throw new Error(err) } } }
在 commitMutationEffects 函數以前其實對 effect 鏈表還進行了另外兩次遍歷,分別是一些生命週期的處理,例如 getSnapshotBeforeUpdate ,以及一些變量的準備。
// 真正改寫文檔中dom的函數 // 提交fiber effect function commitMutationEffects ( root: FiberRoot, renderPriorityLevel: number ) { // @question 這個 while 語句彷佛是多餘的 = = while (nextEffect !== null) { // 當前fiber的tag const effectTag = nextEffect.effectTag // 下方的switch語句只處理 Placement,Deletion 和 Update const primaryEffectTag = effectTag & ( EffectTag.Placement | EffectTag.Update | EffectTag.Deletion | EffectTag.Hydrating ) switch (primaryEffectTag) { case EffectTag.Placement: { // 執行插入 commitPlacement(nextEffect) // effectTag 完成實名制後,要將對應的 effect 去除 nextEffect.effectTag &= ~EffectTag.Placement } case EffectTag.Update: { // 更新現有的 dom 組件 const current = nextEffect.alternate commitWork(current, nextEffect) } } nextEffect = nextEffect.nextEffect } }
截至此刻,第一次渲染的內容已經在屏幕上出現。也就是說,真實 DOM 中的內容再也不對應此時的 current fiber ,而是對應着咱們操做的 workInProgress fiber ,即函數中的 finishedWork 變量。
// 在 commit Mutation 階段以後,workInProgress tree 已是真實 Dom 對應的樹了 // 因此以前的 tree 仍然是 componentWillUnmount 階段的狀態 // 因此此時, workInProgress 代替了 current 成爲了新的 current root.current = finishedWork
若是你是一個常用 React 的打工人,就會發現 React 中的 event 是「閱後即焚的」。假設這樣一段代碼:
import React, { MouseEvent } from 'react' function TestPersist () { const handleClick = ( event: MouseEvent<HTMLElement, globalThis.MouseEvent> ) => { setTimeout(() => console.log('event', event)) } return ( <div onClick={handleClick}>O2</div> ) }
若是咱們須要異步的獲取此次點擊事件在屏幕中的位置而且作出相應處理,那麼在 setTimeout 中可否達到目的呢。
答案是否認的,由於 React 使用了 事件委託 機制,咱們拿到的 event 對象並非原生的 nativeEvent ,而是被 React 挾持處理過的合成事件 SyntheticEvent ,這一點從 ts 類型中也能夠看出, 咱們使用的 MouseEvent 是從 React 包中引入的而不是全局的默認事件類型。在 handleClick 函數同步執行完畢的一瞬間,這個 event 就已經在 React 事件池中被銷燬了,咱們能夠跑這個組件康一康。
固然 React 也提供了使用異步事件對象的解決方案,它提供了一個 persist 函數,可讓事件再也不進入事件池。(在 React17 中爲了解決某些 issue ,已經重寫了合成事件機制,事件再也不由 document 來代理,官網的說法是合成事件再也不由事件池管理,也沒有了 persist 函數)
那,爲何要用事件委託呢。仍是回到那個經典的命題,渲染 2 個 div 固然橫着寫豎着寫都不要緊,若是是 1000 個組件 2000 個點擊事件呢。事件委託的收益就是:
ok,言歸正傳。咱們點擊事件到底發生了什麼呢。首先是在 React 的 render 函數執行以前,在 JS 腳本中就已經自動執行了事件的注入。
事件注入的過程稍微有一點複雜,不光模塊之間有順序,數據也作了很多處理,這裏不 po 太詳細的代碼。可能有人會問爲啥不直接寫死呢,瀏覽器的事件不也就那麼億點點。就像 Redux 不是專門爲 React 服務的同樣, React 也不是專門爲瀏覽器服務的。文章開頭也說了 React 只是一個 javascipt 庫,它也能夠服務 native 端、桌面端甚至各類終端。因此根據底層環境的不一樣動態的注入事件集也是很是合理的作法。
固然注入過程並不重要,咱們須要知道的就是 React 安排了每種事件在 JSX 中的寫法和原生事件的對應關係(例如 onClick 和 onclick ),以及事件的優先級。
/* ReactDOM環境 */ // DOM 環境的事件 plugin const DOMEventPluginOrder = [ 'ResponderEventPlugin', 'SimpleEventPlugin', 'EnterLeaveEventPlugin', 'ChangeEventPlugin', 'SelectEventPlugin', 'BeforeInputEventPlugin', ]; // 這個文件被引入的時候自動執行 injectEventPluginOrder // 肯定 plugin 被註冊的順序,並非真正引入 EventPluginHub.injectEventPluginOrder(DOMEventPluginOrder) // 真正的注入事件內容 EventPluginHub.injectEventPluginByName({ SimpleEventPlugin: SimpleEventPlugin })
這裏以 SimpleEventPlugin 爲例,點擊事件等咱們平時經常使用的事件都屬於這個 plugin。
// 事件元組類型 type EventTuple = [ DOMTopLevelEventType, // React 中的事件類型 string, // 瀏覽器中的事件名稱 EventPriority // 事件優先級 ] const eventTuples: EventTuple[] = [ // 離散的事件 // 離散事件通常指的是在瀏覽器中連續兩次觸發間隔最少 33ms 的事件(沒有依據,我猜的) // 例如你以光速敲打鍵盤兩次,這兩個事件的實際觸發時間戳仍然會有間隔 [ DOMTopLevelEventTypes.TOP_BLUR, 'blur', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CANCEL, 'cancel', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CHANGE, 'change', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CLICK, 'click', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CLOSE, 'close', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CONTEXT_MENU, 'contextMenu', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_COPY, 'copy', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CUT, 'cut', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_DOUBLE_CLICK, 'doubleClick', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_AUX_CLICK, 'auxClick', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_FOCUS, 'focus', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_INPUT, 'input', DiscreteEvent ], ... ]
那麼,這些事件的監聽事件是如何被註冊的呢。還記得在調和 Class 組件的時候會計算要向瀏覽器插入什麼樣的 dom 元素或是要如何更新 dom 元素。在這個過程當中會經過 diffProperty 函數對元素的屬性進行 diff 對比,其中經過 ListenTo 來添加監聽函數
你們都知道,最終被綁定的監聽事件必定是被 React 魔改過,而後綁定在 document 上的。
function trapEventForPluginEventSystem ( element: Document | Element | Node, topLevelType: DOMTopLevelEventType, capture: boolean ): void { // 生成一個 listener 監聽函數 let listener switch (getEventPriority(topLevelType)) { case DiscreteEvent: { listener = dispatchDiscreteEvent.bind( null, topLevelType, EventSystemFlags.PLUGIN_EVENT_SYSTEM ) break } ... default: { listener = dispatchEvent.bind( null, topLevelType, EventSystemFlags.PLUGIN_EVENT_SYSTEM ) } } // @todo 這裏用一個getRawEventName轉換了一下 // 這個函數就是 →_→ // const getRawEventName = a => a // 雖然這個函數什麼都沒有作 // 可是它的名字語義化的說明了這一步 // 目的是獲得瀏覽器環境下addEventListener第一個參數的合法名稱 const rawEventName = topLevelType // 將捕獲事件listener掛載到根節點 // 這兩個部分都是爲了爲了兼容 IE 封裝過的 addEventListener if (capture) { // 註冊捕獲事件 addEventCaptureListener(element, rawEventName, listener) } else { // 註冊冒泡事件 addEventBubbleListener(element, rawEventName, listener) } }
你們應該都知道 addEventListener 的第三個參數是控制監聽捕獲過程 or 冒泡過程的吧
ok,right now,鼠標點了下頁面,頁面調用了這個函數。開局就一個 nativeEvent 對象,這個函數要作的第一件事就是知道真正被點的那個組件是誰,其實看了一些源碼就知道, React 但凡是有什麼事兒第一個步驟老是找到須要負責的那個 fiber 。
首先,經過 nativeEvent 獲取目標 dom 元素也就是 dom.target
const nativeEventTarget = getEventTarget(nativeEvent)
export default function getEventTarget(nativeEvent) { // 兼容寫法 let target = nativeEvent.target || nativeEvent.srcElement || window // Normalize SVG // @todo return target.nodeType === HtmlNodeType.TEXT_NODE ? target.parentNode : target }
那麼如何經過 dom 拿到這個 dom 對應的 fiber 呢,事實上, React 會給這個 dom 元素添加一個屬性指向它對應的 fiber 。對於這個作法我是有疑問的,這樣的映射關係也能夠經過維護一個 WeekMap
對象來實現,操做一個 WeakMap
的性能或許會優於操做一個 DOM 的屬性,且後者彷佛不太優雅,若是你有更好的想法也歡迎在評論區指出。
每當 completeWork 中爲 fiber 構造了新的 dom,都會給這個 dom 一個指針來指向它的 fiber
// 隨機Key const randomKey = Math.random().toString(36).slice(2) // 隨機Key對應的當前實例的Key const internalInstanceKey = '__reactInternalInstance$' + randomKey // Key 對應 render 以後的 props const internalEventHandlersKey = '__reactEventHandlers$' + randomKey // 對應實例 const internalContianerInstanceKey = '__reactContainer$' + randomKey // 綁定操做 export function precacheFiberNode ( hostInst: object, node: Document | Element | Node ): void { node[internalInstanceKey] = hostInst } // 讀取操做 export function getClosestInstanceFromNode (targetNode) { let targetInst = targetNode[internalInstanceKey] // 若是此時沒有Key,直接返回null if (targetInst) { return targetInst } // 省略了一部分代碼 // 若是這個 dom 上面找不到 internalInstanceKey 這個屬性 // 就會向上尋找父節點,直到找到一個擁有 internalInstanceKey 屬性的 dom 元素 // 這也是爲何這個函數名要叫作 從 node 獲取最近的 (fiber) 實例 ... return null }
此時咱們已經擁有了原生事件的對象,以及觸發了事件的 dom 以及對應的 fiber ,就能夠從 fiber.memorizedProps 中取到咱們綁定的 onClick 事件。這些信息已經足夠生成一個 React 合成事件 ReactSyntheticEvent 的實例了。
React 聲明瞭一個全局變量 事件隊列 eventQueue ,這個隊列用來存儲某次更新中全部被觸發的事件,咱們須要讓這個點擊事件入隊。而後觸發。
// 事件隊列 let eventQueue: ReactSyntheticEvent[] | ReactSyntheticEvent | null = null export function runEventsInBatch ( events: ReactSyntheticEvent[] | ReactSyntheticEvent | null ) { if (events !== null) { // 存在 events 的話,加入事件隊列 // react 本身寫的合併數組函數 accumulateInto // 或許是 ES3 時期寫的吧 eventQueue = accumulateInto<ReactSyntheticEvent>(eventQueue, events) } const processingEventQueue = eventQueue // 執行完畢以後要清空隊列 // 雖然已經這些 event 已經被釋放了,但仍是會被遍歷 eventQueue = null if (!processingEventQueue) return // 將這些事件逐個觸發 // forEachAccumulated 是 React 本身實現的 foreach forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel) }
// 觸發一個事件而且馬上將事件釋放到事件池中,除非執行了presistent const executeDispatchesAndRelease = function (event: ReactSyntheticEvent) { if (event) { // 按照次序依次觸發和該事件類型綁定的全部 listener executeDispatchesInOrder(event) } // 若是沒有執行 persist 持久化 , 當即銷燬事件 if (!event.isPersistent()) { (event.constructor as any).release(event) } }
能夠看到合成事件的構造函數實例上掛載了一個函數 release ,用來釋放事件。咱們看一看 SyntheticEvent 的代碼,能夠發現這裏使用了一個事件池的概念 eventPool 。
Object.assign(SyntheticEvent.prototype, { // 模擬原生的 preventDefault 函數 preventDefault: function() { this.defaultPrevented = true; const event = this.nativeEvent; if (!event) { return; } if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } this.isDefaultPrevented = functionThatReturnsTrue; }, // 模擬原生的 stopPropagation stopPropagation: function() { const event = this.nativeEvent; if (!event) { return; } if (event.stopPropagation) { event.stopPropagation(); } else { event.cancelBubble = true; } this.isPropagationStopped = functionThatReturnsTrue; }, /** * 在每次事件循環以後,全部被 dispatch 過的合成事件都會被釋放 * 這個函數可以容許一個引用使用事件不會被 GC 回收 */ persist: function() { this.isPersistent = functionThatReturnsTrue; }, /** * 這個 event 是否會被 GC 回收 */ isPersistent: functionThatReturnsFalse, /** * 銷燬實例 * 就是將全部的字段都設置爲 null */ destructor: function() { const Interface = this.constructor.Interface; for (const propName in Interface) { this[propName] = null; } this.dispatchConfig = null; this._targetInst = null; this.nativeEvent = null; this.isDefaultPrevented = functionThatReturnsFalse; this.isPropagationStopped = functionThatReturnsFalse; this._dispatchListeners = null; this._dispatchInstances = null; }, });
React 在構造函數上直接添加了一個事件池屬性,其實就是一個數組,這個數組將被全局共用。每當事件被釋放的時候,若是線程池的長度尚未超過規定的大小(默認是 10 ),那麼這個被銷燬後的事件就會被放進事件池
// 爲合成事件構造函數添加靜態屬性 // 事件池爲全部實例所共用 function addEventPoolingTo (EventConstructor) { EventConstructor.eventPool = [] EventConstructor.getPooled = getPooledEvent EventConstructor.release = releasePooledEvent } // 將事件釋放 // 事件池有容量的話,放進事件池 function releasePooledEvent (event) { const EventConstructor = this event.destructor() if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) { EventConstructor.eventPool.push(event) } }
咱們都知道單例模式,就是對於一個類在全局最多隻會有一個實例。而這種事件池的設計至關因而 n 例模式,每次事件觸發完畢以後,實例都要還給構造函數放進事件池,後面的每次觸發都將複用這些乾淨的實例,從而減小內存方面的開銷。
// 須要事件實例的時候直接從事件池中取出 function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) { const EventConstructor = this if (EventConstructor.eventPool.length) { // 從事件池中取出最後一個 const instance = EventConstructor.eventPool.pop() EventConstructor.call( instance, dispatchConfig, targetInst, nativeEvent, nativeInst ) return instance } return new EventConstructor ( dispatchConfig, targetInst, nativeEvent, nativeInst ) }
若是在短期內瀏覽器事件被頻繁觸發,那麼將出現的現象是,以前事件池中的實例都被取出複用,然後續的合成事件對象就只能被老老實實從新建立,結束的時候經過放棄引用來被 V8 引擎的 GC 回收。
回到以前的事件觸發,若是不特意將屬性名寫成 onClickCapture 的話,那麼默認將被觸發的就會是冒泡過程。這個過程也是 React 模擬的,就是經過 fiber 逐層向上觸發的方式,捕獲過程也是同理。
咱們都知道正常的事件觸發流程是:
處於事件 階段是一個 try-catch 語句,這樣即便發生錯誤也會處於 React 的錯誤捕獲機制當中。咱們真正想要執行的函數實體就是在此被觸發:
export default function invodeGuardedCallbackImpl< A, B, C, D, E, F, Context >( name: string | null, func: (a: A, b: B, c: C, d: D, e: E, f: F) => void, context?: Context, a?: A, b?: B, c?: C, d?: D, e?: E, f?: F, ): void { const funcArgs = Array.prototype.slice.call(arguments, 3) try { func.apply(context, funcArgs) } catch (error) { this.onError(error) } }
當咱們使用類組件或是函數組件的時候,最終目的都是爲了獲得一份 JSX 來描述咱們的頁面。那麼其中就存在着一個問題—— React 是如何分辨函數組件和類組件的。
雖然在 ES6 中,咱們能夠輕易的看出 Class 和 函數的區別,可是別忘了,咱們實際使用的每每是 babel 編譯後的代碼,而類就是函數和原型鏈構成的語法糖。可能大部分人最直接的想法就是,既然類組件繼承了 React.Component ,那麼應該能夠直接使用類類型判斷就就行:
App instanceof React.Component
固然, React 採用的作法是在原型鏈上添加一個標識
Component.prototype.isReactComponent = {}
源碼中須要判斷是不是類組件的時候,就能夠直接讀取函數的 isReactComponent 屬性時,由於在函數(也是對象)自身找不到時,就會向上遊原型鏈逐級查找,直到到達 Object.prototype 對象爲止。
爲何 isReactComponent 是一個對象而不是布爾以及爲何不能用 instanceOf
以前咱們已經看懂了 React 的事件委託機制,那麼不如在一次點擊事件中嘗試修改組件的狀態來更新咱們的頁面。
首先康康 setState 是如何工做的,咱們知道 this.setState 是 React.Component 類中的方法:
/** * @description 更新組件state * @param { object | Function } partialState 下個階段的狀態 * @param { ?Function } callback 更新完畢以後的回調 */ Component.prototype.setState = function (partialState, callback) { if (!( isObject(partialState) || isFunction(partialState) || isNull )) { console.warn('setState的第一個參數應爲對象、函數或null') return } this.updater.enqueueSetState(this, partialState, callback, 'setState') }
看起來核心步驟就是觸發掛載在實例上的一個 updater 對象。默認的, updater 會是一個展位的空對象,雖然實現了 enqueueSetState 等方法,可是這些方法內部都是空的。
// 咱們初始化這個默認的update,真正的updater會被renderer注入 this.updater = updater || ReactNoopUpdateQueue
export const ReactNoopUpdateQueue = { /** * 檢查組件是否已經掛載 */ isMounted: function (publishInstance) { // 初始化ing的組件就別掛載不掛載了 return false }, /** * 強制更新 */ enqueueForceUpdate: function (publishInstance, callback, callerName) { console.warn('enqueueForceUpdate', publishInstance) }, /** * 直接替換整個state,一般用這個或者setState來更新狀態 */ enqueueReplaceState: function ( publishInstance, completeState, callback, callerName ) { console.warn('enqueueReplaceState', publishInstance) }, /** * 修改部分state */ enqueueSetState: function ( publishInstance, partialState, callback, callerName ) { console.warn('enqueueSetState', publishInstance) } }
還記得咱們在 render 的過程當中,是經過執行 Component.render() 來得到一個類組件的實例,當 React 獲得了這個實例以後,就會將實例的 updater 替換成真正的 classComponentUpdater :
function adoptClassInstance ( workInProgress: Fiber, instance: any ): void { instance.updater = classComponentUpdate ... }
剛剛咱們觸發了這個對象中的 enqueueSetState 函數,那麼能夠看看實現:
const classComponentUpdate = { isMounted, /** * 觸發組件狀態的更新 * @param inst ReactElement * @param payload any * @param callback 更新結束以後的回調 */ enqueueSetState( inst: ReactElement, payload: any, callback?: Function ) { // ReactElement -> fiber const fiber = getInstance(inst) // 當前時間 const currentTime = requestCurrentTime() // 獲取當前 suspense config const suspenseConfig = requestCurrentSuspenseConfig() // 計算當前 fiber 節點的任務過時時間 const expirationTime = computeExpirationForFiber( currentTime, fiber, suspenseConfig ) // 建立一個 update 實例 const update = createUpdate(expirationTime, suspenseConfig) update.payload = payload // 將 update 裝載到 fiber 的 queue 中 enqueueUpdate(fiber, update) // 安排任務 ScheduleWork(fiber, expirationTime) }, ... }
顯然,這個函數的做用就是得到類組件對應的 fiber ,更新它在任務調度器中的過時時間(領導給了新工做,天然要定新的 Deadline ),而後就是建立一個新的 update 任務裝載到 fiber 的任務隊列中。最後經過 ScheduleWork (告訴任務調度器來任務了,趕忙幹活) 要求從這個 fiber 開始調和,至於調和和更新的步驟咱們在第一次渲染中已經有了大體的瞭解。
順帶提一提 Hooks 中的 useState 。網絡上有挺多講解 hook 實現的文章已經講得很全面了,咱們只須要搞清楚如下幾點問題。
Q1. 函數組件不像類組件同樣擁有實例,數據存儲在哪裏
A1. 任何以 ReactElement 爲粒度的組件都須要圍繞 fiber ,數據存儲在 fiber.memorizedState 上
Q2. useState 的實現
A2. 若是你聽過了 useState 那麼你就應該聽過 useReducer ,若是聽過 reducer 就應該知道 redux。首先,useState 的本質就是 useReducer 的語法糖。咱們都知道構建一個狀態庫須要一個 reducer ,useState 就是當 reducer 函數爲 a => a
時的特殊狀況。
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { return typeof action === 'function' ? action(state) : action } function updateState<S>( initialState: (() => S) | S ): [ S, Dispatch<BasicStateAction<S>> ] { return updateReducer<S, (() => S) | S, any>(basicStateReducer, initialState) }
Q3. 爲何 Hooks 的順序和個數不容許改變
A3. 每次執行 Hooks 函數須要取出上一次渲染時數據的最終狀態,由於結構是鏈表而不是一個 Map,因此這些最終狀態也會是有序的,因此若是個數和次序改變會致使數據的錯亂。
雖然今年過時時間 expirationTime 機制已經被淘汰了,可是無論是航道模型仍是過時時間,本質上都是任務優先級的不一樣體現形式。
在探究運行機制以前咱們須要知道一個問題就是,爲何時間片的性能會優於同步計算的性能。此處借用司徒正美老師文章中的例子。
實驗 1,經過 for 循環一次性向 document 中插入 1000 個節點
function randomHexColor(){ return "#" + ("0000"+ (Math.random() * 0x1000000 << 0).toString(16)).substr(-6); } setTimeout(function() { var k = 0; var root = document.getElementById("root"); for(var i = 0; i < 10000; i++){ k += new Date - 0 ; var el = document.createElement("div"); el.innerHTML = k; root.appendChild(el); el.style.cssText = background:${randomHexColor()};height:40px ; } }, 1000);
實驗 2,進行 10 次 setTimeout 分批次操做,每次插入 100 個節點
function randomHexColor() { return "#" + ("0000" + (Math.random() * 0x1000000 << 0).toString(16)).substr(-6); } var root = document.getElementById("root"); setTimeout(function () { function loop(n) { var k = 0; console.log(n); for (var i = 0; i < 100; i++) { k += new Date - 0; var el = document.createElement("div"); el.innerHTML = k; root.appendChild(el); el.style.cssText = background:${randomHexColor()};height:40px ; } if (n) { setTimeout(function () { loop(n - 1); }, 40); } } loop(100); }, 1000);
相同的結果,第一個實驗花費了 1000 ms,而第二個實驗僅僅花費了 31.5 ms。
這和 V8 引擎的底層原理有關,咱們都知道瀏覽器是單線程,一次性須要作到 GUI 描繪,事件處理,JS 執行等多個操做時,V8 引擎會優先對代碼進行執行,而不會對執行速度進行優化。若是咱們稍微給瀏覽器一些時間,瀏覽器就可以進行 JIT ,也叫熱代碼優化。
簡單來講, JS 是一種解釋型語言,每次執行都須要被編譯成字節碼才能被運行。可是若是某個函數被屢次執行,且參數類型和參數個數始終保持不變。那麼這段代碼會被識別爲 熱代碼 ,遵循着「萬物皆可空間換時間」的原則,這段代碼的字節碼會被緩存,下次再次運行的時候就會直接被運行而不須要進行耗時的解釋操做。也就是 解釋器 + 編譯器 的模式。
作個比喻來講,咱們工做不能一直蠻幹,必需要給本身一些時間進行反思和總結,不然工做速度和效率始終是線性的,人也不會有進步。
還記得在 WorkLoop 函數中,每次處理完一個 fiber 都會跳出循環執行一次 shouldYield 函數進行判斷,是否應該將執行權交還給瀏覽器處理用戶時間或是渲染。看看這個 shouldYield 函數的代碼:
// 當前是否應該阻塞 react 的工做 function shouldYield (): boolean { // 獲取當前的時間點 const currentTime = getCurrentTime() // 檢查任務隊列中是否有任務須要執行 advanceTimers(currentTime) // 取出任務隊列中任務優先級最高的任務 const firstTask = peek(taskQueue) // 如下兩種狀況須要yield // 1. 當前任務隊列中存在任務,且第一個任務的開始時間還沒到,且過時時間小於當前任務 // 2. 處於固定的瀏覽器渲染時間區間 return ( ( currentTask !== null && firstTask !== null && (firstTask as any).startTime <= currentTime && (firstTask as any).expirationTime < currentTask.expirationTime ) // 當前處於時間片的阻塞區間 || shouldYieldToHost() ) }
決定一個任務當前是否應該被執行有兩個因素。
若是一個任務的過時時間已經到了必須執行,那麼這個任務就應該處於 待執行隊列 taskQueue 中。相反這個任務的過時時間還沒到,就能夠先放在 延遲列表 中。每一幀結束的時候都會執行 advanceTimer 函數,將一些延遲列表中到期的任務取出,插入待執行隊列。
多是出於最佳實踐考慮,待執行隊列是一個小根堆結構,而延遲隊列是一個有序鏈表。
回想一下 React 的任務調度要求,當一個新的優先級更高的任務產生,須要可以打斷以前的工做並插隊。也就是說,React 須要維持一個始終有序的數組數據結構。所以,React 自實現了一個小根堆,可是這個小根堆無需像堆排序的結果同樣總體有序,只須要保證每次進行 push 和 pop 操做以後,優先級最高的任務可以到達堆頂。
因此 shouldYield 返回 true 的一個關鍵條件就是,當前 taskQueue 堆中的堆頂任務的過時時間已經到了,那麼就應該暫停工做交出線程使用權。
那麼待執行的任務是如何被執行的呢。這裏咱們須要先了解 MessageChannel 的概念。Message
Channel 的實例會擁有兩個端口,其中第一個端口爲發送信息的端口,第二個端口爲接收信息的端口。當接收到信息就能夠執行指定的回調函數。
const channel = new MessageChannel() // 發送端 const port = channel.port2 // 接收端 channel.port1.onmessage = performWorkUntilDeadline // 在必定時間內儘量的處理任務
每當待執行任務隊列中有任務的時候,就會經過 Channel 的發送端發送一個空的 message ,當接收端異步地接收到這個信號的時候,就會在一個時間片內儘量地執行任務。
// 記錄任一時間片的結束時刻 let deadline = 0 // 單位時間切片長度 let yieldInterval = 5 // 執行任務直到用盡當前時間片空閒時間 function performWorkUntilDeadline () { if (scheduledHostCallback !== null) { // 若是有計劃任務,那麼須要執行 // 當前時間 const currentTime = getCurrentTime() // 在每一個時間片以後阻塞(5ms) // deadline 爲這一次時間片的結束時間 deadline = currentTime + yieldInterval // 既然能執行這個函數,就表明着還有時間剩餘 const hasTimeRemaining = true try { // 將當前阻塞的任務計劃執行 const hasMoreWork = scheduledHostCallback( hasTimeRemaining, currentTime ) if (!hasMoreWork) { // 若是沒有任務了, 清空數據 isMessageLoopRunning = false scheduledHostCallback = null } else { // 若是還有任務,在當前時間片的結尾發送一個 message event // 接收端接收到的時候就將進入下一個時間片 port.postMessage(null) } } catch (error) { port.postMessage(null) throw(error) } } else { // 壓根沒有任務,不執行 isMessageLoopRunning = false } }
咱們在以前說過,阻塞 WorkLoop 的條件有兩個,第一個是任務隊列的第一個任務還沒到時間,第二個條件就是 shouldYieldToHost 返回 true,也就是處於時間片期間。
// 此時是不是【時間片阻塞】區間 export function shouldYieldToHost () { return getCurrentTime() >= deadline }
總結一下,時間調度機制其實就是 fiber 遍歷任務 WorkLoop 和調度器中的任務隊列爭奪線程使用權的過程。不過區別是前者徹底是同步的過程,只會在每一個 while 的間隙去詢問 調度器 :我是否能夠繼續執行下去。而在調度器拿到線程使用權的每一個時間片中,都會盡量的處理任務隊列中的任務。
傳統武術講究點到爲止,以上內容,就是此次 React 原理的所有。在文章中我並無放出大量的代碼,只是放出了一些片斷用來佐證我對於源碼的一些見解和觀點,文中的流程只是一個循序思考的過程,若是須要查看更多細節仍是應該從源碼入手。
固然文中的不少觀點帶有主觀色彩,並不必定就正確,同時我也不認爲網絡上的其餘文章的說法就和 React 被設計時的初衷徹底一致,甚至 React 源碼中的不少寫法也未必完美。無論閱讀什麼代碼,咱們都不要神話它,而是應該辯證的去看待它。總的來講,功過 91 開。
前端世界並不須要第二個 React ,咱們學習的意義並非爲了證實咱們對這個框架有多麼瞭解。而是經過窺探這些頂級工程師的實現思路,去完善咱們本身的邏輯體系,從而成爲一個更加嚴謹的人。
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章。