日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

詳解React?Fiber架構原理_React

作者:xiangzhihong8 ? 更新時間: 2022-10-01 編程語言

一、概述

在 React 16 之前,VirtualDOM 的更新采用的是Stack架構實現的,也就是循環遞歸方式。不過,這種對比方式有明顯的缺陷,就是一旦任務開始進行就無法中斷,如果遇到應用中組件數量比較龐大,那么VirtualDOM 的層級就會比較深,帶來的結果就是主線程被長期占用,進而阻塞渲染、造成卡頓現象。

為了避免出現卡頓等問題,我們必須保障在執行更新操作時計算時不能超過16ms,如果超過16ms,就需要先暫停,讓給瀏覽器進行渲染,后續再繼續執行更新計算。而Fiber架構就是為了支持“可中斷渲染”而創建的。

在React中,Fiber使用了一種新的數據結構fiber tree,它可以把虛擬dom tree轉換成一個鏈表,然后再執行遍歷操作,而鏈表在執行遍歷操作時是支持斷點重啟的,示意圖如下。

二、Fiber架構

2.1 執行單元

官方介紹中,Fiber 被理解為是一種數據結構,但是我們也可以將它理解為是一個執行單元。

Fiber 可以理解為一個執行單元,每次執行完一個執行單元,React Fiber就會檢查還剩多少時間,如果沒有時間則將控制權讓出去,然后由瀏覽器執行渲染操作。React Fiber 與瀏覽器的交互流程如下圖。

可以看到,React 首先向瀏覽器請求調度,瀏覽器在執行完一幀后如果還有空閑時間,會去判斷是否存在待執行任務,不存在就直接將控制權交給瀏覽器;如果存在就會執行對應的任務,執行完一個新的任務單元之后會繼續判斷是否還有時間,有時間且有待執行任務則會繼續執行下一個任務,否則將控制權交給瀏覽器執行渲染,這個流程是循環進行的。

所以,我們可以將Fiber 理解為一個執行單元,并且這個執行單元必須是一次完成的,不能出現暫停。并且,這個小的執行單元在執行完后計算之后,可以移交控制權給瀏覽器去響應用戶,從而提升了渲染的效率。

2.2 數據結構

在官方的文檔中,Fiber 被解釋為是一種數據結構,即鏈表結構。在鏈表結構中,每個 Virtual DOM 都可以表示為一個 fiber,如下圖所示。

通常,一個 fiber包括了 child(第一個子節點)、sibling(兄弟節點)、return(父節點)等屬性,React Fiber 機制的實現,就是依賴于上面的數據結構。

2.3 Fiber鏈表結構

通過介紹,我們知道Fiber使用的是鏈表結構,準確的說是單鏈表樹結構,詳見ReactFiber.js源碼。為了放便理解 Fiber 的遍歷過程,下面我們就看下Fiber鏈表結構。

在上面的例子中,每一個單元都包含了payload(數據)和nextUpdate(指向下一個單元的指針)兩個元素,定義結構如下:

class Update {
  constructor(payload, nextUpdate) {
    this.payload = payload          //payload 數據
    this.nextUpdate = nextUpdate    //指向下一個節點的指針
  }
}

接下來定義一個隊列,把每個單元串聯起來。為此,我們需要定義兩個指針:頭指針firstUpdate和尾指針lastUpdate,作用是指向第一個單元和最后一個單元,然后再加入baseState屬性存儲React中的state狀態。

class UpdateQueue {
  constructor() {
    this.baseState = null  // state
    this.firstUpdate = null // 第一個更新
    this.lastUpdate = null // 最后一個更新
  }
}

接下來,再定義兩個方法:用于插入節點單元的enqueueUpdate()和用于更新隊列的forceUpdate()。并且,插入節點單元時需要考慮是否已經存在節點,如果不存在直接將firstUpdate、lastUpdate指向此節點即可。更新隊列是遍歷這個鏈表,根據payload中的內容去更新state的值

class UpdateQueue {
  //.....
  
  enqueueUpdate(update) {
    // 當前鏈表是空鏈表
    if (!this.firstUpdate) {
      this.firstUpdate = this.lastUpdate = update
    } else {
      // 當前鏈表不為空
      this.lastUpdate.nextUpdate = update
      this.lastUpdate = update
    }
  }
  
  // 獲取state,然后遍歷這個鏈表,進行更新
  forceUpdate() {
    let currentState = this.baseState || {}
    let currentUpdate = this.firstUpdate
    while (currentUpdate) {
      // 判斷是函數還是對象,是函數則需要執行,是對象則直接返回
      let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
      currentState = { ...currentState, ...nextState }
      currentUpdate = currentUpdate.nextUpdate
    }
    // 更新完成后清空鏈表
    this.firstUpdate = this.lastUpdate = null
    this.baseState = currentState
    return currentState
  }
}

最后,我們寫一個測試的用例:實例化一個隊列,向其中加入很多節點,再更新這個隊列。

let queue = new UpdateQueue()
queue.enqueueUpdate(new Update({ name: 'www' }))
queue.enqueueUpdate(new Update({ age: 10 }))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.forceUpdate()
console.log(queue.baseState);       //輸出{ name:'www',age:12 }

2.4 Fiber節點

Fiber 框架的拆分單位是 fiber(fiber tree上的一個節點),實際上拆分的節點就是虛擬DOM的節點,我們需要根據虛擬dom去生成 fiber tree。 Fiber節點的數據結構如下:

{
    type: any,   //對于類組件,它指向構造函數;對于DOM元素,它指定HTML tag
    key: null | string,  //唯一標識符
    stateNode: any,  //保存對組件的類實例,DOM節點或與fiber節點關聯的其他React元素類型的引用
    child: Fiber | null, //大兒子
    sibling: Fiber | null, //下一個兄弟
    return: Fiber | null, //父節點
    tag: WorkTag, //定義fiber操作的類型, 詳見https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
    nextEffect: Fiber | null, //指向下一個節點的指針
    updateQueue: mixed, //用于狀態更新,回調函數,DOM更新的隊列
    memoizedState: any, //用于創建輸出的fiber狀態
    pendingProps: any, //已從React元素中的新數據更新,并且需要應用于子組件或DOM元素的props
    memoizedProps: any, //在前一次渲染期間用于創建輸出的props
    // ……     
}

最終, 所有的fiber 節點通過以下屬性:child,sibling 和 return來構成一個樹鏈表。
其他的屬性還有memoizedState(創建輸出的 fiber 的狀態)、pendingProps(將要改變的 props )、memoizedProps(上次渲染創建輸出的 props )、pendingWorkPriority(定義 fiber 工作優先級)等等就不在過多的介紹了。

2.5 API

2.5.1 requestAnimationFrame

requestAnimationFrame是瀏覽器提供的繪制動畫的 API ,它要求瀏覽器在下次重繪之前(即下一幀)調用指定的回調函數以更新動畫。

例如,使用requestAnimationFrame實現正方形的寬度加1px,直到寬度達到100px停止,代碼如下。

<body>
  <div id="div" class="progress-bar "></div>
  <button id="start">開始動畫</button>
</body>
<script>
  let btn = document.getElementById('start')
  let div = document.getElementById('div')
  let start = 0
  let allInterval = []

  const progress = () => {
    div.style.width = div.offsetWidth + 1 + 'px'
    div.innerHTML = (div.offsetWidth) + '%'
    if (div.offsetWidth < 100) {
      let current = Date.now()
      allInterval.push(current - start)
      start = current
      requestAnimationFrame(progress)
    }  
  }

  btn.addEventListener('click', () => {
    div.style.width = 0
    let currrent = Date.now()
    start = currrent
    requestAnimationFrame(progress)
  })
</script>

運行上面的代碼,就可以看到瀏覽器會在每一幀運行結束后,將div的寬度加1px,直到100px為止。

2.5.2 requestIdleCallback

requestIdleCallback 也是 Fiber 的基礎 API 。requestIdleCallback能使開發者在主事件循環上執行后臺和低優先級的工作,而不會影響延遲關鍵事件,如動畫和輸入響應。正常幀任務完成后沒超過16ms,說明有多余的空閑時間,此時就會執行requestIdleCallback里注冊的任務。

具體的執行流程是,開發者采用requestIdleCallback方法注冊對應的任務,告知瀏覽器任務的優先級不高,如果每一幀內存在空閑時間,就可以執行注冊的這個任務。另外,開發者是可以傳入timeout參數去定義超時時間的,如果到了超時時間,那么瀏覽器必須立即執行,使用方法如下:

window.requestIdleCallback(callback, { timeout: 1000 })

瀏覽器執行完方法后,如果沒有剩余時間了,或者已經沒有下一個可執行的任務了,React應該歸還控制權,并同樣使用requestIdleCallback去申請下一個時間片。具體的流程如下圖:

其中,requestIdleCallback的callback中會接收到默認參數 deadline ,其中包含了以下兩個屬性:

  • timeRamining:返回當前幀還剩多少時間供用戶使用。
  • didTimeout:返回 callback 任務是否超時。

三、Fiber執行流程

Fiber的執行流程總體可以分為渲染和調度兩個階段,即render階段和commit 階段。其中,render 階段是可中斷的,需要找出所有節點的變更;而commit 階段是不可中斷的,只會執行操作。

3.1 render階段

此階段的主要任務就是找出所有節點產生的變更,如節點的新增、刪除、屬性變更等。這些變更, React 統稱為副作用,此階段會構建一棵Fiber tree,以虛擬Dom節點的維度對任務進行拆分,即一個虛擬Dom節點對應一個任務,最后產出的結果是副作用列表(effect list)。

3.1.1 遍歷流程

在此階段,React Fiber會將虛擬DOM樹轉化為Fiber tree,這個Fiber tree是由節點構成的,每個節點都有child、sibling、return屬性,遍歷Fiber tree時采用的是后序遍歷方法,遍歷的流程如下:
從頂點開始遍歷;
如果有大兒子,先遍歷大兒子;如果沒有大兒子,則表示遍歷完成;
大兒子: a. 如果有弟弟,則返回弟弟,跳到2 b. 如果沒有弟弟,則返回父節點,并標志完成父節點遍歷,跳到2 d. 如果沒有父節點則標志遍歷結束

下面是后序遍歷的示意圖:

此時,樹結構的定義如下:

const A1 = { type: 'div', key: 'A1' }
const B1 = { type: 'div', key: 'B1', return: A1 }
const B2 = { type: 'div', key: 'B2', return: A1 }
const C1 = { type: 'div', key: 'C1', return: B1 }
const C2 = { type: 'div', key: 'C2', return: B1 }
const C3 = { type: 'div', key: 'C3', return: B2 }
const C4 = { type: 'div', key: 'C4', return: B2 }
A1.child = B1
B1.sibling = B2
B1.child = C1
C1.sibling = C2
B2.child = C3
C3.sibling = C4
module.exports = A1

3.1.2 收集effect list

接下來,就是收集節點產生的變更,并將結果轉化成一個effect list,步驟如下:

  1. 如果當前節點需要更新,則打tag更新當前節點狀態(props, state, context等);
  2. 為每個子節點創建fiber。如果沒有產生child fiber,則結束該節點,把effect list歸并到return,把此節點的sibling節點作為下一個遍歷節點;否則把child節點作為下一個遍歷節點;
  3. 如果有剩余時間,則開始下一個節點,否則等下一次主線程空閑再開始下一個節點;
  4. 如果沒有下一個節點了,進入pendingCommit狀態,此時effect list收集完畢,結束。

如果用代碼來實現的話,首先需要遍歷子虛擬DOM元素數組,為每個虛擬DOM元素創建子fiber。

const reconcileChildren = (currentFiber, newChildren) => {
  let newChildIndex = 0
  let prevSibling // 上一個子fiber

  // 遍歷子虛擬DOM元素數組,為每個虛擬DOM元素創建子fiber
  while (newChildIndex < newChildren.length) {
    let newChild = newChildren[newChildIndex]
    let tag
    // 打tag,定義 fiber類型
    if (newChild.type === ELEMENT_TEXT) { // 這是文本節點
      tag = TAG_TEXT
    } else if (typeof newChild.type === 'string') {  // 如果type是字符串,則是原生DOM節點
      tag = TAG_HOST
    }
    let newFiber = {
      tag,
      type: newChild.type,
      props: newChild.props,
      stateNode: null, // 還未創建DOM元素
      return: currentFiber, // 父親fiber
      effectTag: INSERT, // 副作用標識,包括新增、刪除、更新
      nextEffect: null, // 指向下一個fiber,effect list通過nextEffect指針進行連接
    }
    if (newFiber) {
      if (newChildIndex === 0) {
        currentFiber.child = newFiber // child為大兒子
      } else {
        prevSibling.sibling = newFiber // 讓大兒子的sibling指向二兒子
      }
      prevSibling = newFiber
    }
    newChildIndex++
  }
}

該方法會收集 fiber 節點下所有的副作用,并組成effect list。每個 fiber 有兩個屬性:

  • firstEffect:指向第一個有副作用的子fiber。
  • lastEffect:指向最后一個有副作用的子fiber。

而我們需要收集的就是中間nextEffect,最終形成一個單鏈表。

// 在完成的時候要收集有副作用的fiber,組成effect list
const completeUnitOfWork = (currentFiber) => {
  // 后續遍歷,兒子們完成之后,自己才能完成。最后會得到以上圖中的鏈條結構。
  let returnFiber = currentFiber.return
  if (returnFiber) {
    // 如果父親fiber的firstEffect沒有值,則將其指向當前fiber的firstEffect
    if (!returnFiber.firstEffect) {
      returnFiber.firstEffect = currentFiber.firstEffect
    }
    // 如果當前fiber的lastEffect有值
    if (currentFiber.lastEffect) {
      if (returnFiber.lastEffect) {
        returnFiber.lastEffect.nextEffect = currentFiber.firstEffect
      }
      returnFiber.lastEffect = currentFiber.lastEffect
    }
    const effectTag = currentFiber.effectTag
    if (effectTag) { // 說明有副作用
      // 每個fiber有兩個屬性:
      // 1)firstEffect:指向第一個有副作用的子fiber
      // 2)lastEffect:指向最后一個有副作用的子fiber
      // 中間的使用nextEffect做成一個單鏈表
      if (returnFiber.lastEffect) {
        returnFiber.lastEffect.nextEffect = currentFiber
      } else {
        returnFiber.firstEffect = currentFiber
      }
      returnFiber.lastEffect = currentFiber
    }
  }
}

最后,再定義一個遞歸函數,從根節點出發,把全部的 fiber 節點遍歷一遍,最終產出一個effect list。

const performUnitOfWork = (currentFiber) => {
  beginWork(currentFiber)
  if (currentFiber.child) {
    return currentFiber.child
  }
  while (currentFiber) {
    completeUnitOfWork(currentFiber)  
    if (currentFiber.sibling) {  
      return currentFiber.sibling
    }
    currentFiber = currentFiber.return 
  }
}

3.2 commit階段

commit 階段需要將上階段計算出來的需要處理的副作用一次性執行,此階段不能暫停,否則會出現UI更新不連續的現象。此階段需要根據effect list,將所有更新都 commit 到DOM樹上。

3.2.1 根據effect list 更新視圖

此階段,根據一個 fiber 的effect list列表去更新視圖,此次只列舉了新增節點、刪除節點、更新節點的三種操作 。

const commitWork = currentFiber => {
  if (!currentFiber) return
  let returnFiber = currentFiber.return
  let returnDOM = returnFiber.stateNode // 父節點元素
  if (currentFiber.effectTag === INSERT) {  // 如果當前fiber的effectTag標識位INSERT,則代表其是需要插入的節點
    returnDOM.appendChild(currentFiber.stateNode)
  } else if (currentFiber.effectTag === DELETE) {  // 如果當前fiber的effectTag標識位DELETE,則代表其是需要刪除的節點
    returnDOM.removeChild(currentFiber.stateNode)
  } else if (currentFiber.effectTag === UPDATE) {  // 如果當前fiber的effectTag標識位UPDATE,則代表其是需要更新的節點
    if (currentFiber.type === ELEMENT_TEXT) {
      if (currentFiber.alternate.props.text !== currentFiber.props.text) {
        currentFiber.stateNode.textContent = currentFiber.props.text
      }
    }
  }
  currentFiber.effectTag = null
}

寫一個遞歸函數,從根節點出發,根據effect list完成全部更新。

/**
* 根據一個 fiber 的 effect list 更新視圖
*/
const commitRoot = () => {
  let currentFiber = workInProgressRoot.firstEffect
  while (currentFiber) {
    commitWork(currentFiber)
    currentFiber = currentFiber.nextEffect
  }
  currentRoot = workInProgressRoot // 把當前渲染成功的根fiber賦給currentRoot
  workInProgressRoot = null
}

3.2.2 視圖更新

接下來,就是循環執行工作,當計算完成每個 fiber 的effect list后,調用 commitRoot 完成視圖更新。

const workloop = (deadline) => {
  let shouldYield = false // 是否需要讓出控制權
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1 // 如果執行完任務后,剩余時間小于1ms,則需要讓出控制權給瀏覽器
  }
  if (!nextUnitOfWork && workInProgressRoot) {
    console.log('render階段結束')
    commitRoot() // 沒有下一個任務了,根據effect list結果批量更新視圖
  }
  // 請求瀏覽器進行再次調度
  requestIdleCallback(workloop, { timeout: 1000 })
}

到此,根據收集到的變更信息完成了視圖的刷新操作,Fiber的整個刷新流程也就實現了。

四、總結

相比傳統的Stack架構,Fiber 將工作劃分為多個工作單元,每個工作單元在執行完成后依據剩余時間決定是否讓出控制權給瀏覽器執行渲染。 并且它設置每個工作單元的優先級,暫停、重用和中止工作單元。 每個Fiber節點都是fiber tree上的一個節點,通過子、兄弟和返回引用連接,形成一個完整的fiber tree。

原文鏈接:https://blog.csdn.net/xiangzhihong8/article/details/126166715

欄目分類
最近更新