前言
Facebook 的研發能力真是驚人, Fiber
架構給 React 帶來了新視野的同時,將排程一詞介紹給了前端,然而這個架構實在不好懂,比起以前的 Vdom
樹,新的 Fiber
樹就麻煩太多。
可以說,React 16 和 React 15 已經是技巧上的分水嶺,但是得益於 React 16 的 Fiber
架構,使得 React 即使在沒有開啟非同步的情況下,效能依舊是得到了提高。
經過兩個星期的痛苦研究,終於將 React 16 的渲染脈絡摸得比較清晰,可以寫文章來記錄、回顧一下。
如果你已經稍微理解了 Fiber
架構,可以直接看程式碼:倉庫地址
什麼是 React Fiber ?
React Fiber
並不是所謂的纖程(微執行緒、協程),而是一種基於瀏覽器的單執行緒排程演算法,背後的支援 API 是大名鼎鼎的: requestIdleCallback
,得到了這個 API 的支援,我們便可以將 React 中最耗時的部分放入其中。
回顧 React 歷年來的演算法都知道,reconcilation
演算法實際上是一個大遞迴,大遞迴一旦進行,想要中斷還是比較不好操作的,加上頭大尾大的 React 15 程式碼已經膨脹到了不可思議的地步,在重重壓力之下,React 使用了大迴圈來代替之前的大遞迴,雖然程式碼變得比遞迴難懂了幾個梯度,但是實際上,程式碼量比原來少了非常多(開發版本 3W 行壓縮到了 1.3W 行)
那問題就來了,什麼是 Fiber
:一種將 recocilation
(遞迴 diff
),拆分成無數個小任務的演算法;它隨時能夠停止,恢復。停止恢復的時機取決於當前的一幀( 16ms
)內,還有沒有足夠的時間允許計算。
React 非同步渲染流程圖
- 使用者呼叫
ReactDOM.render
方法,傳入例如<App />
元件,React 開始運作<App />
<App />
在內部會被轉換成RootFiber
節點,一個特殊的節點,並記錄在一個全域性變數中,TopTree
- 拿到
<App />
的RootFiber
,首先建立一個<App />
對應的 Fiber ,然後加上 Fiber 資訊,以便之後回溯。隨後,賦值給之前的全域性變數 TopTree - 使用
requestIdleCallback
重複第三個步驟,直到迴圈到樹的所有節點 - 最後完成了
diff
階段,一次性將變化更新到真實DOM
中,以防止 UI 展示的不連續性
其中,重點就是 3
和 4
階段,這兩個階段將建立真實 DOM 和元件渲染 ( render
)拆分為無數的小碎塊,使用 requestIdleCallback
連續進行。在 React 15 的時候,渲染、建立、插入、刪除等操作是最費時的,在 React 16 中將渲染、建立抽離出來分片,這樣效能就得到了極大的提升。
那為什麼更新到真實 DOM 中不能拆分呢?理論上來說,是可以拆分的,但是這會造成 UI 的不連續性,極大的影響體驗。
遞迴變成了迴圈
以簡單的元件為例子:
- 從頂端的
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 資料結構
一個 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
屬性,這些屬性在錯誤邊界自爆時,用於恢復當前節點。
開始寫程式碼:Component 建構函式
講了那麼多的理論,大家一定是暈了,但是沒辦法,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
函式,我們接下來看看
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
之中。接下來,我們逐一分析三部曲的內部操作。
第一步:createWorkInProgress
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
。
第二步:performUnitOfWork
// 開始遍歷
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
的接下來,我們就來一起看看
beginWork
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
.
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
版本一定會的。
completeWork: 收集 effectTag
// 開始遍歷
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
階段。
第三步: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
作者,我等,一起來學習吧!