目的是初識fiber並實現react基礎功能,請帶着下面幾個問題去閱讀此文。html
如何實現Fiber架構下可中斷和可恢復的的任務調度?node
此文章首發於@careteen/react,轉載請註明來源便可。倉庫存放全部實現代碼和示例,感興趣的能夠fork調試。
鏈表的優點react
Fiber架構git
React使用Fiberchrome
JavaScript就像一條單行道。
JavaScript是單線程運行的。在瀏覽器環境中,他須要負責頁面的JavaScript解析和執行、繪製、事件處理、靜態資源加載和處理。並且只能一個任務一個任務的執行,若是其中某個任務耗時很長,那後面的任務則執行不了,在瀏覽器端則會呈現卡死的狀態。api
React15的渲染和diff會遞歸比對VirtualDOM樹
,找出有增刪改的節點,而後同步更新他們,整個過程是一鼓作氣的。那麼若是頁面節點數量很是龐大,React
會一直霸佔着瀏覽器資源,一則會致使用戶觸發的事件得不到響應,二則會致使掉幀,用戶會感知到這些卡頓。數組
因此針對上述痛點,咱們指望將找出有增刪改的節點,而後同步更新他們這個過程分解成兩個獨立的部分,或者經過某種方式能讓整個過程可中斷可恢復的執行,相似於多任務操做系統的單處理器調度。
爲了實現進程的併發,操做系統會按照必定的調度策略,將CPU的執行權分配給多個進程,多個進程都有被執行的機會,讓他們交替執行,造成一種同時在運行的假象。由於CPU速度太快,人類根本感受不到。實際上在單核的物理環境下同時只有一個程序在運行。
玩遊戲時須要流暢的刷新率,也就是至少60赫茲。否則遊戲體驗極差。
那麼一個幀包含什麼呢?
一幀平均是16.66ms,主要分爲如下幾個部分
在樣式計算以前會執行腳本計算中使用到requestAnimationFrame
的callback
若是你還不瞭解 requestAnimationFrame,前往mdn查看實現的進度條示例。
在合成後還存在一個空閒階段
,即合成及以前的全部步驟耗時若不足16.66ms
,剩下的時間瀏覽器爲咱們提供了requestIdleCallback
進行調用,對其充分利用。
requestIdleCallback目前只支持chrome,須要 polyfill
大體流程以下:
requestIdleCallback示例
requestIdleCallback使開發者可以在主事件循環上執行後臺和低優先級工做,而不會影響延遲關鍵事件,如動畫和輸入響應。
因爲數組的大小是固定的,從數組的起點或者中間插入或移除項的成本很高。鏈表相對於傳統的數組的優點在於添加或移除元素的時候不須要移動其餘元素,須要添加和移除不少元素時,最好的選擇是鏈表,而非數組。 鏈表在React的Fiber架構和Hooks實現發揮很大的做用。
更多關於鏈表的實現和使用
如上可使用鏈表實現相似於React的setState方法
。
// 表示一個節點 class Update { constructor(payload, nextUpdate) { this.payload = payload this.nextUpdate = nextUpdate } }
一個節點須要payload
掛載數據,nextUpdate
指向下一個節點。
// 模擬鏈表 class UpdateQueue { constructor() { this.baseState = null this.firstUpdate = null this.lastUpdate = null } enqueue(update) { if (!this.firstUpdate) { this.firstUpdate = this.lastUpdate = update } else { this.lastUpdate.nextUpdate = update this.lastUpdate = update } } }
鏈表初始化時須要baseState
存放數據,firstUpdate
指向第一個節點,lastUpdate
指向最後一個節點。
以及enqueue
將節點鏈起來。
const isFunction = (func) => { return typeof func === 'function' } class UpdateQueue { forceUpdate() { let currentState = this.baseState || {} let currentUpdate = this.firstUpdate while(currentUpdate) { const nextState = isFunction(currentUpdate.payload) ? currentUpdate.payload(currentState) : currentUpdate.payload currentState = { ...currentState, ...nextState } currentUpdate = currentUpdate.nextUpdate } this.firstUpdate = this.lastUpdate = null return this.baseState = currentState } }
還須要forceUpdate
將全部節點掛載的數據合併。相似於React.setState()
參數可對象可函數。
在React15
及以前,React
會遞歸比對VirtualDOM
樹,找出須要變更的節點,而後同步更新它們。這個過程React
稱爲Reconciliation(協調)
。
在Reconciliation
期間,React
會一直佔用着瀏覽器資源,一則會致使用戶觸發的事件得不到響應, 二則會致使掉幀,用戶可能會感受到卡頓。下面將模擬其遍歷過程。
將上圖節點結構映射成虛擬DOM
const root = { key: 'A1', children: [ { key: 'B1', children: [ { key: 'C1', children: [] }, { key: 'C2', children: [] } ] }, { key: 'B2', children: [] } ] }
採用深度優先算法對其遍歷
詳解DFS
function walk(vdom, cb) { cb && cb(vdom) vdom.children.forEach(child => walk(child, cb)) } // Test walk(root, (node) => { console.log(node.key) // A1 B1 C1 C2 B2 })
在Dom-Diff
時也是如此遞歸遍歷對比,且存在兩個很是影響性能的問題。
詳解React的Dom-Diff
上面瀏覽器任務調度過程
提到在頁面合成後還存在一個空閒階段requestIdleCallback
。
下圖爲React結合空閒階段的調度過程
這是一種合做式調度,須要程序和瀏覽器互相信任。瀏覽器做爲領導者,會分配執行時間片(即requestIdleCallback)給程序去選擇調用,程序須要按照約定在這個時間內執行完畢,並將控制權交還瀏覽器。
Fiber是一個執行單元,每次執行完一個執行單元,React就會檢查如今還剩多少時間,若是沒有時間就將控制權交還瀏覽器;而後繼續進行下一幀的渲染。
React中使用鏈表將Virtual DOM
連接起來,每個節點表示一個Fiber
class FiberNode { constructor(type, payload) { this.type = type // 節點類型 this.key = payload.key // key this.payload = payload // 掛載的數據 this.return = null // 父Fiber this.child = null // 長子Fiber this.sibling = null // 相鄰兄弟Fiber } } // Test const A1 = new FiberNode('div', { key: 'A1' }) const B1 = new FiberNode('div', { key: 'B1' }) const B2 = new FiberNode('div', { key: 'B2' }) const C1 = new FiberNode('div', { key: 'C1' }) const C2 = new FiberNode('div', { key: 'C2' }) A1.child = B1 B1.return = A1 B1.sibling = B2 B1.child = C1 B2.return = A1 C1.return = B1 C1.sibling = C2 C2.return = B1
每次渲染有兩個階段:Reconciliation
(協調/render)階段和Commit
(提交)階段
下面將上面講到的幾個知識點串聯起來使用。
此階段測試例子 fiberRender.html,核心代碼存放 fiberRender.js。
上面Fiber也是一種數據結構
小結已經構建了Fiber樹,而後來開始遍歷,在第一次渲染中,全部操做類型都是新增。
根據Virtual DOM
去構建Fiber Tree
nextUnitOfWork = A1 requestIdleCallback(workLoop, { timeout: 1000 })
空閒時間去遍歷收集A1
根節點
function workLoop (deadline) { // 這一幀渲染還有空閒時間 || 沒超時 && 還存在一個執行單元 while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // 執行當前執行單元 並返回下一個執行單元 } if (!nextUnitOfWork) { console.log('render end !') } else { requestIdleCallback(workLoop, { timeout: 1000 }) } }
這一幀渲染還有空閒時間或沒超時 && 還存在一個執行單元
時去執行當前執行單元 並返回下一個執行單元。不知足上面條件後若還存在一個執行單元,會繼續下一幀的渲染。
function performUnitOfWork (fiber) { beginWork(fiber) // 開始 if (fiber.child) { return fiber.child } while (fiber) { completeUnitOfWork(fiber) // 結束 if (fiber.sibling) { return fiber.sibling } fiber = fiber.return } } function beginWork (fiber) { console.log('start: ', fiber.key) } function completeUnitOfWork (fiber) { console.log('end: ', fiber.key) }
遍歷執行單元流程以下
completeUnitOfWork
中收集completeUnitOfWork
中收集over
beginWork
中收集;收集完後返回其長子,回到第2步
循環遍歷beginWork
中收集;收集完後返回其長子,回到第2步
循環遍歷執行的收集順序以下
相似 二叉樹的先序遍歷
function beginWork (fiber) { console.log('start: ', fiber.key) // A1 B1 C1 C2 B2 }
完成的收集順序以下
相似 二叉樹的後序遍歷
function completeUnitOfWork (fiber) { console.log('end: ', fiber.key) // C1 C2 B1 B2 A1 }
相似於Git
的分支功能,從舊樹裏面fork一份,在新分支中進行添加、刪除、更新操做,而後再進行提交。
此階段測試例子 fiberCommit.html,核心代碼存放 fiberCommit.js。
先構造根fiber,stateNode
表示當前節點真實dom。
let container = document.getElementById('root') workInProgressRoot = { key: 'ROOT', // 節點實例(狀態): // 對於宿主組件,這裏保存宿主組件的實例, 例如DOM節點 // 對於類組件來講,這裏保存類組件的實例 // 對於函數組件說,這裏爲空,由於函數組件沒有實例 stateNode: container, props: { children: [A1] } } nextUnitOfWork = workInProgressRoot // 從RootFiber開始,到RootFiber結束
如上一個階段的beginWork
收集過程,對其進行完善。即將全部節點fiber化。
function beginWork(currentFiber) { // ++ if (!currentFiber.stateNode) { currentFiber.stateNode = document.createElement(currentFiber.type) // 建立真實DOM for (let key in currentFiber.props) { // 循環屬性賦賦值給真實DOM if (key !== 'children' && key !== 'key') currentFiber.stateNode.setAttribute(key, currentFiber.props[key]) } } let previousFiber currentFiber.props.children.forEach((child, index) => { let childFiber = { tag: 'HOST', type: child.type, key: child.key, props: child.props, return: currentFiber, // 當前節點的反作用類型,例如節點更新、刪除、移動 effectTag: 'PLACEMENT', // 和節點關係同樣,React 一樣使用鏈表來將全部有反作用的Fiber鏈接起來 nextEffect: null } if (index === 0) { currentFiber.child = childFiber } else { previousFiber.sibling = childFiber } previousFiber = childFiber }) }
其中effectTag
標識當前節點的反作用類型,第一次渲染爲新增PLACEMENT
,nextEffect
標識下一個有反作用的節點。
而後再完善completeUnitOfWork
(完成的收集)。
function completeUnitOfWork(currentFiber) { // ++ const returnFiber = currentFiber.return if (returnFiber) { if (!returnFiber.firstEffect) { returnFiber.firstEffect = currentFiber.firstEffect } if (currentFiber.lastEffect) { if (returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber.firstEffect } returnFiber.lastEffect = currentFiber.lastEffect } if (currentFiber.effectTag) { if (returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber } else { returnFiber.firstEffect = currentFiber } returnFiber.lastEffect = currentFiber } } }
目的是將完成的收集造成一個鏈表結構,配合commitRoot
階段。
當將全部的執行、完成
收集完成後(即將全部真實DOM、虛擬DOM、Fiber結合,其反作用(增刪改)造成一個鏈表結構),須要對其渲染到頁面中。
function workLoop (deadline) { // ... if (!nextUnitOfWork) { console.log('render end !') commitRoot() } else { requestIdleCallback(workLoop, { timeout: 1000 }) } }
找到第一個反作用完成的fiber節點,遞歸appendChild
到父元素上。
function commitRoot() { // ++ let fiber = workInProgressRoot.firstEffect while (fiber) { console.log('complete: ', fiber.key) // C1 C2 B1 B2 A1 commitWork(fiber) fiber = fiber.nextEffect } workInProgressRoot = null } function commitWork(currentFiber) { currentFiber.return.stateNode.appendChild(currentFiber.stateNode) }
以下爲上述的渲染效果和打印完成的收集順序
使用react-create-app
建立一個項目fiber
// src/index.js import React from 'react' let element = ( <div id="A1"> <div id="B1"> <div id="C1"></div> <div id="C2"></div> </div> <div id="B2"></div> </div> ) console.log(element);
npm i && npm start
以後打印結果以下
借用腳手架的babel編譯,咱們直接寫JSX語法
代碼。
在babel
編譯時將JSX
語法轉爲一個對象,而後調用react下的React.createElement
方法構建虛擬dom。咱們能夠以下模擬:
// core/react.js const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT'); function createElement(type, config, ...children) { return { type, // 元素類型 props: { ...config, children: children.map( child => typeof child === "object" ? child : { type: ELEMENT_TEXT, props: { text: child, children: [] } }) } } } let React = { createElement } export default React;
若是children
中有child是一個React.createElement
返回的React元素
,且是字符串的話,會被轉成文本節點。
準備以下結構
// src/index.js import React from 'react' import ReactDOM from 'react-dom' let style = { border: '3px solid green', margin: '5px' }; let element = ( <div id="A1" style={style}> A1 <div id="B1" style={style}> B1 <div id="C1" style={style}>C1</div> <div id="C2" style={style}>C2</div> </div> <div id="B2" style={style}>B2</div> </div> ) ReactDOM.render( element, document.getElementById('root') );
指望的渲染結果
此時須要定義一些列常量
// core/constants.js export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT'); // 文本元素 export const TAG_ROOT = Symbol.for('TAG_ROOT'); // 根Fiber export const TAG_HOST = Symbol.for('TAG_HOST'); // 原生的節點 span div p 函數組件 類組件 export const TAG_TEXT = Symbol.for('TAG_TEXT'); // 文本節點 export const PLACEMENT = Symbol.for('PLACEMENT'); // 插入節點
而後藉助上述的Reconciliation階段
,在react-dom.js
中先將虛擬dom構建成一根fiber樹
// core/react-dom.js import { TAG_ROOT } from './constants'; import { scheduleRoot } from './scheduler'; function render(element, container) { let rootFiber = { tag: TAG_ROOT, // 這是根Fiber stateNode: container, // 此Fiber對應的DOM節點 props: { children: [element] }, // 子元素就是要渲染的element } scheduleRoot(rootFiber); } export default { render }
而後交由scheduleRoot
進行調度
// core/scheduler.js // ...
代碼量較多,主要爲Reconciliation階段
和Commit階段
的組合代碼。
此過程代碼存放地址
其中對beginWork
進行細化
function beginWork(currentFiber) { if (currentFiber.tag === TAG_ROOT) { // 若是是根節點 updateHostRoot(currentFiber); } else if (currentFiber.tag === TAG_TEXT) { // 若是是原生文本節點 updateHostText(currentFiber); } else if (currentFiber.tag === TAG_HOST) { // 若是是原生DOM節點 updateHostComponent(currentFiber); } } function updateHostRoot(currentFiber) { // 若是是根節點 const newChildren = currentFiber.props.children; // 直接渲染子節點 reconcileChildren(currentFiber, newChildren); } function updateHostText(currentFiber) { if (!currentFiber.stateNode) { currentFiber.stateNode = createDOM(currentFiber); // 先建立真實的DOM節點 } } function updateHostComponent(currentFiber) { // 若是是原生DOM節點 if (!currentFiber.stateNode) { currentFiber.stateNode = createDOM(currentFiber); // 先建立真實的DOM節點 } const newChildren = currentFiber.props.children; reconcileChildren(currentFiber, newChildren); }
其中主要是針對不一樣類型節點賦值給stateNode
stateNode
下面將會對其進行擴展
stateNode
stateNode
爲nullreconcileChildren
也是對不一樣類型節點作處理。
再次鞏固下上一節的兩個階段及調度規則
從根節點開始渲染和調度主要有兩個階段
requestIdleCallback
能夠實現暫停effect list
,收集節點的增刪改render階段有兩個任務
effectlist
調度規則
其中使用到雙緩衝優化策略,下面將重點介紹
相似於圖形化領域繪製引擎經常使用的 雙緩衝技術。先將圖片繪製到一個緩衝區,再一次性傳遞給屏幕進行顯示,這樣能夠防止屏幕抖動,優化渲染性能。
操做頁面進而從新渲染,指望第一次更新爲變動A1/B1/C1/C二、新增B3
,第二次更新爲變動A1/B1/C1/C二、刪除B3
。
對應新增代碼以下
<!-- public/index.html --> <div id="root"></div> <button id="reRender1">reRender1</button> <button id="reRender2">reRender2</button> <button id="reRender3">reRender3</button>
爲兩個按鈕綁定事件,從新渲染頁面
// src/index.js let reRender2 = document.getElementById('reRender2'); reRender2.addEventListener('click', () => { let element2 = ( <div id="A1-new" style={style}> A1-new <div id="B1-new" style={style}> B1-new <div id="C1-new" style={style}>C1-new</div> <div id="C2-new" style={style}>C2-new</div> </div> <div id="B2" style={style}>B2</div> <div id="B3" style={style}>B3</div> </div> ) ReactDOM.render( element2, document.getElementById('root') ); }); let reRender3 = document.getElementById('reRender3'); reRender3.addEventListener('click', () => { let element3 = ( <div id="A1-new2" style={style}> A1-new2 <div id="B1-new2" style={style}> B1-new2 <div id="C1-new2" style={style}>C1-new2</div> <div id="C2-new2" style={style}>C2-new2</div> </div> <div id="B2" style={style}>B2</div> </div> ) ReactDOM.render( element3, document.getElementById('root') ); });
currentRoot
rooterFiber
的alternate
指向上一次渲染好的currentRoot
workInProgressRoot
指向currentRoot.alternate
,而後將當前的workInProgressRoot.alternate
指向上一次渲染好的currentRoot
<details>
<summary>變更代碼以下</summary>
import { setProps } from './utils'; import { ELEMENT_TEXT, TAG_ROOT, TAG_HOST, TAG_TEXT, PLACEMENT, DELETION, UPDATE } from './constants'; +let currentRoot = null;//當前的根Fiber let workInProgressRoot = null;//正在渲染中的根Fiber let nextUnitOfWork = null//下一個工做單元 +let deletions = [];//要刪除的fiber節點 export function scheduleRoot(rootFiber) { // {tag:TAG_ROOT,stateNode:container,props: { children: [element] }} + if (currentRoot && currentRoot.alternate) {//偶數次更新 + workInProgressRoot = currentRoot.alternate; + workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = workInProgressRoot.nextEffect = null; + workInProgressRoot.props = rootFiber.props; + workInProgressRoot.alternate = currentRoot; + } else if (currentRoot) {//奇數次更新 + rootFiber.alternate = currentRoot; + workInProgressRoot = rootFiber; + } else { + workInProgressRoot = rootFiber;//第一次渲染 + } nextUnitOfWork = workInProgressRoot; } function commitRoot() { + deletions.forEach(commitWork); let currentFiber = workInProgressRoot.firstEffect; while (currentFiber) { commitWork(currentFiber); currentFiber = currentFiber.nextEffect; } + deletions.length = 0;//先把要刪除的節點清空掉 + currentRoot = workInProgressRoot; workInProgressRoot = null; } function commitWork(currentFiber) { if (!currentFiber) { return; } let returnFiber = currentFiber.return;//先獲取父Fiber const domReturn = returnFiber.stateNode;//獲取父的DOM節點 if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode != null) {//若是是新增DOM節點 let nextFiber = currentFiber; domReturn.appendChild(nextFiber.stateNode); + } else if (currentFiber.effectTag === DELETION) {//若是是刪除則刪除並返回 + domReturn.removeChild(currentFiber.stateNode); + } else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode != null) {//若是是更新 + if (currentFiber.type === ELEMENT_TEXT) { + if (currentFiber.alternate.props.text != currentFiber.props.text) { + currentFiber.stateNode.textContent = currentFiber.props.text; + } + } else { + updateDOM(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props); + } + } currentFiber.effectTag = null; } function reconcileChildren(currentFiber, newChildren) { let newChildIndex = 0;//新虛擬DOM數組中的索引 + let oldFiber = currentFiber.alternate && currentFiber.alternate.child;//父Fiber中的第一個子Fiber + let prevSibling; + while (newChildIndex < newChildren.length || oldFiber) { + const newChild = newChildren[newChildIndex]; + let newFiber; + const sameType = oldFiber && newChild && newChild.type === oldFiber.type;//新舊都有,而且元素類型同樣 + let tag; + if (newChild && newChild.type === ELEMENT_TEXT) { + tag = TAG_TEXT;//文本 + } else if (newChild && typeof newChild.type === 'string') { + tag = TAG_HOST;//原生DOM組件 + } + if (sameType) { + if (oldFiber.alternate) { + newFiber = oldFiber.alternate; + newFiber.props = newChild.props; + newFiber.alternate = oldFiber; + newFiber.effectTag = UPDATE; + newFiber.nextEffect = null; + } else { + newFiber = { + tag:oldFiber.tag,//標記Fiber類型,例如是函數組件或者原生組件 + type: oldFiber.type,//具體的元素類型 + props: newChild.props,//新的屬性對象 + stateNode: oldFiber.stateNode,//原生組件的話就存放DOM節點,類組件的話是類組件實例,函數組件的話爲空,由於沒有實例 + return: currentFiber,//父Fiber + alternate: oldFiber,//上一個Fiber 指向舊樹中的節點 + effectTag: UPDATE,//反作用標識 + nextEffect: null //React 一樣使用鏈表來將全部有反作用的Fiber鏈接起來 + } # + } + } else { + if (newChild) {//類型不同,建立新的Fiber,舊的不復用了 + newFiber = { + tag,//原生DOM組件 + type: newChild.type,//具體的元素類型 + props: newChild.props,//新的屬性對象 + stateNode: null,//stateNode確定是空的 + return: currentFiber,//父Fiber + effectTag: PLACEMENT//反作用標識 + } + } + if (oldFiber) { + oldFiber.effectTag = DELETION; + deletions.push(oldFiber); + } + } + if (oldFiber) { //比較完一個元素了,老Fiber向後移動1位 + oldFiber = oldFiber.sibling; + } if (newFiber) { if (newChildIndex === 0) { currentFiber.child = newFiber;//第一個子節點掛到父節點的child屬性上 } else { prevSibling.sibling = newFiber; } prevSibling = newFiber;//而後newFiber變成了上一個哥哥了 } prevSibling = newFiber;//而後newFiber變成了上一個哥哥了 newChildIndex++; } }
</details>
構建一個計數器
class ClassCounter extends React.Component { constructor(props) { super(props); this.state = { number: 0 }; } onClick = () => { this.setState(state => ({ number: state.number + 1 })); } render() { return ( <div id="counter"> <span>{this.state.number}</span> <button onClick={this.onClick}>加1</button> </div > ) } } ReactDOM.render( <ClassCounter />, document.getElementById('root') );
import { ELEMENT_TEXT } from './constants'; +import { Update, UpdateQueue } from './updateQueue'; +import { scheduleRoot } from './scheduler'; // ... +class Component { + constructor(props) { + this.props = props; + this.updateQueue = new UpdateQueue(); + } + setState(payload) { + this.internalFiber.updateQueue.enqueueUpdate(new Update(payload)); + scheduleRoot(); + } +} +Component.prototype.isReactComponent = true; let React = { createElement, + Component } export default React;
此過程在模擬setState過程已經說明
export class Update { constructor(payload) { this.payload = payload; } } // 數據結構是一個單鏈表 export class UpdateQueue { constructor() { this.firstUpdate = null; this.lastUpdate = null; } enqueueUpdate(update) { if (this.lastUpdate === null) { this.firstUpdate = this.lastUpdate = update; } else { this.lastUpdate.nextUpdate = update; this.lastUpdate = update; } } forceUpdate(state) { let currentUpdate = this.firstUpdate; while (currentUpdate) { let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(state) : currentUpdate.payload; state = { ...state, ...nextState }; currentUpdate = currentUpdate.nextUpdate; } this.firstUpdate = this.lastUpdate = null; return state; } }
須要在src/scheduler.js
文件中作以下修改
function beginWork(currentFiber) { if (currentFiber.tag === TAG_ROOT) {//若是是根節點 updateHostRoot(currentFiber); } else if (currentFiber.tag === TAG_TEXT) {//若是是原生文本節點 updateHostText(currentFiber); } else if (currentFiber.tag === TAG_HOST) {//若是是原生DOM節點 updateHostComponent(currentFiber); + } else if (currentFiber.tag === TAG_CLASS) {//若是是類組件 + updateClassComponent(currentFiber) + } } +function updateClassComponent(currentFiber) { + if (currentFiber.stateNode === null) { + currentFiber.stateNode = new currentFiber.type(currentFiber.props); + currentFiber.stateNode.internalFiber = currentFiber; + currentFiber.updateQueue = new UpdateQueue(); + } + currentFiber.stateNode.state = currentFiber.updateQueue.forceUpdate(currentFiber.stateNode.state); + const newChildren = [currentFiber.stateNode.render()]; + reconcileChildren(currentFiber, newChildren); +}
若是是類組件,則new這個類將實例緩存到currentFiber.stateNode
,再將實例的render()方法執行結果
遞歸調度reconcileChildren
同類組件同樣,在各對應地方新增一份else..if
便可
function FunctionCounter() { return ( <h1> Count:0 </h1> ) } ReactDOM.render( <FunctionCounter />, document.getElementById('root') );
function beginWork(currentFiber) { if (currentFiber.tag === TAG_ROOT) {//若是是根節點 updateHostRoot(currentFiber); } else if (currentFiber.tag === TAG_TEXT) {//若是是原生文本節點 updateHostText(currentFiber); } else if (currentFiber.tag === TAG_HOST) {//若是是原生DOM節點 updateHostComponent(currentFiber); } else if (currentFiber.tag === TAG_CLASS) {//若是是類組件 updateClassComponent(currentFiber) + } else if (currentFiber.tag === TAG_FUNCTION) {//若是是函數組件 + updateFunctionComponent(currentFiber); + } } +function updateFunctionComponent(currentFiber) { + const newChildren = [currentFiber.type(currentFiber.props)]; + reconcileChildren(currentFiber, newChildren); +}
與類組件不同的是函數式組件沒有實例,故直接將函數執行的返回值遞歸調度。
使用以下
// src/index.js import React from 'react' import ReactDOM from 'react-dom' // import React from '../../../packages/fiber/core/react'; // import ReactDOM from '../../../packages/fiber/core/react-dom'; function reducer(state, action) { switch (action.type) { case 'ADD': return { count: state.count + 1 }; default: return state; } } function FunctionCounter() { const [numberState, setNumberState] = React.useState({ number: 0 }); const [countState, dispatch] = React.useReducer(reducer, { count: 0 }); return ( <div> <h1 onClick={() => setNumberState(state => ({ number: state.number + 1 }))}> Count: {numberState.number} </h1 > <hr /> <h1 onClick={() => dispatch({ type: 'ADD' })}> Count: {countState.count} </h1 > </div> ) } ReactDOM.render( <FunctionCounter />, document.getElementById('root') );
須要react提供useState/useReducer
兩個Hook
// core/react.js +import { scheduleRoot,useState,useReducer} from './scheduler'; let React = { createElement, Component, + useState, + useReducer }
實現過程以下
// core/scheduler.js +import { UpdateQueue, Update } from './updateQueue'; +let workInProgressFiber = null; //正在工做中的fiber +let hookIndex = 0; //hook索引 function updateFunctionComponent(currentFiber) { + workInProgressFiber = currentFiber; + hookIndex = 0; + workInProgressFiber.hooks = []; const newChildren = [currentFiber.type(currentFiber.props)]; reconcileChildren(currentFiber, newChildren); } +export function useReducer(reducer, initialValue) { + let oldHook = + workInProgressFiber.alternate && + workInProgressFiber.alternate.hooks && + workInProgressFiber.alternate.hooks[hookIndex]; + let newHook = oldHook; + if (oldHook) { + oldHook.state = oldHook.updateQueue.forceUpdate(oldHook.state); + } else { + newHook = { + state: initialValue, + updateQueue: new UpdateQueue() + }; + } + const dispatch = action => { + newHook.updateQueue.enqueueUpdate( + new Update(reducer ? reducer(newHook.state, action) : action) + ); + scheduleRoot(); + } + workInProgressFiber.hooks[hookIndex++] = newHook; + return [newHook.state, dispatch]; +} +export function useState(initState) { + return useReducer(null, initState) +}
看完上面很是乾的簡易實現,再來回顧一開始的幾個問題:
[x] React15存在哪些痛點?Fiber是什麼?React16爲何須要引入Fiber?
[x] 如何實現React16下的虛擬DOM?
[x] 如何實現Fiber的數據結構和遍歷算法?
[x] 如何實現Fiber架構下可中斷和可恢復的的任務調度?
[x] 如何實現Fiber架構下的組件渲染和反作用收集提交?
[x] 如何實現Fiber中的調和和雙緩衝優化策略?
但仍然還有後面幾個問題沒有解答,下篇文章繼續探索...