由淺入深React的Fiber架構

fiber-cover

目的是初識fiber並實現react基礎功能,請帶着下面幾個問題去閱讀此文。html

  • React15存在哪些痛點?Fiber是什麼?React16爲何須要引入Fiber?
  • 如何實現React16下的虛擬DOM?
  • 如何實現Fiber的數據結構和遍歷算法?
  • 如何實現Fiber架構下可中斷和可恢復的的任務調度?
    • 如何指定數量更新?如何批量更新?
  • 如何實現Fiber架構下的組件渲染和反作用收集提交?
  • 如何實現Fiber中的調和和雙緩衝優化策略?
  • 如何實現useReducer和useState等Hooks?
  • 如何實現expirationTime 任務的優先級 任務調度 超時時間的處理?
  • 如何實現reconcile domdiff的優化key處理?
  • 如何實現合成事件 SyntheticEvent?
  • 如何實現ref useEffect?

此文章首發於@careteen/react,轉載請註明來源便可。倉庫存放全部實現代碼和示例,感興趣的能夠fork調試。node

目錄

React15的調度策略

JavaScript就像一條單行道。react

JavaScript是單線程運行的。在瀏覽器環境中,他須要負責頁面的JavaScript解析和執行、繪製、事件處理、靜態資源加載和處理。並且只能一個任務一個任務的執行,若是其中某個任務耗時很長,那後面的任務則執行不了,在瀏覽器端則會呈現卡死的狀態。git

browser-render

React15的渲染和diff會遞歸比對VirtualDOM樹找出有增刪改的節點,而後同步更新他們,整個過程是一鼓作氣的。那麼若是頁面節點數量很是龐大,React會一直霸佔着瀏覽器資源,一則會致使用戶觸發的事件得不到響應,二則會致使掉幀,用戶會感知到這些卡頓。github

因此針對上述痛點,咱們指望將找出有增刪改的節點,而後同步更新他們這個過程分解成兩個獨立的部分,或者經過某種方式能讓整個過程可中斷可恢復的執行,相似於多任務操做系統的單處理器調度。算法

爲了實現進程的併發,操做系統會按照必定的調度策略,將CPU的執行權分配給多個進程,多個進程都有被執行的機會,讓他們交替執行,造成一種同時在運行的假象。由於CPU速度太快,人類根本感受不到。實際上在單核的物理環境下同時只有一個程序在運行。chrome

瀏覽器任務調度策略和渲染流程

pubg-stuck

玩遊戲時須要流暢的刷新率,也就是至少60赫茲。否則遊戲體驗極差。npm

那麼一個幀包含什麼呢?api

a-frame

一幀平均是16.66ms,主要分爲如下幾個部分數組

  • 腳本執行
  • 樣式計算
  • 佈局
  • 重繪
  • 合成

在樣式計算以前會執行腳本計算中使用到requestAnimationFrame的callback

若是你還不瞭解requestAnimationFrame,前往mdn查看實現的進度條示例。

在合成後還存在一個空閒階段,即合成及以前的全部步驟耗時若不足16.66ms,剩下的時間瀏覽器爲咱們提供了requestIdleCallback進行調用,對其充分利用。

requestIdleCallback目前只支持chrome,須要polyfill

requestIdleCallback-api

大體流程以下:

requestIdleCallback-flow

requestIdleCallback示例

requestIdleCallback使開發者可以在主事件循環上執行後臺和低優先級工做,而不會影響延遲關鍵事件,如動畫和輸入響應。

鏈表的優點

因爲數組的大小是固定的,從數組的起點或者中間插入或移除項的成本很高。鏈表相對於傳統的數組的優點在於添加或移除元素的時候不須要移動其餘元素,須要添加和移除不少元素時,最好的選擇是鏈表,而非數組。 鏈表在React的Fiber架構和Hooks實現發揮很大的做用。

更多關於鏈表的實現和使用

模擬setState

setState

如上可使用鏈表實現相似於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()參數可對象可函數。

Fiber架構

Fiber出現前怎麼作

React15及以前,React會遞歸比對VirtualDOM樹,找出須要變更的節點,而後同步更新它們。這個過程React稱爲Reconciliation(協調)

Reconciliation期間,React會一直佔用着瀏覽器資源,一則會致使用戶觸發的事件得不到響應, 二則會致使掉幀,用戶可能會感受到卡頓。下面將模擬其遍歷過程。

React15的DOMDIFF

dom-tree

將上圖節點結構映射成虛擬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

Fiber是什麼

  • Fiber是一個執行單元
  • Fiber也是一種數據結構

Fiber是一個執行單元

上面瀏覽器任務調度過程提到在頁面合成後還存在一個空閒階段requestIdleCallback

下圖爲React結合空閒階段的調度過程

fiber-flow

這是一種合做式調度,須要程序和瀏覽器互相信任。瀏覽器做爲領導者,會分配執行時間片(即requestIdleCallback)給程序去選擇調用,程序須要按照約定在這個時間內執行完畢,並將控制權交還瀏覽器。

Fiber是一個執行單元,每次執行完一個執行單元,React就會檢查如今還剩多少時間,若是沒有時間就將控制權交還瀏覽器;而後繼續進行下一幀的渲染。

Fiber也是一種數據結構

fiber-structure

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
複製代碼

Fiber小結

  • 咱們能夠經過某些調度策略合理分配CPU資源,從而提升用戶的響應速度
  • 經過Fiber架構,讓本身的Reconciliation過程變得可被中斷,適時地讓出CPU執行權,可讓瀏覽器及時地響應用戶的交互

Fiber執行階段

每次渲染有兩個階段:Reconciliation(協調/render)階段和Commit(提交)階段

  • 協調/render階段:能夠認爲是Diff階段,這個階段能夠被中斷,這個階段會找出全部節點變動,例如節點增刪改等等,這些變動在React中稱爲Effect(反作用)。
  • 提交階段:將上一個階段計算出來的須要處理的反作用一次性執行。這個階段不能中斷,必須同步一次性執行完。

Reconciliation階段

下面將上面講到的幾個知識點串聯起來使用。

此階段測試例子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)
}

複製代碼

fiber-travers
遍歷執行單元流程以下

  1. 從根節點開始遍歷
  2. 若是沒有長子,則標識當前節點遍歷完成。completeUnitOfWork中收集
  3. 若是沒有相鄰兄弟,則返回父節點標識父節點遍歷完成。completeUnitOfWork中收集
  4. 若是沒有父節點,標識全部遍歷完成。over
  5. 若是有長子,則遍歷;beginWork中收集;收集完後返回其長子,回到第2步循環遍歷
  6. 若是有相鄰兄弟,則遍歷;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
}
複製代碼

Commit階段

相似於Git的分支功能,從舊樹裏面fork一份,在新分支中進行添加、刪除、更新操做,而後再進行提交。

git-branch

此階段測試例子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標識當前節點的反作用類型,第一次渲染爲新增PLACEMENTnextEffect標識下一個有反作用的節點。

而後再完善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)
}
複製代碼

以下爲上述的渲染效果和打印完成的收集順序

fiber-commit-result

React使用Fiber

準備環境

使用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以後打印結果以下

react-vdom

借用腳手架的babel編譯,咱們直接寫JSX語法代碼。

實現createElement方法

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')
);
複製代碼

指望的渲染結果

react-target-render

此時須要定義一些列常量

// 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

  • 原生DOM節點/原生文本節點:直接建立真是DOM節點賦值給stateNode
  • 下面將會對其進行擴展
    • 類組件:須要new一個組件實例掛載到stateNode
    • 函數式組件:沒有實例,stateNode爲null

reconcileChildren也是對不一樣類型節點作處理。

渲染小結

再次鞏固下上一節的兩個階段及調度規則

  • 從根節點開始渲染和調度主要有兩個階段
    • render階段:此階段比較花時間,咱們能夠對任務進行拆分,拆分的維度虛擬DOM。此階段藉助requestIdleCallback能夠實現暫停
    • diff階段:對比新舊的虛擬DOM,進行增量、更新、建立
  • render階段成果是effect list,收集節點的增刪改
  • render階段有兩個任務
    • 根據虛擬DOM生成fiber樹
    • 收集effectlist
  • commit階段,進行DOM更新建立階段,此階段不能暫停,要一鼓作氣

調度規則

  • 遍歷鏈規則:先長子、後兄弟、再二叔
  • 完成鏈規則:全部兒子所有完成,則本身完成
  • effect鏈:同完成鏈

實現元素的更新

其中使用到雙緩衝優化策略,下面將重點介紹

相似於圖形化領域繪製引擎經常使用的雙緩衝技術。先將圖片繪製到一個緩衝區,再一次性傳遞給屏幕進行顯示,這樣能夠防止屏幕抖動,優化渲染性能。

操做頁面進而從新渲染,指望第一次更新爲變動A1/B1/C1/C二、新增B3,第二次更新爲變動A1/B1/C1/C二、刪除B3

react-target-reRender

對應新增代碼以下

<!-- 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')
  );
});
複製代碼

雙緩衝更新策略

fiber-update-process-1

fiber-update-process-2

  • 將每次渲染完後的fiber樹賦值給currentRoot
  • 第一次更新時將rooterFiberalternate指向上一次渲染好的currentRoot
  • 第二次以後的更新將workInProgressRoot指向currentRoot.alternate,而後將當前的workInProgressRoot.alternate指向上一次渲染好的currentRoot
  • ...
  • 進而達到複用fiber對象樹
變更代碼以下
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++;
  }
}
複製代碼

實現類組件

fiber-classCom-process-1
構建一個計數器

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);
+}
複製代碼

與類組件不同的是函數式組件沒有實例,故直接將函數執行的返回值遞歸調度。

實現Hooks

使用以下

// 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)
+}
複製代碼

總結

看完上面很是乾的簡易實現,再來回顧一開始的幾個問題:

  • React15存在哪些痛點?Fiber是什麼?React16爲何須要引入Fiber?
    • 渲染和diff階段一鼓作氣,節點樹龐大時會致使頁面卡死
    • Fiber並不神祕,只是將Virtual-DOM轉變爲一種鏈表結構
    • 鏈表結構配合requestIdleCallback可實現可中斷可恢復的調度機制
  • 如何實現React16下的虛擬DOM?
    • 同React15
  • 如何實現Fiber的數據結構和遍歷算法?
  • 如何實現Fiber架構下可中斷和可恢復的的任務調度?
    • 如何指定數量更新?如何批量更新?
    • 藉助requestIdleCallback交由瀏覽器在一幀渲染後的給出的空閒時間內實現指定數量跟新,批量更新能夠直接跳過這個API,按以前的方式
  • 如何實現Fiber架構下的組件渲染和反作用收集提交?
    • 執行的收集順序相似於二叉樹的先序遍歷
    • 完成的收集順序相似於二叉樹的後序遍歷
  • 如何實現Fiber中的調和和雙緩衝優化策略?
    • 在Fiber結構中增長一個alternate字段標識上一次渲染好的Fiber樹,下次渲染時可複用
  • 如何實現useReducer和useState等Hooks?
  • 如何實現expirationTime 任務的優先級 任務調度 超時時間的處理?
  • 如何實現reconcile domdiff的優化key處理?
  • 如何實現合成事件 SyntheticEvent?
  • 如何實現ref useEffect?

但仍然還有後面幾個問題沒有解答,下篇文章繼續探索...

參考資料

相關文章
相關標籤/搜索