「React 16」爲 Luy 實現 React Fiber 架構

前言

Facebook 的研發能力真是驚人, Fiber 架構給 React 帶來了新視野的同時,將調度一詞介紹給了前端,然而這個架構實在很差懂,比起之前的 Vdom 樹,新的 Fiber 樹就麻煩太多。前端

能夠說,React 16 和 React 15 已是技巧上的分水嶺,可是得益於 React 16 的 Fiber 架構,使得 React 即便在沒有開啓異步的狀況下,性能依舊是獲得了提升。node

通過兩個星期的痛苦研究,終於將 React 16 的渲染脈絡摸得比較清晰,能夠寫文章來記錄、回顧一下。react

若是你已經稍微理解了 Fiber 架構,能夠直接看代碼:倉庫地址git

什麼是 React Fiber ?

React Fiber 並非所謂的纖程(微線程、協程),而是一種基於瀏覽器的單線程調度算法,背後的支持 API 是大名鼎鼎的: requestIdleCallback ,獲得了這個 API 的支持,咱們即可以將 React 中最耗時的部分放入其中。github

回顧 React 歷年來的算法都知道,reconcilation 算法其實是一個大遞歸,大遞歸一旦進行,想要中斷仍是比較很差操做的,加上頭大尾大的 React 15 代碼已經膨脹到了難以想象的地步,在重重壓力之下,React 使用了大循環來代替以前的大遞歸,雖然代碼變得比遞歸難懂了幾個梯度,可是實際上,代碼量比原來少了很是多(開發版本 3W 行壓縮到了 1.3W 行)算法

那問題就來了,什麼是 Fiber :一種將 recocilation (遞歸 diff ),拆分紅無數個小任務的算法;它隨時可以中止,恢復。中止恢復的時機取決於當前的一幀( 16ms )內,還有沒有足夠的時間容許計算。npm

React 16 先後的大小圖

React 異步渲染流程圖

  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 的不連續性,極大的影響體驗。瀏覽器

遞歸變成了循環

以簡單的組件爲例子:bash

  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

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

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

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

  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 秒,就會更新,咱們簡單的更新就到此結束了


再看如下調用棧,咱們的 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 做者,我等,一塊兒來學習吧!

相關文章
相關標籤/搜索