Facebook 的研發能力真是驚人, Fiber
架構給 React 帶來了新視野的同時,將調度一詞介紹給了前端,然而這個架構實在很差懂,比起之前的 Vdom
樹,新的 Fiber
樹就麻煩太多。前端
能夠說,React 16 和 React 15 已是技巧上的分水嶺,可是得益於 React 16 的 Fiber
架構,使得 React 即便在沒有開啓異步的狀況下,性能依舊是獲得了提升。node
通過兩個星期的痛苦研究,終於將 React 16 的渲染脈絡摸得比較清晰,能夠寫文章來記錄、回顧一下。react
若是你已經稍微理解了 Fiber
架構,能夠直接看代碼:倉庫地址git
React Fiber
並非所謂的纖程(微線程、協程),而是一種基於瀏覽器的單線程調度算法,背後的支持 API 是大名鼎鼎的: requestIdleCallback
,獲得了這個 API 的支持,咱們即可以將 React 中最耗時的部分放入其中。github
回顧 React 歷年來的算法都知道,reconcilation
算法其實是一個大遞歸,大遞歸一旦進行,想要中斷仍是比較很差操做的,加上頭大尾大的 React 15 代碼已經膨脹到了難以想象的地步,在重重壓力之下,React 使用了大循環來代替以前的大遞歸,雖然代碼變得比遞歸難懂了幾個梯度,可是實際上,代碼量比原來少了很是多(開發版本 3W 行壓縮到了 1.3W 行)算法
那問題就來了,什麼是 Fiber
:一種將 recocilation
(遞歸 diff
),拆分紅無數個小任務的算法;它隨時可以中止,恢復。中止恢復的時機取決於當前的一幀( 16ms
)內,還有沒有足夠的時間容許計算。npm
ReactDOM.render
方法,傳入例如<App />
組件,React 開始運做<App />
<App />
在內部會被轉換成 RootFiber
節點,一個特殊的節點,並記錄在一個全局變量中,TopTree
<App />
的 RootFiber
,首先建立一個 <App />
對應的 Fiber ,而後加上 Fiber 信息,以便以後回溯。隨後,賦值給以前的全局變量 TopTreerequestIdleCallback
重複第三個步驟,直到循環到樹的全部節點diff
階段,一次性將變化更新到真實 DOM
中,以防止 UI 展現的不連續性其中,重點就是 3
和 4
階段,這兩個階段將建立真實 DOM 和組件渲染 ( render
)拆分爲無數的小碎塊,使用 requestIdleCallback
連續進行。在 React 15 的時候,渲染、建立、插入、刪除等操做是最費時的,在 React 16 中將渲染、建立抽離出來分片,這樣性能就獲得了極大的提高。數組
那爲何更新到真實 DOM 中不能拆分呢?理論上來講,是能夠拆分的,可是這會形成 UI 的不連續性,極大的影響體驗。瀏覽器
以簡單的組件爲例子:bash
div#root
向下走,先走左子樹div
有兩個孩子 span
,繼續走左邊的span
,之下只有一個 hello
,到此,再也不繼續往下,而是往上回到 span
span
有一個兄弟,所以往兄弟 span
走去span
有孩子 luy
,到此,不繼續往下,而是回到 luy
的老爹 span
luy
的老爹 span
右邊沒有兄弟了,所以回到其老爹 div
div
沒有任何的兄弟,所以回到頂端的 div#root
每通過一個 Fiber
節點,執行 render
或者 document.createElement
(或者更新 DOM
)的操做
一個 Fiber
數據結構比較複雜
const Fiber = { tag: HOST_COMPONENT, type: 'div', return: parentFiber, child: childFiber, sibling: null, alternate: currentFiber, stateNode: document.createElement('div') | instance, props: { children: [], className: 'foo' }, partialState: null, effectTag: PLACEMENT, effects: [] }
這是一個比較完整的 Fiber object
,他複雜的緣由是由於一個 Fiber
就表明了一個「正在執行或者執行完畢」的操做單元。這個概念不是那麼好理解,若是要說得簡單一點就是:之前的 VDOM
樹節點的升級版。讓咱們介紹幾個關鍵屬性:
Fiber
中的 return
屬性(之前叫 parent
)。 child
和 sibling
相似,表明這個 Fiber
的子 Fiber
和兄弟 Fiber
stateNode
這個屬性比較特殊,用於記錄當前 Fiber
所對應的真實 DOM
節點 或者 當前虛擬組件的實例,這麼作的緣由第一是爲了實現 Ref
,第二是爲了實現 DOM
的跟蹤tag
屬性在新版的 React
中一共有 14 種值,分別表明了不一樣的 JSX
類型。effectTag
和 effects
這兩個屬性爲的是記錄每一個節點 Diff
後須要變動的狀態,好比刪除,移動,插入,替換,更新等...alternate
屬性我想拿出來單獨說一下,這個屬性是 Fiber
架構新加入的屬性。咱們都知道,VDOM
算法是在更新的時候生成一顆新的 VDOM
樹,去和舊的進行對比。在 Fiber
架構中,當咱們調用 ReactDOM.render
或者 setState
以後,會生成一顆樹叫作:work-in-progress tree
,這一顆樹就是咱們所謂的新樹用來與咱們的舊樹進行對比,新的樹和舊的樹的 Fiber
是徹底不同的,此時,咱們就須要 alternate
屬性去連接新樹和舊樹。
司徒正美的研究中,一個 Fiber
和它的 alternate
屬性構成了一個聯嬰體,他們有共同的 tag
,type
,stateNode
屬性,這些屬性在錯誤邊界自爆時,用於恢復當前節點。
講了那麼多的理論,你們必定是暈了,可是沒辦法,Fiber
架構已經比以前的簡單 React 要複雜太多了,所以不可能期望一次性把 Fiber
的內容所有理解,須要反覆多看。
固然,結合代碼來梳理,思路舊更加清晰了。咱們在構建新的架構時,老的 Luy 代碼大部分都要進行重構了,先來看看幾個主要重構的地方:
export class Component { constructor(props, context) { this.props = props this.context = context this.state = this.state || {} this.refs = {} this.updater = {} } setState(updater) { scheduleWork(this, updater) } render() { throw 'should implement `render()` function' } } Component.prototype.isReactComponent = true
React.Component
的代碼props
,一個是 context
state
,refs
,updater
, updater
用於收集 setState
的信息,便於以後更新用。固然,在這個版本之中,我並無使用。setState
函數也並無作隊列處理,只是調用了 scheduleWork
這個函數Component.prototype.isReactComponent = true
,這段代碼表飾着,若是一個組件的類型爲 function
且擁有 isReactComponent
,那麼他就是一個有狀態組件,在建立實例時須要用 new
,而無狀態組件只須要 fn(props,context)
調用const tag = { HostComponent: 'host', ClassComponent: 'class', HostRoot: 'root', HostText: 6, FunctionalComponent: 1 } const updateQueue = [] export function render(Vnode, Container, callback) { updateQueue.push({ fromTag: tag.HostRoot, stateNode: Container, props: { children: Vnode } }) requestIdleCallback(performWork) //開始幹活 } export function scheduleWork(instance, partialState) { updateQueue.push({ fromTag: tag.ClassComponent, stateNode: instance, partialState: partialState }) requestIdleCallback(performWork) //開始幹活 }
咱們定義了一個全局變量 updateQueue
來記錄咱們全部的更新操做,每當 render
和 scheduleWork (setState)
觸發時,咱們都會往 updateQueue
中 push
一個狀態,而後,進而調用大名鼎鼎的 requestIdleCallback
進行更新。在這裏與以前的 react 15 最大不一樣是,更新階段和首次渲染階段獲得了統一,都是使用了 updateQueue
進行更新。
實際上這裏還有優化的空間,就是屢次 setState
的時候,應該合併成一次再進行 requestIdleCallback
的調用,不過這並非咱們的目標,咱們的目標是搞懂 Fiber
架構。requestIdleCallback
調用的是 performWork
函數,咱們接下來看看
const EXPIRATION_TIME = 1 // ms async 逾期時間 let nextUnitOfWork = null let pendingCommit = null function performWork(deadline) { workLoop(deadline) if (nextUnitOfWork || updateQueue.length > 0) { requestIdleCallback(performWork) //繼續幹 } } function workLoop(deadline) { if (!nextUnitOfWork) { //一個週期內只建立一次 nextUnitOfWork = createWorkInProgress(updateQueue) } while (nextUnitOfWork && deadline.timeRemaining() > EXPIRATION_TIME) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) } if (pendingCommit) { //當全局 pendingCommit 變量被負值 commitAllwork(pendingCommit) } }
熟悉 requestIdleCallback
的同窗必定對這兩個函數並不陌生,這兩個函數其實作的就是所謂的異步調度。
performWork
函數主要作了兩件事,第一件事就是拿到 deadline
進入咱們以前所謂的大循環,也就是正式進入處理新舊 Fiber
的 Diff
階段,這個階段比較的奇妙,咱們叫他 workLoop
階段。workLoop
會一次處理 1 個或者多個 Fiber
,具體處理多少個,要看每一幀具體還剩下多少時間,若是一個 Fiber
消耗太多時間,那麼就會等到下一幀再處理下一個 Fiber
,如此循環,遍歷整個 VDOM
樹。
在這裏咱們注意到,若是一個
Fiber
消耗太多時間,可能會致使一幀時間的逾期,不過其實沒什麼問題啦,也僅僅是一幀逾期而已,對於咱們視覺上並無多大的影響。
workLoop
函數主要是三部曲:
createWorkInProgress
這個函數會構建一顆樹的頂端,賦值給全局變量 nextUnitOfWork
,經過迭代的方式,不斷更新 nextUnitOfWork
直到遍歷完全部樹的節點。performUnitOfWork
函數是第二步,不斷的檢測當前幀是否還剩餘時間,進行 WorkInProgress
tree 的迭代WorkInProgress
tree 迭代完畢之後,調用 commitAllWork
,將全部的變動所有一次性的更新到 DOM
中,以保證 UI 的連續性全部的 Diff
和建立真實 DOM
的操做,都在 performUnitOfWork
之中,可是插入和刪除是在 commitAllWork
之中。接下來,咱們逐一分析三部曲的內部操做。
export function createWorkInProgress(updateQueue) { const updateTask = updateQueue.shift() if (!updateTask) return if (updateTask.partialState) { // 證實這是一個setState操做 updateTask.stateNode._internalfiber.partialState = updateTask.partialState } const rootFiber = updateTask.fromTag === tag.HostRoot ? updateTask.stateNode._rootContainerFiber : getRoot(updateTask.stateNode._internalfiber) return { tag: tag.HostRoot, stateNode: updateTask.stateNode, props: updateTask.props || rootFiber.props, alternate: rootFiber // 用於連接新舊的 VDOM } } function getRoot(fiber) { let _fiber = fiber while (_fiber.return) { _fiber = _fiber.return } return _fiber
這個函數的主要做用就是構建 workInProgress
樹的頂端並賦值給全局變量 nextUnitOfWork。
首先,咱們先從 updateQueue
中獲取一個任務對象 updateTask
。隨後,進行判斷是不是更新階段。而後獲取 workInProgress
樹的頂端。若是是第一次渲染, RootFiber
的值是空的,由於咱們並無構建任何的樹。
最後,咱們將返回一個 Fiber
對象,這個 Fiber
對象的標識符( tag
)是 HostRoot
。
// 開始遍歷 function performUnitOfWork(workInProgress) { const nextChild = beginWork(workInProgress) if (nextChild) return nextChild // 沒有 nextChild, 咱們看看這個節點有沒有 sibling let current = workInProgress while (current) { //收集當前節點的effect,而後向上傳遞 completeWork(current) if (current.sibling) return current.sibling //沒有 sibling,回到這個節點的父親,看看有沒有sibling current = current.return } }
咱們調用 performUnitOfWork
處理咱們的 workInProgress
。
整個函數作的事情其實就是一個左遍歷樹的過程。首先,咱們調用 beginWork
,得到一個當前 Fiber
下的第一個孩子,若是有直接返回出去給 nextUnitOfWork
,看成下一個處理的節點;若是沒有找到任何孩子,證實咱們已經到達了樹的底部,經過下面的 while
循環,回到當前節點的父節點,將當前 Fiber
下擁有 Effect
的孩子所有記錄下來,以便於以後更新 DOM
。
而後查找當前節點的父親節點,是否有兄弟,有就返回,當成下一個處理的節點,若是沒有,就繼續回溯。
整個過程用圖來表示,就是:
在討論第三部以前,咱們仍然有兩個迷惑的地方:
beginWork
是如何建立孩子的completeWork
是如何收集 effect
的接下來,咱們就來一塊兒看看function beginWork(currentFiber) { switch (currentFiber.tag) { case tag.ClassComponent: { return updateClassComponent(currentFiber) } case tag.FunctionalComponent: { return updateFunctionalComponent(currentFiber) } default: { return updateHostComponent(currentFiber) } } } function updateHostComponent(currentFiber) { // 當一個 fiber 對應的 stateNode 是原生節點,那麼他的 children 就放在 props 裏 if (!currentFiber.stateNode) { if (currentFiber.type === null) { //表明這是文字節點 currentFiber.stateNode = document.createTextNode(currentFiber.props) } else { //表明這是真實原生 DOM 節點 currentFiber.stateNode = document.createElement(currentFiber.type) } } const newChildren = currentFiber.props.children return reconcileChildrenArray(currentFiber, newChildren) } function updateFunctionalComponent(currentFiber) { let type = currentFiber.type let props = currentFiber.props const newChildren = currentFiber.type(props) return reconcileChildrenArray(currentFiber, newChildren) } function updateClassComponent(currentFiber) { let instance = currentFiber.stateNode if (!instance) { // 若是是 mount 階段,構建一個 instance instance = currentFiber.stateNode = createInstance(currentFiber) } // 將新的state,props刷給當前的instance instance.props = currentFiber.props instance.state = { ...instance.state, ...currentFiber.partialState } // 清空 partialState currentFiber.partialState = null const newChildren = currentFiber.stateNode.render() // currentFiber 表明老的,newChildren表明新的 // 這個函數會返回孩子隊列的第一個 return reconcileChildrenArray(currentFiber, newChildren) }
beginWork
實際上是一個判斷分支的函數,整個函數的意思是:
Fiber
是什麼類型,是 class
的走 class
分支,是 stateless
的走 stateless,是原生節點的走原生分支
stateNode
,則建立一個 stateNode
class
,則建立實例,調用 render
函數,渲染其兒子;若是是原生節點,調用 DOM API
建立原生節點;若是是 stateless
,就執行它,渲染出 VDOM
節點recocileChildrenArray
函數,將其每個孩子進行鏈表的連接,進行 diff
,而後返回當前 Fiber
之下的第一個孩子咱們來看看比較重要的 classComponent
的構建流程
function updateClassComponent(currentFiber) { let instance = currentFiber.stateNode if (!instance) { // 若是是 mount 階段,構建一個 instance instance = currentFiber.stateNode = createInstance(currentFiber) } // 將新的state,props刷給當前的instance instance.props = currentFiber.props instance.state = { ...instance.state, ...currentFiber.partialState } // 清空 partialState currentFiber.partialState = null const newChildren = currentFiber.stateNode.render() // currentFiber 表明老的,newChildren表明新的 // 這個函數會返回孩子隊列的第一個 return reconcileChildrenArray(currentFiber, newChildren) } function createInstance(fiber) { const instance = new fiber.type(fiber.props) instance._internalfiber = fiber return instance }
若是是首次渲染,那麼組件並無被實例話,此時咱們調用 createInstance
實例化組件,而後將當前的 props
和 state
賦值給 props
、state
,隨後咱們調用 render
函數,得到了新兒子 newChildren
。
渲染出新兒子以後,來到了新架構下最重要的核心函數 reconcileChildrenArray
.
const PLACEMENT = 1 const DELETION = 2 const UPDATE = 3 function placeChild(currentFiber, newChild) { const type = newChild.type if (typeof newChild === 'string' || typeof newChild === 'number') { // 若是這個節點沒有 type ,這個節點就多是 number 或者 string return createFiber(tag.HostText, null, newChild, currentFiber, PLACEMENT) } if (typeof type === 'string') { // 原生節點 return createFiber(tag.HOST_COMPONENT, newChild.type, newChild.props, currentFiber, PLACEMENT) } if (typeof type === 'function') { const _tag = type.prototype.isReactComponent ? tag.CLASS_COMPONENT : tag.FunctionalComponent return { type: newChild.type, tag: _tag, props: newChild.props, return: currentFiber, effectTag: PLACEMENT } } } function reconcileChildrenArray(currentFiber, newChildren) { // 對比節點,相同的標記更新 // 不一樣的標記 替換 // 多餘的標記刪除,而且記錄下來 const arrayfiyChildren = arrayfiy(newChildren) let index = 0 let oldFiber = currentFiber.alternate ? currentFiber.alternate.child : null let newFiber = null while (index < arrayfiyChildren.length || oldFiber !== null) { const prevFiber = newFiber const newChild = arrayfiyChildren[index] const isSameFiber = oldFiber && newChild && newChild.type === oldFiber.type if (isSameFiber) { newFiber = { type: oldFiber.type, tag: oldFiber.tag, stateNode: oldFiber.stateNode, props: newChild.props, return: currentFiber, alternate: oldFiber, partialState: oldFiber.partialState, effectTag: UPDATE } } if (!isSameFiber && newChild) { newFiber = placeChild(currentFiber, newChild) } if (!isSameFiber && oldFiber) { // 這個狀況的意思是新的節點比舊的節點少 // 這時候,咱們要將變動的 effect 放在本節點的 list 裏 oldFiber.effectTag = DELETION currentFiber.effects = currentFiber.effects || [] currentFiber.effects.push(oldFiber) } if (oldFiber) { oldFiber = oldFiber.sibling || null } if (index === 0) { currentFiber.child = newFiber } else if (prevFiber && newChild) { // 這裏不懂是幹嗎的 prevFiber.sibling = newFiber } index++ } return currentFiber.child }
這個函數作了幾件事
array
化,這麼作可以使得 react
的 render
函數返回數組currentFiber
是新的 workInProgress
上的一個節點,是屬於新的 VDOM
樹 ,而此時,咱們必需要找到舊的 VDOM
樹來進行比對。那麼在這裏, Alternate
屬性就起到了關鍵性做用,這個屬性連接了舊的 VDOM
,使得咱們可以獲取原來的 VDOM
type
與原來的相同,那麼咱們將新建一個 Fiber
,標記這個 Fiber
爲 UPDATE
type
與原來的不相同,那咱們使用 PALCEMENT
來標記他DELETION
,並構建一個 effect list
記錄下來currentFiber
的 child
字段中newFiber
用鏈表的形式將他們一塊兒推入到 currentFiber
中currentFiber
下的第一個孩子看着比較囉嗦,可是實際上作的就是構建鏈表和 diff
孩子的過程,這個函數有不少優化的空間,使用 key
之後,在這裏能提升不少的性能,爲了簡單,我並無對 key
進行操做,以後的 Luy
版本必定會的。
// 開始遍歷 function performUnitOfWork(workInProgress) { const nextChild = beginWork(workInProgress) if (nextChild) return nextChild // 沒有 nextChild, 咱們看看這個節點有沒有 sibling let current = workInProgress while (current) { //收集當前節點的effect,而後向上傳遞 completeWork(current) if (current.sibling) return current.sibling //沒有 sibling,回到這個節點的父親,看看有沒有sibling current = current.return } } //收集有 effecttag 的 fiber function completeWork(currentFiber) { if (currentFiber.tag === tag.classComponent) { // 用於回溯最高點的 root currentFiber.stateNode._internalfiber = currentFiber } if (currentFiber.return) { const currentEffect = currentFiber.effects || [] //收集當前節點的 effect list const currentEffectTag = currentFiber.effectTag ? [currentFiber] : [] const parentEffects = currentFiber.return.effects || [] currentFiber.return.effects = parentEffects.concat(currentEffect, currentEffectTag) } else { // 到達最頂端了 pendingCommit = currentFiber } }
這個函數作了兩件事,第一件事情就是收集當前 currentFiber
的 effectTag
,將其 append
到父 Fiber
的 effectlist
中去,經過循環一層一層往上,最終到達頂端 currentFiber.return === void 666
的時候,證實咱們到達了 root
,此時咱們已經把全部的 effect
收集到了頂端的 currentFiber.effect
上,並把它賦值給 pendingCommit
,進入 commitAllWork
階段。
終於,咱們已經經過不斷不斷的調用 requestIdleCallback
和 大循環,將咱們的全部變動都找出來放在了 workInProgress tree
裏,咱們接下來就要作最後一步:將全部的變動一次性的變動到真實 DOM
中,注意,這個階段裏咱們再也不運行建立 DOM
和 render
,所以,雖然咱們一次性變動全部的 DOM
,可是性能來講並非太差。
function commitAllwork(topFiber) { topFiber.effects.forEach(f => { commitWork(f) }) topFiber.stateNode._rootContainerFiber = topFiber topFiber.effects = [] nextUnitOfWork = null pendingCommit = null }
咱們直接拿到 TopFiber
中的 effects list
,遍歷,將變動所有打到 DOM
中去,而後咱們將全局變量清理乾淨。
function commitWork(effectFiber) { if (effectFiber.tag === tag.HostRoot) { // 表明 root 節點沒什麼必要操做 return } // 拿到parent的緣由是,咱們要將元素插入的點,插在父親的下面 let domParentFiber = effectFiber.return while (domParentFiber.tag === tag.classComponent || domParentFiber.tag === tag.FunctionalComponent) { // 若是是 class 就直接跳過,由於 class 類型的fiber.stateNode 是其自己實例 domParentFiber = domParentFiber.return } //拿到父親的真實 DOM const domParent = domParentFiber.stateNode if (effectFiber.effectTag === PLACEMENT) { if (effectFiber.tag === tag.HostComponent || effectFiber.tag === tag.HostText) { //經過 tag 檢查是否是真實的節點 domParent.appendChild(effectFiber.stateNode) } // 其餘狀況 } else if (effectFiber.effectTag == UPDATE) { // 更新邏輯 只能是沒實現 } else if (effectFiber.effectTag == DELETION) { //刪除多餘的舊節點 commitDeletion(effectFiber, domParent) } } function commitDeletion(fiber, domParent) { let node = fiber while (true) { if (node.tag == tag.classComponent) { node = node.child continue } domParent.removeChild(node.stateNode) while (node != fiber && !node.sibling) { node = node.return } if (node == fiber) { return } node = node.sibling } }
這一部分代碼是最好理解的了,就是作的是刪除和插入或者更新 DOM
的操做,值得注意的是,刪除操做依舊使用的鏈表操做。
最後來一段測試代碼:
import React from './Luy/index' import { Component } from './component' import { render } from './vdom' class App extends Component { state = { info: true } constructor(props) { super(props) setTimeout(() => { this.setState({ info: !this.state.info }) }, 1000) } render() { return ( <div> <span>hello</span> <span>luy</span> <div>{this.state.info ? 'imasync' : 'iminfo'}</div> </div> ) } } render(<App />, document.getElementById('root'))
咱們來看看動圖吧!當節點 mount
之後,過了 1 秒,就會更新,咱們簡單的更新就到此結束了
再看如下調用棧,咱們的 requestIdleCallback
函數已經正確的運行了。
若是你想下載代碼親自體驗,能夠到 Luy 倉庫中:
git clone https://github.com/Foveluy/Luy.git cd Luy npm i --save-dev npm run start
目前我能找到的全部資料都放在倉庫中:資料
一開始咱們就使用了一個數組來記錄 update
的信息,經過調用 requestIdleCallback
來將更新一個一個的取出來,大部分時間隊列裏只有一個。
取出來之後,使用從左向右遍歷的方式,用鏈表連接一個一個的 Fiber
,並作 diff
和建立,最後一次性的 patch
到真實 DOM
中去。
如今 react 的架構已經變得極其複雜,而本文也只是將 React 的總體架構通篇流程描述了一遍,裏面的細節依舊值得咱們的深究,好比,如何傳遞 context
,如何實現 ref
,如何實現錯誤邊界處理,聲明週期的處理,這些都是很大的話題,在接下去的文章裏,我會一步一步的將這些關係講清楚。
最後,感謝支持個人迷你框架項目:Luy ,如今正在向 Fiber
晉級!若是你喜歡,請給我一點 star🌟 表示鼓勵!謝謝
若是有什麼問題,能夠加入咱們的學習 QQ 羣: 370262116
,羣裏幾乎全部的迷你 React
做者都在了,包括 anu
做者司徒正美, omi
做者,我等,一塊兒來學習吧!