欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解React?Fiber架構原理

 更新時間:2022年08月05日 14:14:30   作者:xiangzhihong8  
Fiber?可以理解為一個執(zhí)行單元,每次執(zhí)行完一個執(zhí)行單元,React?Fiber就會檢查還剩多少時間,如果沒有時間則將控制權讓出去,然后由瀏覽器執(zhí)行渲染操作,這篇文章主要介紹了React?Fiber架構原理剖析,需要的朋友可以參考下

一、概述

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

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

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

二、Fiber架構

2.1 執(zhí)行單元

官方介紹中,F(xiàn)iber 被理解為是一種數(shù)據(jù)結構,但是我們也可以將它理解為是一個執(zhí)行單元。

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

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

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

2.2 數(shù)據(jù)結構

在官方的文檔中,F(xiàn)iber 被解釋為是一種數(shù)據(jù)結構,即鏈表結構。在鏈表結構中,每個 Virtual DOM 都可以表示為一個 fiber,如下圖所示。

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

2.3 Fiber鏈表結構

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

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

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

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

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

接下來,再定義兩個方法:用于插入節(jié)點單元的enqueueUpdate()和用于更新隊列的forceUpdate()。并且,插入節(jié)點單元時需要考慮是否已經存在節(jié)點,如果不存在直接將firstUpdate、lastUpdate指向此節(jié)點即可。更新隊列是遍歷這個鏈表,根據(jù)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) {
      // 判斷是函數(shù)還是對象,是函數(shù)則需要執(zhí)行,是對象則直接返回
      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
  }
}

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

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節(jié)點

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

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

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

2.5 API

2.5.1 requestAnimationFrame

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

例如,使用requestAnimationFrame實現(xiàn)正方形的寬度加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能使開發(fā)者在主事件循環(huán)上執(zhí)行后臺和低優(yōu)先級的工作,而不會影響延遲關鍵事件,如動畫和輸入響應。正常幀任務完成后沒超過16ms,說明有多余的空閑時間,此時就會執(zhí)行requestIdleCallback里注冊的任務。

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

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

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

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

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

三、Fiber執(zhí)行流程

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

3.1 render階段

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

3.1.1 遍歷流程

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

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

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

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

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

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

如果用代碼來實現(xiàn)的話,首先需要遍歷子虛擬DOM元素數(shù)組,為每個虛擬DOM元素創(chuàng)建子fiber。

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

  // 遍歷子虛擬DOM元素數(shù)組,為每個虛擬DOM元素創(chuàng)建子fiber
  while (newChildIndex < newChildren.length) {
    let newChild = newChildren[newChildIndex]
    let tag
    // 打tag,定義 fiber類型
    if (newChild.type === ELEMENT_TEXT) { // 這是文本節(jié)點
      tag = TAG_TEXT
    } else if (typeof newChild.type === 'string') {  // 如果type是字符串,則是原生DOM節(jié)點
      tag = TAG_HOST
    }
    let newFiber = {
      tag,
      type: newChild.type,
      props: newChild.props,
      stateNode: null, // 還未創(chuàng)建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 節(jié)點下所有的副作用,并組成effect list。每個 fiber 有兩個屬性:

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

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

// 在完成的時候要收集有副作用的fiber,組成effect list
const completeUnitOfWork = (currentFiber) => {
  // 后續(xù)遍歷,兒子們完成之后,自己才能完成。最后會得到以上圖中的鏈條結構。
  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
    }
  }
}

最后,再定義一個遞歸函數(shù),從根節(jié)點出發(fā),把全部的 fiber 節(jié)點遍歷一遍,最終產出一個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 階段需要將上階段計算出來的需要處理的副作用一次性執(zhí)行,此階段不能暫停,否則會出現(xiàn)UI更新不連續(xù)的現(xiàn)象。此階段需要根據(jù)effect list,將所有更新都 commit 到DOM樹上。

3.2.1 根據(jù)effect list 更新視圖

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

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

寫一個遞歸函數(shù),從根節(jié)點出發(fā),根據(jù)effect list完成全部更新。

/**
* 根據(jù)一個 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 視圖更新

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

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

到此,根據(jù)收集到的變更信息完成了視圖的刷新操作,F(xiàn)iber的整個刷新流程也就實現(xiàn)了。

四、總結

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

到此這篇關于React Fiber架構原理剖析的文章就介紹到這了,更多相關React Fiber原理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

您可能感興趣的文章:

相關文章

  • 關于react-router/react-router-dom v4 history不能訪問問題的解決

    關于react-router/react-router-dom v4 history不能訪問問題的解決

    這篇文章主要給大家介紹了關于react-router/react-router-dom v4 history不能訪問問題的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧。
    2018-01-01
  • React-router4路由監(jiān)聽的實現(xiàn)

    React-router4路由監(jiān)聽的實現(xiàn)

    這篇文章主要介紹了React-router4路由監(jiān)聽的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-08-08
  • ES6 class類鏈式繼承,實例化及react super(props)原理詳解

    ES6 class類鏈式繼承,實例化及react super(props)原理詳解

    這篇文章主要介紹了ES6 class類鏈式繼承,實例化及react super(props)原理,結合實例形式詳細分析了ES6 中class類鏈式繼承,實例化及react super(props)原理相關概念、原理、定義與使用技巧,需要的朋友可以參考下
    2020-02-02
  • React18從0實現(xiàn)dispatch?update流程

    React18從0實現(xiàn)dispatch?update流程

    這篇文章主要為大家介紹了React18從0實現(xiàn)dispatch?update流程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01
  • React中常用的Hook有哪些

    React中常用的Hook有哪些

    這篇文章主要介紹了react hooks實現(xiàn)原理,文中給大家介紹了useState dispatch 函數(shù)如何與其使用的 Function Component 進行綁定,節(jié)后實例代碼給大家介紹的非常詳細,需要的朋友可以參考下
    2023-01-01
  • React Hook之使用Effect Hook的方法

    React Hook之使用Effect Hook的方法

    這篇文章主要為大家詳細介紹了React 使用Effect Hook的方法,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2022-03-03
  • react-native彈窗封裝的方法

    react-native彈窗封裝的方法

    這篇文章主要為大家詳細介紹了react-native彈窗封裝的方法,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-08-08
  • react高階組件經典應用之權限控制詳解

    react高階組件經典應用之權限控制詳解

    在React中,高階組件是重用組件邏輯的一項高級技術。下面這篇文章主要給大家介紹了關于react高階組件經典應用之權限控制的相關資料,文中通過示例代碼介紹的非常詳細,對大家具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
    2017-09-09
  • create-react-app修改為多頁面支持的方法

    create-react-app修改為多頁面支持的方法

    本篇文章主要介紹了create-react-app修改為多頁面支持的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-05-05
  • react實現(xiàn)同頁面三級跳轉路由布局

    react實現(xiàn)同頁面三級跳轉路由布局

    這篇文章主要為大家詳細介紹了react實現(xiàn)同頁面三級跳轉路由布局,一個路由小案例,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-09-09

最新評論