為 Luy 實現 React Fiber 架構

Foveluy發表於2018-05-21

前言

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 16 前後的大小圖

React 非同步渲染流程圖

為 Luy 實現 React Fiber 架構

  1. 使用者呼叫 ReactDOM.render 方法,傳入例如<App />元件,React 開始運作<App />
  2. <App /> 在內部會被轉換成 RootFiber 節點,一個特殊的節點,並記錄在一個全域性變數中,TopTree
  3. 拿到 <App />RootFiber ,首先建立一個 <App /> 對應的 Fiber ,然後加上 Fiber 資訊,以便之後回溯。隨後,賦值給之前的全域性變數 TopTree
  4. 使用 requestIdleCallback 重複第三個步驟,直到迴圈到樹的所有節點
  5. 最後完成了 diff 階段,一次性將變化更新到真實 DOM 中,以防止 UI 展示的不連續性

其中,重點就是 34 階段,這兩個階段將建立真實 DOM 和元件渲染 ( render )拆分為無數的小碎塊,使用 requestIdleCallback 連續進行。在 React 15 的時候,渲染、建立、插入、刪除等操作是最費時的,在 React 16 中將渲染、建立抽離出來分片,這樣效能就得到了極大的提升。

那為什麼更新到真實 DOM 中不能拆分呢?理論上來說,是可以拆分的,但是這會造成 UI 的不連續性,極大的影響體驗。

遞迴變成了迴圈

為 Luy 實現 React Fiber 架構

以簡單的元件為例子:

  1. 從頂端的 div#root 向下走,先走左子樹
  2. div 有兩個孩子 span ,繼續走左邊的
  3. 來到 span ,之下只有一個 hello ,到此,不再繼續往下,而是往上回到 span
  4. 因為 span 有一個兄弟,因此往兄弟 span 走去
  5. 兄弟 span 有孩子 luy ,到此,不繼續往下,而是回到 luy 的老爹 span
  6. luy 的老爹 span 右邊沒有兄弟了,因此回到其老爹 div
  7. 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 )。 childsibling 類似,代表這個 Fiber 的子 Fiber 和兄弟 Fiber
  • stateNode 這個屬性比較特殊,用於記錄當前 Fiber 所對應的真實 DOM 節點 或者 當前虛擬元件的例項,這麼做的原因第一是為了實現 Ref ,第二是為了實現 DOM 的跟蹤
  • tag 屬性在新版的 React 中一共有 14 種值,分別代表了不同的 JSX 型別。
  • effectTageffects 這兩個屬性為的是記錄每個節點 Diff 後需要變更的狀態,比如刪除,移動,插入,替換,更新等...

alternate 屬性我想拿出來單獨說一下,這個屬性是 Fiber 架構新加入的屬性。我們都知道,VDOM 演算法是在更新的時候生成一顆新的 VDOM 樹,去和舊的進行對比。在 Fiber 架構中,當我們呼叫 ReactDOM.render 或者 setState 之後,會生成一顆樹叫做:work-in-progress tree,這一顆樹就是我們所謂的新樹用來與我們的舊樹進行對比,新的樹和舊的樹的 Fiber 是完全不一樣的,此時,我們就需要 alternate 屬性去連結新樹和舊樹。

司徒正美的研究中,一個 Fiber 和它的 alternate 屬性構成了一個聯嬰體,他們有共同的 tagtypestateNode 屬性,這些屬性在錯誤邊界自爆時,用於恢復當前節點。

開始寫程式碼: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
  • 內部有 staterefsupdaterupdater 用於收集 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 來記錄我們所有的更新操作,每當 renderscheduleWork (setState) 觸發時,我們都會往 updateQueuepush 一個狀態,然後,進而呼叫大名鼎鼎的 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 的同學一定對這兩個函式並不陌生,這兩個函式其實做的就是所謂的非同步排程。

requestIdleCallback 用法

performWork 函式主要做了兩件事,第一件事就是拿到 deadline 進入我們之前所謂的大迴圈,也就是正式進入處理新舊 FiberDiff 階段,這個階段比較的奇妙,我們叫他 workLoop 階段。workLoop 會一次處理 1 個或者多個 Fiber ,具體處理多少個,要看每一幀具體還剩下多少時間,如果一個 Fiber 消耗太多時間,那麼就會等到下一幀再處理下一個 Fiber ,如此迴圈,遍歷整個 VDOM 樹。

在這裡我們注意到,如果一個 Fiber 消耗太多時間,可能會導致一幀時間的逾期,不過其實沒什麼問題啦,也僅僅是一幀逾期而已,對於我們視覺上並沒有多大的影響。

workLoop 函式主要是三部曲:

  1. createWorkInProgress 這個函式會構建一顆樹的頂端,賦值給全域性變數 nextUnitOfWork ,通過迭代的方式,不斷更新 nextUnitOfWork 直到遍歷完所有樹的節點。
  2. performUnitOfWork 函式是第二步,不斷的檢測當前幀是否還剩餘時間,進行 WorkInProgress tree 的迭代
  3. 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

然後查詢當前節點的父親節點,是否有兄弟,有就返回,當成下一個處理的節點,如果沒有,就繼續回溯。

整個過程用圖來表示,就是:

為 Luy 實現 React Fiber 架構

在討論第三部之前,我們仍然有兩個迷惑的地方:

  1. beginWork 是如何建立孩子的
  2. 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 例項化元件,然後將當前的 propsstate 賦值給 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 化,這麼做能夠使得 reactrender 函式返回陣列
  • currentFiber 是新的 workInProgress 上的一個節點,是屬於新的 VDOM 樹 ,而此時,我們必須要找到舊的 VDOM 樹來進行比對。那麼在這裡, Alternate 屬性就起到了關鍵性作用,這個屬性連結了舊的 VDOM ,使得我們能夠獲取原來的 VDOM
  • 接下來我們進行對比,如果新的節點的 type 與原來的相同,那麼我們將新建一個 Fiber ,標記這個 FiberUPDATE
  • 如果新的節點的 type 與原來的不相同,那我們使用 PALCEMENT 來標記他
  • 如果舊的節點數量比新的節點少,那就證明,我們要刪除舊的節點,我們把舊節點標記為 DELETION ,並構建一個 effect list 記錄下來
  • 當前遍歷的是元件的第一個孩子,那麼我們將他記錄在 currentFiberchild 欄位中
  • 當遍歷的不是第一個孩子,我們將 新建的 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
  }
}
複製程式碼

這個函式做了兩件事,第一件事情就是收集當前 currentFibereffectTag ,將其 append 到父 Fibereffectlist 中去,通過迴圈一層一層往上,最終到達頂端 currentFiber.return === void 666 的時候,證明我們到達了 root ,此時我們已經把所有的 effect 收集到了頂端的 currentFiber.effect 上,並把它賦值給 pendingCommit ,進入 commitAllWork 階段。

第三步:commitAllWork

終於,我們已經通過不斷不斷的呼叫 requestIdleCallback 和 大迴圈,將我們的所有變更都找出來放在了 workInProgress tree 裡,我們接下來就要做最後一步:將所有的變更一次性的變更到真實 DOM 中,注意,這個階段裡我們不再執行建立 DOMrender ,因此,雖然我們一次性變更所有的 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 秒,就會更新,我們簡單的更新就到此結束了

為 Luy 實現 React Fiber 架構
為 Luy 實現 React Fiber 架構

再看以下呼叫棧,我們的 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 作者,我等,一起來學習吧!

相關文章