由淺入深快速掌握React Fiber

React-fiber.png

前言


React Fiber 不是一個(gè)新的東西辰晕,但在前端領(lǐng)域是第一次廣為認(rèn)知的應(yīng)用猎贴。幾年前全新的Fiber架構(gòu)讓剛剛上手React的我又迷茫了逛漫。我不是升到React v16了嗎? 沒什么出奇的啊? 真正要體會(huì)到 React Fiber 重構(gòu)效果,可能下個(gè)月责蝠、可能要等到 v17裆悄。v16 只是一個(gè)過渡版本矛纹,也就是說,現(xiàn)在的React 還是同步渲染的光稼,一直在跳票或南、不是說今年第二季度就出來了嗎。是我開始查閱資料艾君,無奈尼瑪理解起來很吃力啊采够。一直想總結(jié)一篇文章來著,這都 2020年了腻贰,我才開始寫 Fiber 的文章吁恍,表示慚愧呀。不過現(xiàn)在好的是關(guān)于 Fiber 的資料已經(jīng)很豐富了播演,我發(fā)現(xiàn)網(wǎng)上很多文章寫的非常不容易理解冀瓦。而且比較片面。于是決定先講一下React Fiber學(xué)習(xí)前的準(zhǔn)備知識(shí)写烤。然后
用比較容易理解話語讓讀者盡快明白R(shí)eact Fiber是個(gè)什么東東翼闽。見解有限,如有描述不當(dāng)之處洲炊,請(qǐng)幫忙及時(shí)指出感局,如有錯(cuò)誤尼啡,會(huì)及時(shí)修正。

框架總覽


  • ?? 從瀏覽器多進(jìn)程到JS單線程
    • ?? 區(qū)分進(jìn)程和線程
    • ?? 瀏覽器是多進(jìn)程的
    • ?? 瀏覽器內(nèi)核(渲染進(jìn)程)
    • ?? 梳理瀏覽器內(nèi)核中線程之間的關(guān)系
    • ??Browser進(jìn)程和瀏覽器內(nèi)核(Renderer進(jìn)程)的通信過程
    • ?? 簡單梳理下瀏覽器渲染流程
    • ?? 從Event Loop談JS的運(yùn)行機(jī)制
    • ?? 單獨(dú)說說定時(shí)器
    • ?? setTimeout而不是setInterval
    • ?? 事件循環(huán)進(jìn)階:macrotask與microtask
  • ?? React 的核心知識(shí)點(diǎn)
    • ?? React 的核心思想询微。
    • ?? JSX(JavaScript XML)element(元素)與 React.createElement
    • ?? virtual DOM 和Diff算法
    • ?? React 渲染原理(1)--首次渲染
    • ?? React 渲染原理(2)--執(zhí)行setState之后做了什么
  • ?? React 16之前
    • ?? React 16 之前的不足
    • ?? 解決方案
  • ?? 什么是 Fiber
  • ?? React 的Fiber架構(gòu)
    • ?? 新的的調(diào)和器(Fiber-Reconciler)
    • ?? Fiber-Reconciler的階段劃分
    • ?? 數(shù)據(jù)結(jié)構(gòu)的演進(jìn)
    • ?? 任務(wù)如何分片及分片的優(yōu)先級(jí)
    • ?? Fiber Tree
    • ?? Side Effect(副作用)
    • ?? Effects List
    • ?? react Fiber是如何工作的
    • ?? altername崖瞭、current Tree及 workInProgress Tree的關(guān)系
    • ?? Reconciliation(協(xié)調(diào)階段)
    • ?? commit(提交階段)
    • ?? 雙緩沖原理
    • ?? 后記
    • ?? 站在巨人肩上

從瀏覽器多進(jìn)程到JS單線程

掌握瀏覽器多進(jìn)程到JS單線程對(duì)了解React Fiber的架構(gòu)初衷特別有幫助,網(wǎng)上查閱了很多資料撑毛。已經(jīng)將其整理成文章
從瀏覽器多進(jìn)程到JS單線程

React 的核心知識(shí)點(diǎn)

React 的核心思想

內(nèi)存中維護(hù)一顆虛擬DOM樹书聚,數(shù)據(jù)變化時(shí)(setState),自動(dòng)更新虛擬 DOM藻雌,得到一顆新樹雌续,然后 Diff 新老虛擬 DOM 樹才睹,找到有變化的部分瓣履,得到一個(gè) Change(Patch),將這個(gè) Patch 加入隊(duì)列窿侈,最終批量更新這些 Patch 到 DOM 中做个。

JSX(JavaScript XML)鸽心、element(元素)與 React.createElement

jsx.png

(1)什么是JSX
我們來看一下這樣一段代碼:

const title = <h1 className="title">Hello, world!</h1>;

定義:JSX是JavaScript XML,是React提供的語法糖。 通過它我們就可以很方便的在js代碼中書寫html片段叁温。
作用:React中用JSX來創(chuàng)建虛擬DOM(元素)再悼。
注意:JSX不是字符串核畴,也不是HTML/XML標(biāo)簽膝但,本質(zhì)上還是javascript;上面這段代碼會(huì)被babel轉(zhuǎn)換成如下javascript:

const title = React.createElement(
    'h1',
    { className: 'title' },
    'Hello, world!'
);

(2)JSX基本語法規(guī)則
如以下代碼:

class Home extends React.Component {
  render() {
    return (
      <div>
        <Welcome name='飛哥' />
        <p>Anything you like</p>
        {this.state.product.length>0?<p className="title">簡書</p>:''"}
      </div>
    )
  }
}
  • 變量使用
    1.注釋

在 HTML 中谤草,我們會(huì)使用 進(jìn)行注釋跟束,不過 JSX 中并不支持:


render() {
  return (
    <div>
      <!-- This doesn't work! -->
    </div>
  )
}

我們需要以 JavaScript 中塊注釋的方式進(jìn)行注釋:


{/* A JSX comment */}

{/* 
  Multi
  line
  comment
*/}  

2.數(shù)組

JSX 允許使用任意的變量,因此如果我們需要使用數(shù)組進(jìn)行循環(huán)元素渲染時(shí)丑孩,直接使用 map冀宴、reduce、filter 等方法即可:

function NumberList(props) {
  const numbers = props.numbers;
  return (
    <ul>
      {numbers.map((number) =>
        <ListItem key={number.toString()}
                  value={number} />
      )}
    </ul>
  );
}

3.條件渲染

在JSX中我們不能再使用傳統(tǒng)的if/else條件判斷語法温学,但是可以使用更為簡潔明了的Conditional Operator運(yùn)算符略贮,譬如我們要進(jìn)行if操作:

{condition && <span>為真時(shí)進(jìn)行渲染</span> }

如果要進(jìn)行非操作:

{condition || <span>為假時(shí)進(jìn)行渲染</span> }

我們也可以使用常見的三元操作符進(jìn)行判斷:

{condition
  ? <span>為真時(shí)進(jìn)行渲染</span>
  : <span>為假時(shí)進(jìn)行渲染</span>
}

如果對(duì)于較大的代碼塊,建議是進(jìn)行換行以提升代碼可讀性:

{condition ? (
  <span>
   為假時(shí)進(jìn)行渲染
  </span>
) : (
  <span>
   為假時(shí)進(jìn)行渲染
  </span>
)}
  • 元素屬性
    1.style 屬性

JSX 中的 style 并沒有跟 HTML 一樣接收某個(gè) CSS 字符串仗岖,而是接收某個(gè)使用 camelCase 風(fēng)格屬性的 JavaScript 對(duì)象逃延,這一點(diǎn)倒是和DOM 對(duì)象的 style 屬性一致。譬如:

const divStyle = {
  color: 'blue',
  backgroundImage: 'url(' + imgUrl + ')',
};

function HelloWorldComponent() {
  return <div style={divStyle}>Hello World!</div>;
}

注意轧拄,內(nèi)聯(lián)樣式并不能自動(dòng)添加前綴揽祥,這也是筆者不太喜歡使用 CSS-in-JS 這種形式設(shè)置樣式的的原因。為了支持舊版本瀏覽器檩电,需要提供相關(guān)的前綴:

const divStyle = {
  WebkitTransition: 'all', // note the capital 'W' here
  msTransition: 'all' // 'ms' is the only lowercase vendor prefix
};

function ComponentWithTransition() {
  return <div style={divStyle}>This should work cross-browser</div>;
}

2.className

React 中是使用 className 來聲明 CSS 類名拄丰,這一點(diǎn)對(duì)于所有的 DOM 與 SVG 元素都起作用府树。不過如果你是將 React 與 Web Components 結(jié)合使用,也是可以使用 class 屬性的料按。

3.htmlFor

因?yàn)?for 是JavaScript中的保留關(guān)鍵字奄侠,因此 React 元素是使用 htmlFor 作為替代。

4.Boolean 系列屬性

HTML 表單元素中我們經(jīng)常會(huì)使用 disabled载矿、required遭铺、checked 與 readOnly 等 Boolean 值性質(zhì)的書,缺省的屬性值會(huì)導(dǎo)致 JSX 認(rèn)為 bool 值設(shè)為 true恢准。當(dāng)我們需要傳入 false 時(shí)魂挂,必須要使用屬性表達(dá)式。譬如 <input type='checkbox' checked={true}> 可以簡寫為<input type='checkbox' checked>馁筐,而 <input type='checkbox' checked={falsed}> 即不可以省略 checked 屬性涂召。

  • 自定義屬性

如果在 JSX 中向 DOM 元素中傳入自定義屬性,React 是會(huì)自動(dòng)忽略的:

<div customProperty='a' />

不過如果要使用HTML標(biāo)準(zhǔn)的自定義屬性敏沉,即以 data-* 或者 aria-* 形式的屬性是支持的果正。

<div data-attr='attr' />
  • 總結(jié)
  • JSX只允許被一個(gè)標(biāo)簽包裹,因此最外層要包裹一個(gè)閉合的標(biāo)簽盟迟。
  • 如果使用數(shù)據(jù)形式秋泳,必須對(duì)數(shù)組中的每一個(gè)元素都要給一個(gè)唯一的key值
  • 元素如果是小寫首字母則是元素,如果是大寫首字母則是組件元素
  • 遇到以 { 開頭的代碼攒菠,以JS的語法解析: 標(biāo)簽中的js代碼必須用{}包含迫皱。
  • class屬性改為className
  • for屬性改成htmlFor
  • 傳入數(shù)據(jù)的展開性{…props}
  • JSX中,我們必須使用駝峰的形式來書寫事件的屬性名
  • 如果你在JSX中使用了不存在于HTML規(guī)范的屬性辖众,這個(gè)屬性是會(huì)被忽略的卓起,你需要使用data-方式來自定義屬性

React.createElement和 element

依然拿上面的代碼舉個(gè)例子:

上面的JSX經(jīng)過bable轉(zhuǎn)義之后返回javascript 函數(shù)。函數(shù)React.createElement知道如何渲染type = 'div' 和 type = 'p' 的節(jié)點(diǎn)凹炸,但不知道如何渲染type='Welcome'的節(jié)點(diǎn)當(dāng)React 發(fā)現(xiàn)Welcome 是一個(gè)React 組件時(shí)戏阅,會(huì)調(diào)用該組件的render方法,產(chǎn)生該組件的Element啤它,如果該組件的element中有首字母大寫開頭的Element的type奕筐,繼續(xù)找下去,直到?jīng)]有首字母大寫的type变骡。
因此离赫,所有的React組件必須首字母大寫,原因是生成React Element的時(shí)候锣光,type屬性會(huì)直接使用該組件的實(shí)例化時(shí)使用的名字(<Welcome/>)如果沒大寫React將不能判斷是否需要繼續(xù)調(diào)用該組件的render方法創(chuàng)建Element

class Home extends React.Component {
  render() {
    return (
      <div>
        <Welcome name='飛哥' />
        <p>Anything you like</p>
        {this.state.product.length>0?<p className="title">簡書</p>:''"}
      </div>
    )
  }
}
// 被babel.js轉(zhuǎn)義之后jsx變成了一段javascript :
React.createElement(
    'div',
     null,
    React.createElement(
      'Welcome',
      {"name":'飛哥'},
       null
    ),
    React.createElement(
      'p',
       null
      'Anything you like'
    ),
    this.state.product.length>0? React.createElement(
      'p',
      {'className':'title'},
      '簡書'
    )
);
// 執(zhí)行上面的javascript 函數(shù)之后生成的對(duì)象就是element笆怠,element就是VDom,多個(gè)element組成的一整個(gè)對(duì)象就是VDom樹。
{
  type: 'div',
  props:{
    children: [
      {
        type: 'Welcome',
        props: {
          name: '老干部'
        },
      },
      {
        type: 'p',
        props: {
          children: 'Anything you like'
        },
      },
     {
        type: 'p',
        props: {
          className:‘title’,
          children: '簡書'
        },
      }
    ]
  }
}

(1)element
在React中Element通常指代的是render函數(shù)或者stateless函數(shù)返回的Object對(duì)象,擁有props誊爹,type蹬刷,key等等屬性瓢捉,用來描述真實(shí)的DOM 長什么樣子,這個(gè)對(duì)象通常作為虛擬DOM的一個(gè)節(jié)點(diǎn)办成,React通過調(diào)用ReactDOM.render將這些虛擬DOM在瀏覽器上渲染成真實(shí)DOM泡态。在React中Element根據(jù)其type屬性的不同,分成兩類: 以原生的DOM元素作為return值的組件迂卢,以及以React組件作為return值的組件某弦。

可以通過React 庫提供的React.createElement 函數(shù)來創(chuàng)建。

(2)React.createElement(a, b, c)

let h1Elem = React.createElement('h1', {id: 'recipe', 'data-type': 'title'}, 'Hello World');

作用:根據(jù)指定的第一個(gè)參數(shù)創(chuàng)建一個(gè)React元素(element)

  • 第一個(gè)參數(shù) a:表示元素的類型而克,比如:h1, div 等靶壮。
  • 第二個(gè)參數(shù) b:表示該元素上的屬性,使用 JavaScript 對(duì)象方式表示员萍。
  • 第三個(gè)參數(shù) c:表示該元素內(nèi)部的內(nèi)容腾降,可以是文字,可以繼續(xù)嵌套另外一個(gè) React.createElement(a, b, c)碎绎。
    這種方法其實(shí)在實(shí)際 React 開發(fā)中幾乎不會(huì)使用螃壤,因?yàn)榭梢灾苯邮褂?JSX 。

Virtual DOM和Diff算法

什么是Virtual DOM?

概念: Virtual DOM(虛擬DOM)是對(duì)DOM的抽象筋帖,本質(zhì)上是JavaScript對(duì)象奸晴,這個(gè)對(duì)象就是更加輕量級(jí)的對(duì)DOM的描述。簡寫為vdom日麸。
我的理解: 虛擬DOM不是真實(shí)的DOM寄啼,而是一個(gè)JS對(duì)象。React內(nèi)存中始終維護(hù)著一個(gè)對(duì)象赘淮,用來描述當(dāng)前頁面中真實(shí)的DOM. 這個(gè)對(duì)象就是Virtual DOM,它的作用是判斷DOM是否改變辕录、哪些部分需要被重新渲染睦霎。這樣梢卸,不需要操縱真實(shí)的DOM,極大的提高了React的性能。

<div class="title">
    <p>hello</p>
</div>

例如可以將上面的DOM使用JS對(duì)象來表示副女,大致可以是如下結(jié)構(gòu):

let vNode = {
    type: 'div',
    props: {
        className: 'title'
    },
    children: [
        {type: 'p',text: 'hello'}
    ]
}

type: 指定元素的標(biāo)簽類型蛤高,案例為:'ul' (react中用type)
props: 表示指定元素身上的屬性,如id,class, style, 自定義屬性等
children: 表示指定元素是否有子節(jié)點(diǎn)碑幅,參數(shù)以數(shù)組的形式傳入,如果是文本就是數(shù)組中為text的字符串


為什么要使用Virtual DOM
image (2).jpg
  • 首先戴陡,我們都知道在前端性能優(yōu)化的一個(gè)秘訣就是盡可能少地操作DOM,不僅僅是DOM相對(duì)較慢,更因?yàn)轭l繁變動(dòng)DOM會(huì)造成瀏覽器的回流或者重繪沟涨,這些都是性能的殺手恤批,因此我們需要這一層抽象,在patch過程中盡可能地一次性將差異更新到DOM中裹赴,這樣保證了DOM不會(huì)出現(xiàn)性能很差的情況.
  • 其次喜庞,也是Virtual DOM最初的目的诀浪,就是更好的跨平臺(tái),比如Node.js就沒有DOM,如果想實(shí)現(xiàn)SSR(服務(wù)端渲染),那么一個(gè)方式就是借助Virtual DOM,因?yàn)閂irtual DOM本身是JavaScript對(duì)象. 而且在的ReactNative延都,React VR雷猪、weex都是使用了虛擬dom。

React中的核心--Diff算法**

JSX轉(zhuǎn)成dom流程

用JSX語法時(shí)晰房,渲染dom的流程:JSX——Virtual DOM——真實(shí)dom

具體步驟:


1749373634-5bbec7416c02f_articlex.png

(1)獲取state數(shù)據(jù)
(2)解析JSX模板
(3)生成虛擬dom(虛擬dom就是一個(gè)JS對(duì)象求摇,里面包含了對(duì)真實(shí)dom的描述

['div',{id:'a'},['span',{},'hello']]

(4)用虛擬dom解構(gòu),生成真實(shí)dom并顯示

<div id='a'><span>hello</span></div>

(5)state數(shù)據(jù)發(fā)生變化(比如hello變成了hi)
(6)生成新的虛擬dom

['div',{id:'a'},['span',{},'hi']]

(7)比較原始虛擬dom和新的虛擬dom的區(qū)別殊者,找出區(qū)別是span里的內(nèi)容
(8)直接操作dom与境,只改變span里的內(nèi)容


虛擬dom中的diff算法

18616547-937784fb8fbbde22.png

在上面我們介紹了react中state變化時(shí),dom是如何發(fā)生變化的猖吴,在第七步中比較原始虛擬dom和新的虛擬dom的區(qū)別采用的方法嚷辅,就是diff算法(diffrence)。傳統(tǒng)的 Diff 算法也是一直都有的, 但是它的時(shí)間復(fù)雜度為O(n^3) 意思是: 在React中更新10個(gè)元素則需要進(jìn)行1000次的比較距误。(1000個(gè)===10億)簸搞,React 通過制定大膽的策略,將 O(n^3) 復(fù)雜度的問題轉(zhuǎn)換成 O(n^1=n) 復(fù)雜度的問題:

  • 兩個(gè)不同類型的元素會(huì)產(chǎn)生不同的樹
  • 對(duì)于同一層級(jí)的一組子節(jié)點(diǎn)准潭,它們可以通過唯一 key 進(jìn)行區(qū)分

基于以上兩個(gè)前提策略趁俊,React 分別對(duì) tree diff、component diff 以及 element diff 三種 diff 方法是 進(jìn)行算法優(yōu)化刑然,

Tree Diff

Web UI中DOM節(jié)點(diǎn)跨層級(jí)的移動(dòng)操作特別少寺擂,可以忽略不計(jì)。


Tree Diff 01.png

策略:

1 只會(huì)對(duì)同一父節(jié)點(diǎn)下的所有子節(jié)點(diǎn)(相同顏色方框內(nèi))的DOM節(jié)點(diǎn)進(jìn)行比較
2 當(dāng)發(fā)現(xiàn)節(jié)點(diǎn)已經(jīng)不存在泼掠,則該節(jié)點(diǎn)及其子節(jié)點(diǎn)直接移除怔软,不再進(jìn)行深度比較
image.png

執(zhí)行過程:


image.png

如上圖所示,以A為根節(jié)點(diǎn)的整棵樹會(huì)被重新創(chuàng)建择镇,而不是移動(dòng)挡逼,因此 官方建議不要進(jìn)行DOM節(jié)點(diǎn)跨層級(jí)操作,可以通過CSS隱藏腻豌、顯示節(jié)點(diǎn)家坎,而不是真正地移除、添加DOM節(jié)點(diǎn)吝梅。

Component Diff

策略:

1. 如果組件類型相同,暫時(shí)不更新,

2. 如果組件類型不相同,就需要更新; ( 刪除舊的組件,再創(chuàng)建一個(gè)新的組件,插入到刪除組件的那個(gè)位置)
Component Diff 01.png

React 是基于組件構(gòu)建應(yīng)用的虱疏,對(duì)于組件間的比較所采取的策略也是簡潔高效。
如果是同一類型的組件苏携,按照原策略繼續(xù)比較 virtual DOM tree做瞪。
如果不是,則將該組件判斷為 dirty component右冻,從而替換整個(gè)組件下的所有子節(jié)點(diǎn)装蓬。
對(duì)于同一類型的組件衩侥,有可能其 Virtual DOM 沒有任何變化,如果能夠確切的知道這點(diǎn)那可以節(jié)省大量的 diff 運(yùn)算時(shí)間矛物,因此 React 允許用戶通過 **shouldComponentUpdate() **來判斷該組件是否需要進(jìn)行 diff茫死。
如下圖,當(dāng) component D 改變?yōu)?component G 時(shí)履羞,即使這兩個(gè) component 結(jié)構(gòu)相似峦萎,一旦 React 判斷 D 和 G 是不同類型的組件,就不會(huì)比較二者的結(jié)構(gòu)忆首,而是直接刪除 component D爱榔,重新創(chuàng)建 component G 以及其子節(jié)點(diǎn)。雖然當(dāng)兩個(gè) component 是不同類型但結(jié)構(gòu)相似時(shí)糙及,React diff 會(huì)影響性能详幽,但正如 React 官方博客所言:不同類型的 component 是很少存在相似 DOM tree 的機(jī)會(huì),因此這種極端因素很難在實(shí)現(xiàn)開發(fā)過程中造成重大影響的浸锨。

  • 執(zhí)行過程:delete D -> create G
Element Diff

當(dāng)節(jié)點(diǎn)處于同一層級(jí)時(shí)唇聘,React diff 提供了三種節(jié)點(diǎn)操作,分別為: INSERT_MARKUP(插入)柱搜、 MOVE_EXISTING(移動(dòng))REMOVE_NODE(刪除)迟郎。

  1. INSERT_MARKUP(插入),新的組件集合(A聪蘸,B宪肖,C)中 C 不在老組件集合(A,B)中, 即C是全新的節(jié)點(diǎn)健爬,需要對(duì)新節(jié)點(diǎn)執(zhí)行插入操作控乾。

  2. MOVE_EXISTING(移動(dòng)),組件D已經(jīng)在老組件集合(A,B,C,D)里了娜遵,且組件集合更新時(shí)蜕衡,D沒有發(fā)生更新,只是位置改變魔熏,如新集合(A,D,B,C)衷咽,D在第二個(gè),無須像傳統(tǒng)diff蒜绽,讓舊集合的第二個(gè)B和新集合的第二個(gè)D 比較,并且刪除第二個(gè)位置的B桶现,再在第二個(gè)位置插入D躲雅,而是 (對(duì)同一層級(jí)的同組子節(jié)點(diǎn)) 添加唯一key進(jìn)行區(qū)分,做移動(dòng)操作骡和,可以復(fù)用以前的 DOM 節(jié)點(diǎn)相赁。

  3. REMOVE_NODE(刪除)
    (1)新老組件集合中同一個(gè)位置的組件相寇,對(duì)應(yīng)的type不同則不能直接復(fù)用和更新,需要執(zhí)行刪除操作刪除舊的組件再創(chuàng)建新的組件钮科。
    (2)老 element 不在新集合里唤衫,比如組件 D 之前在 集合(A,B,D)中,但集合變成新的集合(A,B)了绵脯,也需要執(zhí)行刪除操作佳励。


重點(diǎn)說下element diff的邏輯

element diff針對(duì)這三個(gè)方法進(jìn)行運(yùn)算,但是單純的去比較新舊集合的差異化蛆挫,會(huì)導(dǎo)致只是進(jìn)行繁雜的添加赃承、刪除操作,于是React 提出優(yōu)化策略:允許開發(fā)者對(duì)同一層級(jí)的同組子節(jié)點(diǎn)悴侵,添加唯一 key 進(jìn)行區(qū)分瞧剖,這樣的策略便使性能有了質(zhì)的飛躍:

情形一:新舊集合中存在相同節(jié)點(diǎn)但位置不同時(shí),如何移動(dòng)節(jié)點(diǎn)

image

(1)看著上圖的 B可免,React先從新中取得B抓于,然后判斷舊中是否存在相同節(jié)點(diǎn)B经宏,當(dāng)發(fā)現(xiàn)存在節(jié)點(diǎn)B后源请,就去判斷是否移動(dòng)B晰绎。
B在舊 中的index=1吧兔,它的lastIndex=0滤蝠,不滿足 index < lastIndex 的條件踱侣,因此 B 不做移動(dòng)操作斥废。此時(shí)筒愚,一個(gè)操作是修己,lastIndex=(index,lastIndex)中的較大數(shù)=1.

注意:lastIndex有點(diǎn)像浮標(biāo)恢总,或者說是一個(gè)map的索引,一開始默認(rèn)值是0睬愤,它會(huì)與map中的元素進(jìn)行比較片仿,比較完后,會(huì)改變自己的值的(取index和lastIndex的較大數(shù))尤辱。

(2)看著 A砂豌,A在舊的index=0,此時(shí)的lastIndex=1(因?yàn)橄惹芭c新的B比較過了)光督,滿足index<lastIndex阳距,因此,對(duì)A進(jìn)行移動(dòng)操作结借,此時(shí)lastIndex=max(index,lastIndex)=1筐摘。

(3)看著D,同(1),不移動(dòng)咖熟,由于D在舊的index=3圃酵,比較時(shí),lastIndex=1馍管,所以改變lastIndex=max(index,lastIndex)=3

(4)看著C郭赐,同(2),移動(dòng)确沸,C在舊的index=2捌锭,滿足index<lastIndex(lastIndex=3),所以移動(dòng)张惹。

由于C已經(jīng)是最后一個(gè)節(jié)點(diǎn)舀锨,所以diff操作結(jié)束。

情形二:新集合中有新加入的節(jié)點(diǎn)宛逗,舊集合中有刪除的節(jié)點(diǎn)

image

(1)B不移動(dòng)坎匿,不贅述,更新l astIndex=1

(2)新集合取得 E雷激,發(fā)現(xiàn)舊不存在替蔬,故在lastIndex=1的位置 創(chuàng)建E,更新lastIndex=1

(3)新集合取得C屎暇,C不移動(dòng)承桥,更新lastIndex=2

(4)新集合取得A,A移動(dòng)根悼,同上凶异,更新lastIndex=2

(5)新集合對(duì)比后,再對(duì)舊集合遍歷挤巡。判斷 新集合 沒有剩彬,但 舊集合 有的元素(如D,新集合沒有矿卑,舊集合有)喉恋,發(fā)現(xiàn) D,刪除D母廷,diff操作結(jié)束轻黑。


diff的不足與待優(yōu)化的地方

image

看圖的 D,此時(shí)D不移動(dòng)琴昆,但它的index是最大的氓鄙,導(dǎo)致更新lastIndex=3,從而使得其他元素A,B,C的index<lastIndex椎咧,導(dǎo)致A,B,C都要去移動(dòng)玖详。

理想情況是只移動(dòng)D把介,不移動(dòng)A,B,C勤讽。因此蟋座,在開發(fā)過程中,盡量減少類似將最后一個(gè)節(jié)點(diǎn)移動(dòng)到列表首部的操作脚牍,當(dāng)節(jié)點(diǎn)數(shù)量過大或更新操作過于頻繁時(shí)向臀,會(huì)影響React的渲染性能。

到此诸狭,我們介紹完了神秘而又強(qiáng)大的React diff算法介紹完畢了

React 渲染原理(1)--首次渲染

我們知道, 對(duì)于一般的React 應(yīng)用, 瀏覽器會(huì)首先執(zhí)行代碼ReactDOM.render來渲染頂層組件, 在這個(gè)過程中遞歸渲染嵌套的子組件, 最終所有組件被插入到DOM中. 我們來看看:調(diào)用ReactDOM.render發(fā)生了什么

一券膀、JSX生成element

這里是一段寫在render里的jsx代碼。

return (
  <div className="cn">
       <Header> Hello, This is React </Header>
       <div>Start to learn right now!</div>
       Right Reserve.
  </div>
)

首先驯遇,它會(huì)經(jīng)過babel編譯成React.createElement的表達(dá)式芹彬。

return (
  React.createElement(
      'div',
      { className: 'cn' },
      React.createElement(
          Header,
          null,
          'Hello, This is React'
      ),
      React.createElement(
          'div',
          null,
          'Start to learn right now!'
      ),
      'Right Reserve'
  )
)

這個(gè)React.createElement的表達(dá)式會(huì)在render函數(shù)被調(diào)用的時(shí)候執(zhí)行,換句話說叉庐,當(dāng)render函數(shù)被調(diào)用的時(shí)候舒帮,會(huì)返回如下element

{
  type: 'div',
    props: {
      className: 'cn',
        children: [
          {
            type: function Header,
            props: {
                children: 'Hello, This is React'
            }
          },
          {
            type: 'div',
            props: {
                children: 'start to learn right now陡叠!'
            }
          },
          'Right Reserve'
      ]
  }
}

我們來觀察一下這個(gè)對(duì)象的children玩郊,現(xiàn)在有三種類型:

1、string

2枉阵、原生DOM節(jié)點(diǎn)

3译红、React Component - 自定義組件

除了這三種,還有兩種類型:

4兴溜、fale ,null, undefined,number

5侦厚、數(shù)組 - 使用map方法的時(shí)候

這里需要記住一個(gè)點(diǎn):element不一定是Object類型。

二拙徽、element如何生成真實(shí)節(jié)點(diǎn)

順利得到element之后刨沦,我們?cè)賮砜纯碦eact是如何把element轉(zhuǎn)化成真實(shí)DOM節(jié)點(diǎn)的:

創(chuàng)建出來的 element 被當(dāng)作參數(shù)和指定的 DOM container 一起傳進(jìn)ReactDOM.render. 接下來會(huì)調(diào)用一些內(nèi)部方法, 接著調(diào)用了 instantiateReactComponent, 這個(gè)函數(shù)根據(jù)element的類型實(shí)例化對(duì)應(yīng)的component. 規(guī)則如下:

先判斷element是否為Object類型,是的話斋攀,看它的type是否是原生DOM標(biāo)簽已卷,是的話創(chuàng)建ReactDOMComponent的實(shí)例對(duì)象并返回,其他同理淳蔼。

image

簡單的instantiateReactComponent函數(shù)實(shí)現(xiàn)侧蘸、如下:

function instantiateReactComponent(node){
    // 文本節(jié)點(diǎn)的情況
    if(typeof node === 'string' || typeof node === 'number'){
        return new ReactDOMTextComponent(node);
    }
    // 瀏覽器默認(rèn)節(jié)點(diǎn)的情況
    if(typeof node === 'object' && typeof node.type === 'string'){
        //注意這里,使用了一種新的component
        return new ReactDOMComponent(node);

    }
    // 自定義的元素節(jié)點(diǎn)鹉梨,類型為構(gòu)造函數(shù)
    if(typeof node === 'object' && typeof node.type === 'function'){
        // 注意這里讳癌,使用新的component,專門針對(duì)自定義元素
        return new ReactCompositeComponent(node);

    }
}

這時(shí)候有的人可能會(huì)有所疑問:這些個(gè)ReactDOMComponent, ReactCompositeComponentWrapper怎么開發(fā)的時(shí)候都沒有見過?

其實(shí)這些都是React的私有類存皂,React自己使用晌坤,不會(huì)暴露給用戶的逢艘。它們的常用方法有:mountComponent,updateComponent等。其中mountComponent 用于創(chuàng)建組件骤菠,而updateComponent用于用戶更新組件它改。而我們自定義組件的生命周期函數(shù)以及render函數(shù)都是在這些私有類的方法里被調(diào)用的。

既然這些私有類的方法那么重要我們就先來簡單了解一下吧~

ReactDOMComponent

首先是ReactMComponent的mountComponent方法商乎,這個(gè)方法的作用是:將element轉(zhuǎn)成真實(shí)DOM節(jié)點(diǎn)央拖,并且插入到相應(yīng)的container里,然后返回realDOM鹉戚。

由此可知ReactDOMComponentmountComponent是element生成真實(shí)節(jié)點(diǎn)的關(guān)鍵鲜戒。

下面看個(gè)栗子它是怎么做到的吧。

假設(shè)有這樣一個(gè)type類型是原生DOM的element:

{
  type: 'div',
    props: {
    className: 'cn',
      children: 'Hello world',
    }
}

簡單mountComponent的實(shí)現(xiàn):


mountComponent(container) {
  const domElement = document.createElement(this._currentElement.type);
  const textNode = document.createTextNode(this._currentElement.props.children);

  domElement.appendChild(textNode);
  container.appendChild(domElement);
  return domElement;
}

其實(shí)實(shí)現(xiàn)的過程很簡單抹凳,就是根據(jù)type生成domElement,再將子節(jié)點(diǎn)append進(jìn)來返回遏餐。當(dāng)然,真實(shí)的mountComponent沒有那么簡單赢底,感興趣的可以自己去看源碼啦失都。

講完ReactDOMComponent,再來看看ReactCompositeComponentWrapper颖系。

ReactCompositeComponentWrapper

這個(gè)類的mountComponent方法作用是:實(shí)例化自定義組件嗅剖,最后是通過遞歸調(diào)用到ReactDOMComponent的mountComponent方法來得到真實(shí)DOM。

注意:也就是說他自己是不直接生成DOM節(jié)點(diǎn)的嘁扼。

那這個(gè)遞歸是一個(gè)怎樣的過程呢信粮?我們通過首次渲染來看下。

首次渲染

假設(shè)我們有一個(gè)Example的組件趁啸,它返回<div>hello world</div> 這樣一個(gè)標(biāo)簽强缘。

首次渲染的過程如下:

image

首先從React.render開始,由于我們剛剛說不傅,render函數(shù)被調(diào)用的時(shí)候會(huì)返回一個(gè)element旅掂,所以此時(shí)返回給我們的element是:

{
  type: function Example,
  props: {
    children: null
  }
}

由于這個(gè)type是一個(gè)自定義組件類,此時(shí)要初始化的類是ReactCompositeComponentWrapper,接著調(diào)用它的mountComponent方法访娶。這里面會(huì)做四件事情商虐,詳情可以看上圖。其中崖疤,第二步的render的得到的element為:

{
  type: 'div',
    props: {
    children: 'Hello World'
  }
}

由于這個(gè)type是一個(gè)原生DOM標(biāo)簽秘车,此時(shí)要初始化的類是ReactDOMComponent。接下來它的mountComponent方法就可以幫我們生成對(duì)應(yīng)的DOM節(jié)點(diǎn)放在瀏覽器里啦劫哼。

這時(shí)候有人可能會(huì)有疑問叮趴,如果第二步render出來的element 類型也是自定義組件呢?

這時(shí)候它就會(huì)去調(diào)用ReactCompositeComponentWrapper的mountComponent方法权烧,從而形成了一個(gè)遞歸眯亦。不管你的自定義組件嵌套多少層伤溉,最后總會(huì)生成原生dom類型的element,所以最后一定能調(diào)用到ReactDOMComponent的mountComponent方法妻率。

感興趣的可以自己在打斷點(diǎn)看下這個(gè)遞歸的過程乱顾。

由我打的斷點(diǎn)圖可以看出在ReactCompositeComponentmountComponent被調(diào)用多次之后,最后調(diào)用到了ReactDOMComponent的mountComponent方法舌涨。

image

到這里糯耍,首次渲染的過程就基本講完了:-D扔字。

但是還有一個(gè)問題:前面我們說自定義組件的生命周期跟render函數(shù)都是在私有類的方法里被調(diào)用的囊嘉,現(xiàn)在只看到render函數(shù)被調(diào)用了,那么首次渲染時(shí)候生命周期函數(shù) componentWillMountcomponentDidMount在哪被調(diào)用呢革为?

image

由圖可知扭粱,在第一步得到instance對(duì)象之后,就會(huì)去看instance.componentWillMount是否有被定義震檩,有的話調(diào)用琢蛤,而在整個(gè)渲染過程結(jié)束之后調(diào)用componentDidMount。

以上抛虏,就是渲染原理的部分博其,讓我們來總結(jié)以下:

JSX代碼經(jīng)過babel編譯之后變成React.createElement的表達(dá)式,這個(gè)表達(dá)式在render函數(shù)被調(diào)用的時(shí)候執(zhí)行生成一個(gè)element迂猴。

在首次渲染的時(shí)候慕淡,先去按照規(guī)則初始化element,接著ReactComponentComponentWrapper通過遞歸沸毁,最終調(diào)用ReactDOMComponent的mountComponent方法來幫助生成真實(shí)DOM節(jié)點(diǎn)峰髓。

React 渲染原理(2)--執(zhí)行setState之后做了什么

由于總結(jié)的內(nèi)容比較多并且是重點(diǎn),單獨(dú)整理了一篇文章
【React進(jìn)階系列】 setState機(jī)制

至此息尺,React 的所有知識(shí)點(diǎn)大概梳理了一下携兵。熟練掌握React的核心知識(shí)對(duì)學(xué)習(xí)React Fiber很有幫助。對(duì)React精通的大佬可以繞過搂誉。
接下來通過和上面的知識(shí)進(jìn)行對(duì)比的形式來講解React 對(duì) Fiber的改造徐紧。


React 16之前

React.png
React 16 之前的不足

當(dāng)時(shí)被大家拍手叫好的 VDOM,為什么今日會(huì)略顯疲態(tài)炭懊,這還要從它的工作原理說起并级。在 react 發(fā)布之初,設(shè)想未來的 UI 渲染會(huì)是異步的凛虽,從 setState() 的設(shè)計(jì)和 react 內(nèi)部的事務(wù)機(jī)制可以看出這點(diǎn)死遭。在 react@16 以前的版本,reconciler(現(xiàn)被稱為 stack reconciler )采用自頂向下遞歸凯旋,從根組件或 setState() 后的組件開始呀潭,更新整個(gè)子樹钉迷。如果組件樹不大不會(huì)有問題,但是當(dāng)組件樹越來越大钠署,遞歸遍歷的成本就越高糠聪,持續(xù)占用主線程,這樣主線程上的布局谐鼎、動(dòng)畫等周期性任務(wù)以及交互響應(yīng)就無法立即得到處理舰蟆,造成頓卡的視覺效果。

理論上人眼最高能識(shí)別的幀數(shù)不超過 30 幀狸棍,電影的幀數(shù)大多固定在 24身害,瀏覽器最優(yōu)的幀率是 60,即16.5ms 左右渲染一次草戈。 瀏覽器正常的工作流程應(yīng)該是這樣的塌鸯,運(yùn)算 -> 渲染 -> 運(yùn)算 -> 渲染 -> 運(yùn)算 -> 渲染 …

image.png

但是當(dāng) JS 執(zhí)行時(shí)間過長,就變成了這個(gè)樣子唐片,F(xiàn)PS(每秒顯示幀數(shù))下降造成視覺上的頓卡丙猬。

image.png

之前的問題主要的問題是任務(wù)一旦執(zhí)行,就無法中斷费韭,js 線程一直占用主線程茧球,導(dǎo)致卡頓。

可能有些接觸前端不久的不是特別理解上面為什么 js 一直占用主線程就會(huì)卡頓星持,我這里還是簡單的普及一下抢埋。

瀏覽器每一幀都需要完成哪些工作?

頁面是一幀一幀繪制出來的钉汗,當(dāng)每秒繪制的幀數(shù)(FPS)達(dá)到 60 時(shí)羹令,頁面是流暢的,小于這個(gè)值時(shí)损痰,用戶會(huì)感覺到卡頓福侈。

1s 60 幀,所以每一幀分到的時(shí)間是 1000/60 ≈ 16 ms卢未。所以我們書寫代碼時(shí)力求不讓一幀的工作量超過 16ms肪凛。

image.png

瀏覽器一幀內(nèi)的工作

瀏覽器在一幀內(nèi)可能會(huì)做執(zhí)行下列任務(wù),而且它們的執(zhí)行順序基本是固定的:

  • 處理用戶輸入事件
  • Javascript執(zhí)行
  • 幀開始辽社,處理瀏覽器事件伟墙,比如resize,scroll等
  • requestAnimation 調(diào)用
  • 布局 Layout
  • 繪制 Paint

在上一小節(jié)提到的調(diào)和階段花的時(shí)間過長,也就是 js 執(zhí)行的時(shí)間過長滴铅,那么就有可能在用戶有交互的時(shí)候戳葵,本來應(yīng)該是渲染下一幀了前联,但是在當(dāng)前一幀里還在執(zhí)行 JS筋遭,就導(dǎo)致用戶交互不能麻煩得到反饋镣衡,從而產(chǎn)生卡頓感崩哩。

解決方案

把渲染更新過程拆分成多個(gè)子任務(wù),每次只做一小部分戏自,做完看是否還有剩余時(shí)間邦投,如果有繼續(xù)下一個(gè)任務(wù);如果沒有擅笔,掛起當(dāng)前任務(wù)志衣,將時(shí)間控制權(quán)交給主線程,等主線程不忙的時(shí)候在繼續(xù)執(zhí)行猛们。 這種策略叫做 Cooperative Scheduling(合作式調(diào)度)念脯,操作系統(tǒng)常用任務(wù)調(diào)度策略之一。

在上面我們已經(jīng)知道瀏覽器是一幀一幀執(zhí)行的阅懦,在執(zhí)行完子任務(wù)和二,下一幀到來之前,主線程通常會(huì)有一小段空閑時(shí)間耳胎,requestIdleCallback可以在這個(gè)空閑期(Idle Period)調(diào)用空閑期回調(diào)(Idle Callback),執(zhí)行一些任務(wù)惕它。

image.png

從上圖也可看出怕午,和 requestAnimationFrame 每一幀必定會(huì)執(zhí)行不同,requestIdleCallback 是撿瀏覽器空閑來執(zhí)行任務(wù)淹魄。

如此一來郁惜,假如瀏覽器一直處于非常忙碌的狀態(tài),requestIdleCallback 注冊(cè)的任務(wù)有可能永遠(yuǎn)不會(huì)執(zhí)行甲锡。此時(shí)可通過設(shè)置 timeout (見下一節(jié) API 介紹)來保證執(zhí)行兆蕉。
這個(gè)方案看似確實(shí)不錯(cuò),但是怎么實(shí)現(xiàn)可能會(huì)遇到幾個(gè)問題:

  • 如何拆分成子任務(wù)缤沦?
  • 一個(gè)子任務(wù)多大合適虎韵?
  • 怎么判斷是否還有剩余時(shí)間?
  • 有剩余時(shí)間怎么去調(diào)度應(yīng)該執(zhí)行哪一個(gè)任務(wù)缸废?

接下里整個(gè) Fiber 架構(gòu)就是來解決這些問題的包蓝。

什么是 Fiber

為了解決上一節(jié)提到解決方案遇到的問題,我們首先需要一種方法將任務(wù)分解為單元企量。從某種意義上說测萎,這就是 Fiber,將可中斷的任務(wù)拆分成多個(gè)子任務(wù),通過按照優(yōu)先級(jí)來自由調(diào)度子任務(wù)届巩,分段更新硅瞧,從而將之前的同步渲染改為異步渲染。

這是一種’契約‘調(diào)度恕汇,要求我們的程序和瀏覽器緊密結(jié)合腕唧,互相信任冒嫡。比如可以由瀏覽器給我們分配執(zhí)行時(shí)間片(通過 requestIdleCallback實(shí)現(xiàn), 下文會(huì)介紹),我們要按照約定在這個(gè)時(shí)間內(nèi)執(zhí)行完畢四苇,并將控制權(quán)還給瀏覽器孝凌。

image.png


Fiber與requestIdleCallback

Fiber所做的就是需要分解渲染任務(wù),然后根據(jù)優(yōu)先級(jí)使用API調(diào)度月腋,異步執(zhí)行指定任務(wù):

1.低優(yōu)先級(jí)任務(wù)由 requestIdleCallback處理蟀架;
2.高優(yōu)先級(jí)任務(wù),如動(dòng)畫相關(guān)的由 requestAnimationFrame處理榆骚;

  1. requestIdleCallback可以在多個(gè)空閑期調(diào)用空閑期回調(diào)片拍,執(zhí)行任務(wù);
  2. requestIdleCallback方法提供 deadline妓肢,即任務(wù)執(zhí)行限制時(shí)間捌省,以切分任務(wù),避免長時(shí)間執(zhí)行碉钠,阻塞UI渲染而導(dǎo)致掉幀纲缓;

requestIdleCallback API
var handle = window.requestIdleCallback(callback[, options])

  • callback:回調(diào),即空閑時(shí)需要執(zhí)行的任務(wù)喊废,該回調(diào)函數(shù)接收一個(gè)IdleDeadline對(duì)象作為入?yún)⒆8摺F渲?code>IdleDeadline對(duì)象包含:
    • didTimeout,布爾值污筷,表示任務(wù)是否超時(shí)工闺,結(jié)合 timeRemaining 使用。
    • timeRemaining()瓣蛀,表示當(dāng)前幀剩余的時(shí)間陆蟆,也可理解為留給任務(wù)的時(shí)間還有多少。
  • options:目前 options 只有一個(gè)參數(shù)
    • timeout惋增。表示超過這個(gè)時(shí)間后叠殷,如果任務(wù)還沒執(zhí)行,則強(qiáng)制執(zhí)行器腋,不必等待空閑溪猿。

IdleDeadline對(duì)象參考MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline

示例
requestIdleCallback(myNonEssentialWork, { timeout: 2000 });

let tasks = {
  length: 4
}

function myNonEssentialWork (deadline) {
  // 當(dāng)回調(diào)函數(shù)是由于超時(shí)才得以執(zhí)行的話,deadline.didTimeout為true
  // deadline.timeRemaining() 獲取每一幀還剩余的時(shí)間
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
    console.log('done', tasks, deadline.timeRemaining())
    tasks.length = tasks.length - 1
  }
  if (tasks.length > 0) {
    requestIdleCallback(myNonEssentialWork);
  }
}

超時(shí)的情況纫塌,其實(shí)就是瀏覽器很忙诊县,沒有空閑時(shí)間,此時(shí)會(huì)等待指定的 timeout 那么久再執(zhí)行措左,通過入?yún)?dealine 拿到的 didTmieout 會(huì)為 true依痊,同時(shí) timeRemaining () 返回的也是 0。超時(shí)的情況下如果選擇繼續(xù)執(zhí)行的話,肯定會(huì)出現(xiàn)卡頓的胸嘁,因?yàn)楸厝粫?huì)將一幀的時(shí)間拉長瓶摆。


React 的Fiber架構(gòu)

新的的調(diào)和器(Fiber-Recocilation)

為了和之前的Task-Reconciler做區(qū)分,我們把Fiber的 Reconciler叫做Fiber-Reconciler性宏。 是 React 里的調(diào)和器群井,這也是任務(wù)調(diào)度完成之后,如何去執(zhí)行每個(gè)任務(wù)毫胜,如何去更新每一個(gè)節(jié)點(diǎn)的過程

??Stack-Recocilation:JSX中創(chuàng)建(或更新)一些元素书斜,react會(huì)根據(jù)這些元素創(chuàng)建(或更新)Virtual DOM,然后react根據(jù)更新前后virtual DOM的區(qū)別酵使,去修改真正的DOM荐吉。注意,在stack reconciler下口渔,DOM的更新是同步的样屠,通過遞歸的方式進(jìn)行渲染,發(fā)現(xiàn)一個(gè)或幾個(gè)instance有更新缺脉,會(huì)立即執(zhí)行DOM更新操作痪欲。


image.png

??Fiber-Recocilation:React 16版本提出了一個(gè)更先進(jìn)的調(diào)和器,它允許渲染進(jìn)程分段完成枪向,而不必須一次性完成勤揩,中間可以返回至主進(jìn)程控制執(zhí)行其他任務(wù)。而這是通過計(jì)算部分組件樹的變更秘蛔,并暫停渲染更新,詢問主進(jìn)程是否有更高需求的繪制或者更新任務(wù)需要執(zhí)行傍衡,這些高需求的任務(wù)完成后才開始渲染深员。這一切的實(shí)現(xiàn)是在代碼層引入了一個(gè)新的數(shù)據(jù)結(jié)構(gòu)-Fiber對(duì)象,每一個(gè)組件實(shí)例對(duì)應(yīng)有一個(gè)fiber實(shí)例蛙埂,此fiber實(shí)例負(fù)責(zé)管理組件實(shí)例的更新倦畅,渲染任務(wù)及與其他fiber實(shí)例的聯(lián)系。通過stateNode屬性管理Instance自身的特性绣的。通過child和sibling表征當(dāng)前工作單元的下一個(gè)工作單元叠赐,return表示處理完成后返回結(jié)果所要合并的目標(biāo),通常指向父節(jié)點(diǎn)屡江。整個(gè)結(jié)構(gòu)是一個(gè)鏈表樹芭概,結(jié)構(gòu)如下:

image.png

Fiber-Reconciler的階段劃分

為了和之前的task Reconciler做區(qū)分,我們把Fiber的 Reconciler叫做Fiber Reconciler惩嘉。 是 React 里的調(diào)和器罢洲,這也是任務(wù)調(diào)度完成之后,如何去執(zhí)行每個(gè)任務(wù)文黎,如何去更新每一個(gè)節(jié)點(diǎn)的過程

如果你現(xiàn)在使用最新的 React 版本(v16), 使用 Chrome 的 Performance 工具惹苗,可以很清晰地看到reconciler 過程分為2個(gè)階段(phase):Reconciliation(協(xié)調(diào)階段) 和 Commit(提交階段).

image.png

  • ?? 協(xié)調(diào)階段: 可以認(rèn)為是 Diff 階段, 這個(gè)階段可以被中斷, 這個(gè)階段會(huì)找出所有節(jié)點(diǎn)變更殿较,例如節(jié)點(diǎn)新增、刪除桩蓉、屬性變更等等, 這些變更React 稱之為'副作用(Effect)' . 以下生命周期鉤子會(huì)在協(xié)調(diào)階段被調(diào)用:

    • constructor
    • componentWillMount 廢棄
    • componentWillReceiveProps 廢棄
    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • componentWillUpdate 廢棄
    • render
  • ?? 提交階段: 將上一個(gè)階段計(jì)算出來的需要處理的副作用(Effects)一次性執(zhí)行了淋纲。這個(gè)階段必須同步執(zhí)行,不能被打斷. 這些生命周期鉤子在提交階段被執(zhí)行:

    • getSnapshotBeforeUpdate() 嚴(yán)格來說院究,這個(gè)是在進(jìn)入 commit 階段前調(diào)用
    • componentDidMount
    • componentDidUpdate
    • componentWillUnmount

也就是說洽瞬,在協(xié)調(diào)階段如果時(shí)間片用完,React就會(huì)選擇讓出控制權(quán)儡首。因?yàn)閰f(xié)調(diào)階段執(zhí)行的工作不會(huì)導(dǎo)致任何用戶可見的變更片任,所以在這個(gè)階段讓出控制權(quán)不會(huì)有什么問題。

需要注意的是:因?yàn)閰f(xié)調(diào)階段可能被中斷蔬胯、恢復(fù)对供,甚至重做,??React 協(xié)調(diào)階段的生命周期鉤子可能會(huì)被調(diào)用多次!, 例如 componentWillMount 可能會(huì)被調(diào)用兩次氛濒。

因此建議 協(xié)調(diào)階段的生命周期鉤子不要包含副作用. 索性 React 就廢棄了這部分可能包含副作用的生命周期方法产场,例如componentWillMountcomponentWillUpdate. v17后我們就不能再用它們了, 所以現(xiàn)有的應(yīng)用應(yīng)該盡快遷移.

現(xiàn)在你應(yīng)該知道為什么'提交階段'必須同步執(zhí)行舞竿,不能中斷的吧京景? 因?yàn)槲覀円_地處理各種副作用,包括DOM變更骗奖、還有你在componentDidMount中發(fā)起的異步請(qǐng)求确徙、useEffect 中定義的副作用... 因?yàn)橛懈弊饔茫员仨毐WC按照次序只調(diào)用一次执桌,況且會(huì)有用戶可以察覺到的變更, 不容差池鄙皇。


數(shù)據(jù)結(jié)構(gòu)的演進(jìn)

Stack-Recocilation運(yùn)行時(shí)存在3種實(shí)例:
{
    DOM //真實(shí)DOM節(jié)點(diǎn)
     -------
    Instances //instance是組件的實(shí)例,但是注意function形式的component沒有實(shí)例
     -------
    Elements //Elements其實(shí)就基本就可以解釋為Virtual DOM仰挣,是利用js對(duì)象的形式來描述一個(gè)DOM節(jié)點(diǎn)(type,props,...children)
}

// Elements是的數(shù)據(jù)結(jié)構(gòu)簡化如下:
let vNode = {
    type: 'div',
    key: '1',
    props: {
        className: 'title'
    },
    children: [
        {type: 'p', key: '2',text: 'hello'}
    ]
}

在首次渲染過程中構(gòu)建出vDOM tree伴逸,后續(xù)需要更新時(shí)(setState())tree diff,compontent diffelement diff根據(jù)vDOM tree的數(shù)據(jù)結(jié)構(gòu)(層級(jí),類型膘壶,key)對(duì)比前后生成的vDOM tree得到DOM change错蝴,并把DOM change應(yīng)用(patch)到DOM樹。

Fiber數(shù)據(jù)結(jié)構(gòu)

Fiber把渲染/更新過程(遞歸diff)拆分成一系列小任務(wù)颓芭,每次檢查樹上的一小部分顷锰,做完看是否還有時(shí)間繼續(xù)下一個(gè)任務(wù),有的話繼續(xù)畜伐,沒有的話把自己掛起馍惹,主線程不忙的時(shí)候再繼續(xù)。增量更新需要更多的上下文信息,之前的vDOM tree顯然難以滿足万矾,所以擴(kuò)展出了fiber tree(即Fiber上下文的vDOM tree)悼吱,更新過程就是根據(jù)輸入數(shù)據(jù)以及現(xiàn)有的fiber tree構(gòu)造出新的fiber tree(workInProgress tree)。因此良狈,Instance層新增了這些實(shí)例:

DOM //
 真實(shí)DOM節(jié)點(diǎn)
-------
effect
    每個(gè)workInProgress tree節(jié)點(diǎn)上都有一個(gè)effect list
    用來存放diff結(jié)果
    當(dāng)前節(jié)點(diǎn)更新完畢會(huì)向上merge effect list(queue收集diff結(jié)果)
- - - -
workInProgress
    workInProgress tree是reconcile過程中從fiber tree建立的當(dāng)前進(jìn)度快照后添,用于斷點(diǎn)恢復(fù)
- - - -
fiber
    fiber tree與vDOM tree類似,用來描述增量更新所需的上下文信息
-------
Elements
    描述UI長什么樣子(type, props,...children)

截止目前薪丁,我們對(duì)Fiber應(yīng)該有了初步的了解遇西,簡單介紹一下Fiber Node的數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)能一定程度反映其整體工作架構(gòu)严嗜。
其實(shí)粱檀,一個(gè)fiber就是一個(gè)JavaScript對(duì)象,以鍵值對(duì)形式存儲(chǔ)了一個(gè)關(guān)聯(lián)組件的信息漫玄,包括組件接收的props茄蚯,維護(hù)的state,最后需要渲染出的內(nèi)容等睦优。fiber 節(jié)點(diǎn)相當(dāng)于以前的虛擬 dom 節(jié)點(diǎn)渗常,結(jié)構(gòu)如下:

interface Fiber {
  /**
   * ?? 節(jié)點(diǎn)的類型信息
   */
  tag: number,   // Fiber 類型,以數(shù)字表示汗盘,可選擇的如下
    - IndeterminateComponent
    - FunctionalComponent
    - ClassComponent // Menu, Table
    - HostRoot // ReactDOM.render 的第二個(gè)參數(shù)
    - HostPortal
    - HostComponent // div, span
    - HostText // 純文本節(jié)點(diǎn)皱碘,即 dom  的 nodeName 等于 '#text'
    - CallComponent // 對(duì)應(yīng) call return 中的 call
    - CallHandlerPhase // call 中的 handler 階段
    - ReturnComponent // 對(duì)應(yīng) call return 中的 return
    - Fragment 
    - Mode // AsyncMode || StrictMode
    - ContextConsumer
    - ContextProvider
    - ForwardRef
  type: any,  // 節(jié)點(diǎn)元素類型, /與 react element 里的 type 一致
  key: null | string, // fiber 的唯一標(biāo)識(shí)
  stateNode: any,  // 對(duì)應(yīng)組件或者 dom 的實(shí)例
  /**
   * ?? 結(jié)構(gòu)信息
   */ 
  // 單鏈表樹結(jié)構(gòu)
  return: Fiber | null,// 指向他在Fiber節(jié)點(diǎn)樹中的`parent`,用來在處理完這個(gè)節(jié)點(diǎn)之后向上返回
  child: Fiber | null,// 指向自己的第一個(gè)子節(jié)點(diǎn)
  sibling: Fiber | null,  // 指向自己的兄弟結(jié)構(gòu)隐孽,兄弟節(jié)點(diǎn)的return指向同一個(gè)父節(jié)點(diǎn)

  /**
   * ?? 更新相關(guān)
   */
   pendingProps: any,  // 新的癌椿、待處理的props
   updateQueue: UpdateQueue<any> | null,  // 該Fiber對(duì)應(yīng)的組件產(chǎn)生的Update會(huì)存放在這個(gè)隊(duì)列里面
   memoizedProps: any,  // 上一次渲染完成之后的props
   memoizedState: any, // 上一次渲染的時(shí)候的state
  /**
   * ??  Effect 相關(guān)的
   */
// 和節(jié)點(diǎn)關(guān)系一樣,React 同樣使用鏈表來將所有有副作用的Fiber連接起來
  effectTag: SideEffectTag<number>, //當(dāng)前節(jié)點(diǎn)的副作用類型菱阵,例如節(jié)點(diǎn)更新如失、刪除、移動(dòng)
  nextEffect: Fiber | null, // 單鏈表用來快速查找下一個(gè)side effect
  firstEffect: Fiber | null,  // 子樹中第一個(gè)side effect
  lastEffect: Fiber | null, // 子樹中最后一個(gè)side effect
}
/**
   * ?? 替身
   */
 // 在Fiber樹更新的過程中送粱,每個(gè)Fiber都會(huì)有一個(gè)跟其對(duì)應(yīng)的Fiber
  // 我們稱他為`current <==> workInProgress`
  // 在渲染完成之后他們會(huì)交換位置
   alternate: Fiber | null,  // WIP 樹里面的 fiber,如果不在更新期間掂之,那么就等于當(dāng)前的 fiber抗俄,如果是新創(chuàng)建的節(jié)點(diǎn),那么就沒有 

Fiber 包含的屬性可以劃分為 5 個(gè)部分:

  • ?? 節(jié)點(diǎn)類型信息 - 這個(gè)也容易理解世舰,tag表示節(jié)點(diǎn)的分類动雹、type 保存具體的類型值,如div跟压、MyComp
  • ?? 結(jié)構(gòu)信息 - 這個(gè)上文我們已經(jīng)見過了胰蝠,F(xiàn)iber 使用鏈表的形式來表示節(jié)點(diǎn)在樹中的定位,每一個(gè) Fiber Node 節(jié)點(diǎn)與 Virtual Dom 一一對(duì)應(yīng)
  • ?? 更新相關(guān) - 節(jié)點(diǎn)的組件實(shí)例、props、state等茸塞,它們將影響組件的輸出
  • ?? Effect 相關(guān)的 - 這個(gè)也是新東西. 在 Reconciliation 過程中發(fā)現(xiàn)的'副作用'(變更需求)就保存在節(jié)點(diǎn)的effectTag 中(想象為打上一個(gè)標(biāo)記).
    那么怎么將本次渲染的所有節(jié)點(diǎn)副作用都收集起來呢躲庄? 這里也使用了鏈表結(jié)構(gòu),在遍歷過程中React會(huì)將所有有‘副作用’的節(jié)點(diǎn)都通過nextEffect連接起來
  • ?? 替身 - Fiber在update的時(shí)候钾虐,會(huì)從原來的Fiber(我們稱為current)clone出一個(gè)新的Fiber(我們稱為alternate)噪窘。兩個(gè)Fiber diff出的變化(side effect)記錄在alternate上。所以一個(gè)組件在更新時(shí)最多會(huì)有兩個(gè)Fiber與其對(duì)應(yīng)效扫,在更新結(jié)束后alternate會(huì)取代之前的current的成為新的current節(jié)點(diǎn)倔监。
Fiber類型

上一小節(jié),F(xiàn)iber對(duì)象中有個(gè)tag屬性菌仁,標(biāo)記fiber類型浩习,而fiber實(shí)例是和組件對(duì)應(yīng)的,所以其類型基本上對(duì)應(yīng)于組件類型济丘,源碼見ReactTypeOfWork模塊

export  const  IndeterminateComponent  =  0;  // 尚不知是類組件還是函數(shù)式組件 
export const FunctionalComponent = 1; // 函數(shù)式組件
export const ClassComponent = 2; // Class類組件
export const HostRoot = 3; // 組件樹根組件谱秽,可以嵌套
export const HostPortal = 4; // 子樹. Could be an entry point to a different renderer.
export const HostComponent = 5; // 標(biāo)準(zhǔn)組件,如地div闪盔, span等 
export const HostText = 6; // 文本
export const CallComponent = 7; // 組件調(diào)用
export const CallHandlerPhase = 8; // 調(diào)用組件方法
export const ReturnComponent = 9; // placeholder(占位符)
export const Fragment = 10; // 片段

在調(diào)度執(zhí)行任務(wù)的時(shí)候會(huì)根據(jù)不同類型fiber弯院,即fiber.tag值進(jìn)行不同處理。


任務(wù)如何分片及分片的優(yōu)先級(jí)

任務(wù)分片泪掀,或者叫工作單元(work unit)听绳,是怎么拆分的呢。因?yàn)樵赗econciliation階段任務(wù)分片可以被打斷异赫,用來執(zhí)行優(yōu)先級(jí)高的任務(wù)椅挣。如何拆分一個(gè)任務(wù)就很重要了。

為了達(dá)到任務(wù)分片的效果塔拳,就需要有一個(gè)調(diào)度器 (Scheduler) 來進(jìn)行任務(wù)分配鼠证。任務(wù)的優(yōu)先級(jí)有六種:

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
module.exports = {
    synchronous,//0,synchronous首屏(首次渲染)用靠抑,要求盡量快量九,不管會(huì)不會(huì)阻塞UI線程
    task,//1,在next tick之前執(zhí)行
    animation颂碧,//2,animation通過requestAnimationFrame來調(diào)度荠列,這樣在下一幀就能立即開始動(dòng)畫過程                                                                                                                                                                               
    high,//3,在不久的將來立即執(zhí)行
    low载城,//4,稍微延遲執(zhí)行也沒關(guān)系
    offscreen肌似,//5,下一次render時(shí)或scroll時(shí)才執(zhí)行
}

也就是說,(不考慮突發(fā)事件的)正常調(diào)度是由工作循環(huán)來完成的,基本規(guī)則是:每個(gè)工作單元結(jié)束檢查是否還有時(shí)間做下一個(gè),沒時(shí)間了就先“掛起”

優(yōu)先級(jí)機(jī)制用來處理突發(fā)事件與優(yōu)化次序,例如:

  • 到commit階段了雀哨,提高優(yōu)先級(jí)
  • 高優(yōu)任務(wù)做一半出錯(cuò)了固额,給降一下優(yōu)先級(jí)
  • 抽空關(guān)注一下低優(yōu)任務(wù)眠蚂,別給餓死了
  • 如果對(duì)應(yīng)DOM節(jié)點(diǎn)此刻不可見,給降到最低優(yōu)先級(jí)

這些策略用來動(dòng)態(tài)調(diào)整任務(wù)調(diào)度对雪,是工作循環(huán)的輔助機(jī)制河狐,最先做最重要的事情

任務(wù)調(diào)度的過程是:

在任務(wù)隊(duì)列中選出高優(yōu)先級(jí)的fiber node執(zhí)行,調(diào)用requestIdleCallback獲取所剩時(shí)間瑟捣,若執(zhí)行時(shí)間超過了deathLine馋艺,或者突然插入更高優(yōu)先級(jí)的任務(wù),則執(zhí)行中斷迈套,保存當(dāng)前結(jié)果捐祠,修改fiber node 的tag標(biāo)記,設(shè)置為pending狀態(tài)桑李,迅速收尾并再調(diào)用一個(gè)requestIdleCallback踱蛀,等主線程釋放出來再繼續(xù)
恢復(fù)任務(wù)執(zhí)行時(shí),檢查tag是被中斷的任務(wù)贵白,會(huì)接著繼續(xù)做任務(wù)或者重做


Fiber Tree

React 在 render 第一次渲染時(shí)率拒,會(huì)通過 React.createElement 創(chuàng)建一顆 Element 樹,可以稱之為 Virtual DOM Tree禁荒,由于要記錄上下文信息猬膨,加入了 Fiber,每一個(gè) Element 會(huì)對(duì)應(yīng)一個(gè) Fiber Node呛伴,將 Fiber Node 鏈接起來的結(jié)構(gòu)成為 Fiber Tree勃痴。它反映了用于渲染 UI 的應(yīng)用程序的狀態(tài)。這棵樹通常被稱為 current 樹(當(dāng)前樹热康,記錄當(dāng)前頁面的狀態(tài))沛申。

Fiber Tree 一個(gè)重要的特點(diǎn)是鏈表結(jié)構(gòu),將遞歸遍歷編程循環(huán)遍歷姐军,然后配合 requestIdleCallback API, 實(shí)現(xiàn)任務(wù)拆分铁材、中斷與恢復(fù)。

這個(gè)鏈接的結(jié)構(gòu)是怎么構(gòu)成的呢奕锌,這就要主要到之前 Fiber Node 的節(jié)點(diǎn)的這幾個(gè)字段:

// 單鏈表樹結(jié)構(gòu)
{
   return: Fiber | null, // 指向父節(jié)點(diǎn)
   child: Fiber | null,// 指向自己的第一個(gè)子節(jié)點(diǎn)
   sibling: Fiber | null,// 指向自己的兄弟結(jié)構(gòu)衫贬,兄弟節(jié)點(diǎn)的return指向同一個(gè)父節(jié)點(diǎn)
}

每一個(gè) Fiber Node 節(jié)點(diǎn)與 Virtual Dom 一一對(duì)應(yīng),所有 Fiber Node 連接起來形成 Fiber tree, 是個(gè)單鏈表樹結(jié)構(gòu)歇攻,因?yàn)槭褂昧随湵斫Y(jié)構(gòu),即使處理流程被中斷了梆造,我們隨時(shí)可以從上次未處理完的Fiber繼續(xù)遍歷下去缴守。如下圖所示:

image.png

比如你在text(hello)中斷了葬毫,那么下一次就會(huì)從 p 節(jié)點(diǎn)開始處理

這個(gè)數(shù)據(jù)結(jié)構(gòu)調(diào)整還有一個(gè)好處,就是某些節(jié)點(diǎn)異常時(shí)屡穗,我們可以打印出完整的’節(jié)點(diǎn)椞瘢‘,只需要沿著節(jié)點(diǎn)的return回溯即可村砂。


Side Effect(副作用)

我們可以將 React 中的一個(gè)組件視為一個(gè)使用 state 和 props 來計(jì)算 UI 表示的函數(shù)烂斋。其他所有活動(dòng),如改變 DOM 或調(diào)用生命周期方法础废,都應(yīng)該被視為副作用汛骂,或者簡單地說是一種效果。文檔中 是這樣描述的:

您之前可能已經(jīng)在 React 組件中執(zhí)行數(shù)據(jù)提取评腺,訂閱或手動(dòng)更改 DOM帘瞭。我們將這些操作稱為“副作用”(或簡稱為“效果”),因?yàn)樗鼈儠?huì)影響其他組件蒿讥,并且在渲染過程中無法完成蝶念。

您可以看到大多 state 和 props 更新都會(huì)導(dǎo)致副作用。既然使用副作用是工作(活動(dòng))的一種類型芋绸,F(xiàn)iber 節(jié)點(diǎn)是一種方便的機(jī)制來跟蹤除了更新以外的效果媒殉。每個(gè) Fiber 節(jié)點(diǎn)都可以具有與之相關(guān)的副作用,它們可在 effectTag 字段中編碼摔敛。

因此廷蓉,F(xiàn)iber 中的副作用基本上定義了處理更新后需要為實(shí)例完成的 工作。對(duì)于宿主組件(DOM 元素)舷夺,所謂的工作包括添加苦酱,更新或刪除元素。對(duì)于類組件给猾,React可能需要更新 refs 并調(diào)用 componentDidMountcomponentDidUpdate 生命周期方法疫萤。對(duì)于其他類型的 Fiber ,還有相對(duì)應(yīng)的其他副作用敢伸。

Effects List

React 處理更新的素對(duì)非常迅速扯饶,為了達(dá)到這種水平的性能,它采用了一些有趣的技術(shù)池颈。其中之一是構(gòu)建具有副作用的 Fiber 節(jié)點(diǎn)的線性列表尾序,從而能夠快速遍歷。遍歷線性列表比樹快得多躯砰,并且沒有必要在沒有副作用的節(jié)點(diǎn)上花費(fèi)時(shí)間每币。

此列表的目標(biāo)是標(biāo)記具有 DOM 更新或其他相關(guān)副作用的節(jié)點(diǎn)。此列表是 finishedWork 樹的子集琢歇,并使用 nextEffect 屬性而不是 currentworkInProgress 樹中使用的 child 屬性進(jìn)行鏈接兰怠。

Dan Abramov 為副作用列表提供了一個(gè)類比梦鉴。他喜歡將它想象成一棵圣誕樹,「圣誕燈」將所有有效節(jié)點(diǎn)捆綁在一起揭保。為了使這個(gè)可視化肥橙,讓我們想象如下的 Fiber 節(jié)點(diǎn)樹,其中標(biāo)亮的節(jié)點(diǎn)有一些要做的工作秸侣。例如存筏,我們的更新導(dǎo)致 c2 被插入到 DOM 中,d2c1 被用于更改屬性味榛,而 b2 被用于觸發(fā)生命周期方法椭坚。副作用列表會(huì)將它們鏈接在一起,以便 React 稍后可以跳過其他節(jié)點(diǎn):

image.png

可以看到具有副作用的節(jié)點(diǎn)是如何鏈接在一起的励负。當(dāng)遍歷節(jié)點(diǎn)時(shí)藕溅,React 使用 firstEffect 指針來確定列表的開始位置。所以上面的圖表可以表示為這樣的線性列表:

image.png

如您所見继榆,React 按照從子到父的順序應(yīng)用副作用巾表。


react Fiber是如何工作的

舉個(gè)例子
export class List extend React.component {
    render() {
        return (
            <div>
               <button value="平方" />
               <button value="字體" />
               <Item item={1}/>
               <Item item={2}/>
               <Item item={3}/>
            </div>
        )
    }
}
export class Item extend React.component {
    render() {
        return (
            <div>
                {this.props.item}
            </div>
        )
    }
}
export class Home extend React.component<HomeProps, any> {
    componentWillReceiveProps(nextProps: HomeProps) {}
    componentDidMount() {}
    componentDidUpdate() {}
    componentWillUnmount() {}
    .....
    render() {
        return (
            <div>
              <List/>
            </div>
        )
    }
}
ReactDom.render(<Home />, document.querySelector(selectors: '#hostRoot'))

當(dāng)前頁面包含一個(gè)列表,通過該列表渲染出一個(gè)button和一組Item略吨,Item中包含一個(gè)div集币,其中的內(nèi)容為數(shù)字。通過點(diǎn)擊button翠忠,可以使列表中的所有數(shù)字進(jìn)行平方鞠苟。另外有一個(gè)按鈕,點(diǎn)擊可以調(diào)節(jié)字體大小秽之。


image.png

頁面渲染完成后当娱,就會(huì)初始化生成一個(gè)fiber-tree。初始化fiber-tree和初始化Virtual DOM tree沒什么區(qū)別考榨,這里就不再贅述跨细。

image.png

于此同時(shí),react還會(huì)維護(hù)一個(gè)workInProgressTree河质。workInProgressTree用于計(jì)算更新冀惭,完成reconciliation過程。

image.png

用戶點(diǎn)擊平方按鈕后掀鹅,利用各個(gè)元素平方后的list調(diào)用setState散休,react會(huì)把當(dāng)前的更新送入list組件對(duì)應(yīng)的update queue中。但是react并不會(huì)立即執(zhí)行對(duì)比并修改DOM的操作乐尊。而是交給scheduler去處理戚丸。

image.png

scheduler會(huì)根據(jù)當(dāng)前主線程的使用情況去處理這次update。為了實(shí)現(xiàn)這種特性扔嵌,使用了requestIdelCallbackAPI昏滴。對(duì)于不支持這個(gè)API的瀏覽器猴鲫,react會(huì)加上pollyfill。

總的來講谣殊,通常,客戶端線程執(zhí)行任務(wù)時(shí)會(huì)以幀的形式劃分牺弄,大部分設(shè)備控制在30-60幀是不會(huì)影響用戶體驗(yàn)姻几;在兩個(gè)執(zhí)行幀之間,主線程通常會(huì)有一小段空閑時(shí)間势告,requestIdleCallback可以在這個(gè)空閑期(Idle Period)調(diào)用空閑期回調(diào)(Idle Callback)蛇捌,執(zhí)行一些任務(wù)

image.png
  1. 低優(yōu)先級(jí)任務(wù)由requestIdleCallback處理;
  2. 高優(yōu)先級(jí)任務(wù)咱台,如動(dòng)畫相關(guān)的由requestAnimationFrame處理络拌;
  3. requestIdleCallback可以在多個(gè)空閑期調(diào)用空閑期回調(diào),執(zhí)行任務(wù)回溺;
  4. requestIdleCallback方法提供deadline春贸,即任務(wù)執(zhí)行限制時(shí)間,以切分任務(wù)遗遵,避免長時(shí)間執(zhí)行萍恕,阻塞UI渲染而導(dǎo)致掉幀;

image.png

image.png

一旦reconciliation過程得到時(shí)間片车要,就開始進(jìn)入work loop允粤。work loop機(jī)制可以讓react在計(jì)算狀態(tài)和等待狀態(tài)之間進(jìn)行切換。為了達(dá)到這個(gè)目的翼岁,對(duì)于每個(gè)loop而言类垫,需要追蹤兩個(gè)東西:下一個(gè)工作單元(下一個(gè)待處理的fiber);當(dāng)前還能占用主線程的時(shí)間。第一個(gè)loop琅坡,下一個(gè)待處理單元為根節(jié)點(diǎn)悉患。

image.png

因?yàn)楦?jié)點(diǎn)上的更新隊(duì)列為空,所以直接從fiber-tree上將根節(jié)點(diǎn)復(fù)制到workInProgressTree中去脑蠕。根節(jié)點(diǎn)中包含指向子節(jié)點(diǎn)(List)的指針购撼。

image.png

根節(jié)點(diǎn)沒有什么更新操作,根據(jù)其child指針谴仙,接下來把List節(jié)點(diǎn)及其對(duì)應(yīng)的update queue也復(fù)制到workinprogress中迂求。List插入后,向其父節(jié)點(diǎn)返回晃跺,標(biāo)志根節(jié)點(diǎn)的處理完成揩局。

image.png

根節(jié)點(diǎn)處理完成后,react此時(shí)檢查時(shí)間片是否用完掀虎。如果沒有用完凌盯,根據(jù)其保存的下個(gè)工作單元的信息開始處理下一個(gè)節(jié)點(diǎn)List付枫。

image.png

接下來進(jìn)入處理List的work loop,List中包含更新驰怎,因此此時(shí)react會(huì)調(diào)用setState時(shí)傳入的updater funciton獲取最新的state值阐滩,此時(shí)應(yīng)該是[1,4,9]。通常我們現(xiàn)在在調(diào)用setState傳入的是一個(gè)對(duì)象县忌,但在使用fiber conciler時(shí)掂榔,必須傳入一個(gè)函數(shù),函數(shù)的返回值是要更新的state症杏。react從很早的版本就開始支持這種寫法了装获,不過通常沒有人用。在之后的react版本中厉颤,可能會(huì)廢棄直接傳入對(duì)象的寫法穴豫。

setState({}, callback); // stack conciler
setState(() => { return {} }, callback); // fiber conciler
復(fù)制代碼

在獲取到最新的state值后,react會(huì)更新List的state和props值逼友,然后調(diào)用render精肃,然后得到一組通過更新后的list值生成的elements。react會(huì)根據(jù)生成elements的類型翁逞,來決定fiber是否可重用肋杖。對(duì)于當(dāng)前情況來說,新生成的elments類型并沒有變(依然是Button和Item)挖函,所以react會(huì)直接從fiber-tree中復(fù)制這些elements對(duì)應(yīng)的fiber到workInProgress 中状植。并給List打上標(biāo)簽,因?yàn)檫@是一個(gè)需要更新的節(jié)點(diǎn)怨喘。

image.png

List節(jié)點(diǎn)處理完成津畸,react仍然會(huì)檢查當(dāng)前時(shí)間片是否夠用。如果夠用則處理下一個(gè)必怜,也就是button肉拓。加入這個(gè)時(shí)候,用戶點(diǎn)擊了放大字體的按鈕梳庆。這個(gè)放大字體的操作暖途,純粹由js實(shí)現(xiàn),跟react無關(guān)膏执。但是操作并不能立即生效驻售,因?yàn)閞eact的時(shí)間片還未用完,因此接下來仍然要繼續(xù)處理button更米。

image.png

button沒有任何子節(jié)點(diǎn)欺栗,所以此時(shí)可以返回,并標(biāo)志button處理完成。如果button有改變迟几,需要打上tag消请,但是當(dāng)前情況沒有,只需要標(biāo)記完成即可类腮。

image.png

老規(guī)矩臊泰,處理完一個(gè)節(jié)點(diǎn)先看時(shí)間夠不夠用。注意這里放大字體的操作已經(jīng)在等候釋放主線程了蚜枢。

image.png

接下來處理第一個(gè)item因宇。通過shouldComponentUpdate鉤子可以根據(jù)傳入的props判斷其是否需要改變。對(duì)于第一個(gè)Item而言祟偷,更改前后都是1,所以不會(huì)改變,shouldComponentUpdate返回false打厘,復(fù)制div修肠,處理完成,檢查時(shí)間户盯,如果還有時(shí)間進(jìn)入第二個(gè)Item嵌施。

第二個(gè)Item shouldComponentUpdate返回true,所以需要打上tag莽鸭,標(biāo)志需要更新吗伤,復(fù)制div,調(diào)用render硫眨,講div中的內(nèi)容從2更新為4足淆,因?yàn)閐iv有更新,所以標(biāo)記div礁阁。當(dāng)前節(jié)點(diǎn)處理完成巧号。

image.png

對(duì)于上面這種情況,div已經(jīng)是葉子節(jié)點(diǎn)姥闭,且沒有任何兄弟節(jié)點(diǎn)丹鸿,且其值已經(jīng)更新,這時(shí)候棚品,需要將此節(jié)點(diǎn)改變產(chǎn)生的effect合并到父節(jié)點(diǎn)中靠欢。此時(shí)react會(huì)維護(hù)一個(gè)列表,其中記錄所有產(chǎn)生effect的元素铜跑。

image.png

合并后门怪,回到父節(jié)點(diǎn)Item,父節(jié)點(diǎn)標(biāo)記完成疼进。

image.png

下一個(gè)工作單元是Item薪缆,在進(jìn)入Item之前,檢查時(shí)間。但這個(gè)時(shí)候時(shí)間用完了拣帽。此時(shí)react必須交換主線程疼电,并告訴主線程以后要為其分配時(shí)間以完成剩下的操作。

image.png

主線程接下來進(jìn)行放大字體的操作减拭。完成后執(zhí)行react接下來的操作蔽豺,跟上一個(gè)Item的處理流程幾乎一樣,處理完成后整個(gè)fiber-tree和workInProgress如下:

image.png

完成后拧粪,Item向List返回并merge effect修陡,effect List現(xiàn)在如下所示:

image.png

此時(shí)List向根節(jié)點(diǎn)返回并merge effect,所有節(jié)點(diǎn)都可以標(biāo)記完成了可霎。此時(shí)react將workInProgress標(biāo)記為pendingCommit魄鸦。意思是可以進(jìn)入commit階段了。

image.png

此時(shí)癣朗,要做的是還是檢查時(shí)間夠不夠用拾因,如果沒有時(shí)間,會(huì)等到時(shí)間再去提交修改到DOM旷余。進(jìn)入到階段2后绢记,reacDOM會(huì)根據(jù)階段1計(jì)算出來的effect-list來更新DOM。

更新完DOM之后正卧,workInProgress就完全和DOM保持一致了堂油,為了讓當(dāng)前的fiber-tree和DOM保持一直咆霜,react交換了current和workinProgress兩個(gè)指針。

image.png

事實(shí)上,react大部分時(shí)間都在維持兩個(gè)樹(Double-buffering)照激。這可以縮減下次更新時(shí)测蹲,分配內(nèi)存矫户、垃圾清理的時(shí)間离斩。commit完成后,執(zhí)行componentDidMount函數(shù)抽高。


下面是一個(gè)詳細(xì)的執(zhí)行過程圖:

image.png
  • ?? 1.第一部分從 用戶操作引起setState被調(diào)用以后判耕,把接收的 React Element 轉(zhuǎn)換為 Fiber 節(jié)點(diǎn),并為其設(shè)置優(yōu)先級(jí)翘骂,創(chuàng)建 Update壁熄,根據(jù)Fiber的優(yōu)先級(jí)加入到Update相應(yīng)的位置,這部分主要是做一些初始數(shù)據(jù)的準(zhǔn)備碳竟。
  • ?? 2.第二部分主要是三個(gè)函數(shù):scheduleWork草丧、requestWorkperformWork莹桅,即調(diào)度工作昌执、申請(qǐng)工作、正式工作三部曲,React 16 新增的異步調(diào)度的功能則在這部分實(shí)現(xiàn)懂拾,這部分就是 Schedule 階段煤禽,完成調(diào)度主要靠scheduleCallbackWithExpriation這個(gè)方法。scheduleCallbackWithExpriation這個(gè)方法在不同環(huán)境岖赋,實(shí)現(xiàn)不一樣檬果,chrome等覽器中使用requestIdleCallback API,沒有這個(gè)API的瀏覽器中唐断,通過requestAnimationFrame模擬一個(gè)requestIdleCallback选脊,任務(wù)調(diào)度的過程是:在任務(wù)隊(duì)列中選出高優(yōu)先級(jí)的fiber node執(zhí)行,調(diào)用requestIdleCallback獲取所剩時(shí)間脸甘,若執(zhí)行時(shí)間超過了deathLine恳啥,或者突然插入更高優(yōu)先級(jí)的任務(wù),則執(zhí)行中斷丹诀,保存當(dāng)前結(jié)果角寸,修改tag標(biāo)記一下,設(shè)置為pending狀態(tài)忿墅,迅速收尾并再調(diào)用一個(gè)requestIdleCallback,等主線程釋放出來再繼續(xù)沮峡。執(zhí)行到performWorkOnRoot時(shí)疚脐,第二部分結(jié)束。
  • ?? 3.第三部分基本就是 Fiber Reconciler 邢疙,分為2個(gè)階段:第一階段Render/recocilation Phase棍弄,遍歷所有的 Fiber 節(jié)點(diǎn),通過 Diff 算法計(jì)算所有更新工作疟游,產(chǎn)出 EffectList 給到 commit Phase使用呼畸,這部分的核心是 beginWork 函數(shù);然后進(jìn)入Commit Phase颁虐,這個(gè)階段不能被打斷蛮原,不再贅述。下一節(jié)將著重講這兩個(gè)階段另绩。

Reconciliation Phase(協(xié)調(diào)階段)

Reconciliation Phase階段以fiber tree為藍(lán)本儒陨,把每個(gè)fiber作為一個(gè)工作單元,自頂向下逐節(jié)點(diǎn)構(gòu)造workInProgress tree(構(gòu)建中的新fiber tree)笋籽,具體過程如下(以組件節(jié)點(diǎn)為例):

  • ??1.找到高優(yōu)先級(jí)的待處理的節(jié)點(diǎn)

  • ?? 2.如果當(dāng)前節(jié)點(diǎn)不需要更新蹦漠,直接把子節(jié)點(diǎn)clone過來,跳到5车海;要更新的話打個(gè)tag

  • ?? 3.打個(gè)tag標(biāo)記笛园,更新當(dāng)前節(jié)點(diǎn)狀態(tài)(組件更新props,context等,DOM節(jié)點(diǎn)記下DOM change)

  • ?? 4.組件節(jié)點(diǎn)的話研铆,調(diào)用shouldComponentUpdate()埋同,false的話,跳到5

  • ?? 5.調(diào)用render()獲得新的子節(jié)點(diǎn)蚜印,生成子節(jié)點(diǎn)的workInProgress節(jié)點(diǎn)(創(chuàng)建過程有alternate的用alternate,沒有的復(fù)用子節(jié)點(diǎn)莺禁,子節(jié)點(diǎn)增刪也發(fā)生在這里)

  • ?? 6.如果沒有產(chǎn)生child fiber,該工作單元結(jié)束窄赋,把effect list歸并到父節(jié)點(diǎn)哟冬,并把當(dāng)前節(jié)點(diǎn)的sibling作為下一個(gè)工作單元;否則把當(dāng)前節(jié)點(diǎn)的child fiber作為下一個(gè)工作單元

  • ?? 7.如果沒有剩余可用時(shí)間了忆绰,等到下一次主線程空閑時(shí)才開始下一個(gè)工作單元浩峡;否則,立即開始做

  • ?? 8.如果沒有下一個(gè)工作單元了(回到了workInProgress tree的根節(jié)點(diǎn))错敢,第1階段結(jié)束翰灾,進(jìn)入pendingCommit狀態(tài)

實(shí)際上1-7是Reconciliation階段的工作循環(huán),下一節(jié)講重點(diǎn)講稚茅。7是Reconciliation階段的出口纸淮,工作循環(huán)每次只做一件事,做完看要不要喘口氣亚享。工作循環(huán)結(jié)束時(shí)咽块,workInProgress tree的根節(jié)點(diǎn)身上的effect list就是收集到的所有side effect(因?yàn)槊孔鐾暌粋€(gè)都向上歸并)

alternate、current Tree及 workInProgress Tree的關(guān)系

在第一次渲染之后欺税,React 最終得到一個(gè) Fiber 樹侈沪,它反映了用于渲染 UI 的應(yīng)用程序的狀態(tài)。這棵樹通常被稱為 current 樹(當(dāng)前樹)晚凿。當(dāng) React 開始處理更新時(shí)亭罪,它會(huì)構(gòu)建一個(gè)所謂的workInProgress tree(工作進(jìn)度樹)workInProgress tree是reconcile過程中從fiber tree建立的當(dāng)前進(jìn)度快照,用于斷點(diǎn)恢復(fù)歼秽。

Fiber在update的時(shí)候应役,會(huì)從原來的Fiber(我們稱為current)clone出一個(gè)新的Fiber(我們稱為alternate)。兩個(gè)Fiber diff出的變化(side effect)記錄在alternate上燥筷。所以一個(gè)組件在更新時(shí)最多會(huì)有兩個(gè)Fiber與其對(duì)應(yīng)扛吞,在更新結(jié)束后alternate會(huì)取代之前的current的成為新的current節(jié)點(diǎn)。

所有工作都在 workInProgress 樹的 Fiber 節(jié)點(diǎn)上執(zhí)行荆责。當(dāng) React 遍歷 current 樹時(shí)滥比,對(duì)于每個(gè)現(xiàn)有 Fiber 節(jié)點(diǎn),React 會(huì)創(chuàng)建一個(gè)構(gòu)成 workInProgress 樹的備用節(jié)點(diǎn)做院,這一節(jié)點(diǎn)會(huì)使用 render 方法返回的 React 元素中的數(shù)據(jù)來創(chuàng)建盲泛。處理完更新并完成所有相關(guān)工作后濒持,React 將準(zhǔn)備好一個(gè)備用樹以刷新到屏幕。一旦這個(gè) workInProgress 樹在屏幕上呈現(xiàn)寺滚,它就會(huì)變成 current 樹柑营。

React 的核心原則之一是一致性。 React 總是一次性更新 DOM - 它不會(huì)顯示部分中間結(jié)果村视。workInProgress 樹充當(dāng)用戶不可見的「草稿」官套,這樣 React 可以先處理所有組件,然后將其更改刷新到屏幕蚁孔。

在源代碼中奶赔,您將看到很多函數(shù)從 currentworkInProgress 樹中獲取 Fiber 節(jié)點(diǎn)。這是一個(gè)這類函數(shù)的簽名:

function updateHostComponent(current, workInProgress, renderExpirationTime) {...}

每個(gè)Fiber節(jié)點(diǎn)持有備用域在另一個(gè)樹的對(duì)應(yīng)部分的引用杠氢。來自 current 樹中的節(jié)點(diǎn)會(huì)指向 workInProgress 樹中的節(jié)點(diǎn)站刑,反之亦然。


工作循環(huán)的主要步驟

舉個(gè)例子:

React.Component.prototype.setState = function( partialState, callback ) {
  updateQueue.pus( {
    stateNode: this,
    partialState: partialState
  } );
  requestIdleCallback(performWork); // 這里就開始干活了
}

function performWork(deadline) {
  workLoop(deadline)
  if (nextUnitOfWork || updateQueue.length > 0) {
    requestIdleCallback(performWork) //繼續(xù)干
  }
}

setState先把此次更新放到更新隊(duì)列 updateQueue 里面鼻百,然后調(diào)用調(diào)度器開始做更新任務(wù)绞旅。performWork 先調(diào)用 workLoop 對(duì) fiber 樹進(jìn)行遍歷比較,就是我們上面提到的遍歷過程温艇。當(dāng)此次時(shí)間片時(shí)間不夠遍歷完整個(gè) fiber 樹因悲,或者遍歷并比較完之后workLoop 函數(shù)結(jié)束。接下來我們判斷下 fiber 樹是否遍歷完或者更新隊(duì)列 updateQueue 是否還有待更新的任務(wù)勺爱。如果有則調(diào)用 requestIdleCallback 在下個(gè)時(shí)間片繼續(xù)干活囤捻。nextUnitOfWork 是個(gè)全局變量,記錄 workLoop 遍歷 fiber 樹中斷在哪個(gè)節(jié)點(diǎn)邻寿。

所有的 Fiber 節(jié)點(diǎn)都會(huì)在 工作循環(huán) 中進(jìn)行處理。如下是該循環(huán)的同步部分的實(shí)現(xiàn):

function workLoop(deadline) {
  if (!nextUnitOfWork) {
    //一個(gè)周期內(nèi)只創(chuàng)建一次
    nextUnitOfWork = createWorkInProgress(updateQueue)
  }

  while (nextUnitOfWork && deadline.timeRemaining() > EXPIRATION_TIME) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
  }

  if (pendingCommit) {
    //當(dāng)全局 pendingCommit 變量被負(fù)值
    commitAllwork(pendingCommit)
  }
}

剛開始遍歷的時(shí)候判斷全局變量 nextUnitOfWork 是否存在视哑?如果存在表示上次任務(wù)中斷了绣否,我們繼續(xù),如果不存在我們就從更新隊(duì)列里面取第一個(gè)任務(wù)挡毅,并生成對(duì)應(yīng)的 fiber 根節(jié)點(diǎn)蒜撮。接下來我們就是正式的工作了,用循環(huán)從某個(gè)節(jié)點(diǎn)開始遍歷 fiber 樹跪呈。performUnitOfWork 根據(jù)我們上面提到的遍歷規(guī)則段磨,在對(duì)當(dāng)前節(jié)點(diǎn)處理完之后,返回下一個(gè)需要遍歷的節(jié)點(diǎn)耗绿。循環(huán)除了要判斷是否有下一個(gè)節(jié)點(diǎn)(是否遍歷完)苹支,還要判斷當(dāng)前給你的時(shí)間是否用完,如果用完了則需要返回误阻,讓瀏覽器響應(yīng)用戶的交互事件债蜜,然后再在下個(gè)時(shí)間片繼續(xù)晴埂。workLoop 最后一步判斷全局變量 pendingCommit 是否存在,如果存在則把這次遍歷 fiber 樹產(chǎn)生的所有更新一次更新到真實(shí)的 dom 上去寻定。注意 pendingCommit 在完成一次完整的遍歷過程之前是不會(huì)有值的儒洛。

遍歷樹、初始化或完成工作主要用到 4 個(gè)函數(shù):

1.workLoop階段

構(gòu)建workInProgress tree的過程就是diff的過程狼速,對(duì) Fiber tree前后進(jìn)行比對(duì)主要是beginWork琅锻,源碼如下:

function beginWork(fiber: Fiber): Fiber | undefined {
  if (fiber.tag === WorkTag.HostComponent) {
    // 宿主節(jié)點(diǎn)diff
    diffHostComponent(fiber)
  } else if (fiber.tag === WorkTag.ClassComponent) {
    // 類組件節(jié)點(diǎn)diff
    diffClassComponent(fiber)
  } else if (fiber.tag === WorkTag.FunctionComponent) {
    // 函數(shù)組件節(jié)點(diǎn)diff
    diffFunctionalComponent(fiber)
  } else {
    // ... 其他類型節(jié)點(diǎn),省略
  }
}

宿主節(jié)點(diǎn)比對(duì):

function diffHostComponent(fiber: Fiber) {
  // 新增節(jié)點(diǎn)
  if (fiber.stateNode == null) {
    fiber.stateNode = createHostComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }

  const newChildren = fiber.pendingProps.children;

  // 比對(duì)子節(jié)點(diǎn)
  diffChildren(fiber, newChildren);
}

類組件節(jié)點(diǎn)比對(duì)也差不多:

function diffClassComponent(fiber: Fiber) {
  // 創(chuàng)建組件實(shí)例
  if (fiber.stateNode == null) {
    fiber.stateNode = createInstance(fiber);
  }

  if (fiber.hasMounted) {
    // 調(diào)用更新前生命周期鉤子
    applybeforeUpdateHooks(fiber)
  } else {
    // 調(diào)用掛載前生命周期鉤子
    applybeforeMountHooks(fiber)
  }

  // 渲染新節(jié)點(diǎn)
  const newChildren = fiber.stateNode.render();
  // 比對(duì)子節(jié)點(diǎn)
  diffChildren(fiber, newChildren);

  fiber.memoizedState = fiber.stateNode.state
}

子節(jié)點(diǎn)比對(duì):

function diffChildren(fiber: Fiber, newChildren: React.ReactNode) {
  let oldFiber = fiber.alternate ? fiber.alternate.child : null;
  // 全新節(jié)點(diǎn)向胡,直接掛載
  if (oldFiber == null) {
    mountChildFibers(fiber, newChildren)
    return
  }

  let index = 0;
  let newFiber = null;
  // 新子節(jié)點(diǎn)
  const elements = extraElements(newChildren)

  // 比對(duì)子元素
  while (index < elements.length || oldFiber != null) {
    const prevFiber = newFiber;
    const element = elements[index]
    const sameType = isSameType(element, oldFiber)
    if (sameType) {
      newFiber = cloneFiber(oldFiber, element)
      // 更新關(guān)系
      newFiber.alternate = oldFiber
      // 打上Tag
      newFiber.effectTag = UPDATE
      newFiber.return = fiber
    }

    // 新節(jié)點(diǎn)
    if (element && !sameType) {
      newFiber = createFiber(element)
      newFiber.effectTag = PLACEMENT
      newFiber.return = fiber
    }

    // 刪除舊節(jié)點(diǎn)
    if (oldFiber && !sameType) {
      oldFiber.effectTag = DELETION;
      oldFiber.nextEffect = fiber.nextEffect
      fiber.nextEffect = oldFiber
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    if (index == 0) {
      fiber.child = newFiber;
    } else if (prevFiber && element) {
      prevFiber.sibling = newFiber;
    }

    index++
  }
}

function createWorkInProgress(updateQueue) {
  const updateTask = updateQueue.shift()
  if (!updateTask) return

  if (updateTask.partialState) {
    // 證明這是一個(gè)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
}

createWorkInProgress 拿出更新隊(duì)列 updateQueue 第一個(gè)任務(wù)恼蓬,然后看觸發(fā)這個(gè)任務(wù)的節(jié)點(diǎn)是什么類型。如果不是根節(jié)點(diǎn)捷枯,則通過循環(huán)迭代節(jié)點(diǎn)的 return 找到最上層的根節(jié)點(diǎn)滚秩。最后生成一個(gè)新的 fiber 節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)就是當(dāng)前 fiber 節(jié)點(diǎn)的 alternate 指向的淮捆,也就是說下面會(huì)在當(dāng)前節(jié)點(diǎn)和這個(gè)新生成的節(jié)點(diǎn)直接進(jìn)行 diff郁油。

function performUnitOfWork(workInProgress) {
  const nextChild = beginWork(workInProgress)
  if (nextChild) return nextChild

  // 沒有 nextChild, 我們看看這個(gè)節(jié)點(diǎn)有沒有 sibling
  let current = workInProgress
  while (current) {
    //收集當(dāng)前節(jié)點(diǎn)的effect,然后向上傳遞
    completeWork(current)
    if (current.sibling) return current.sibling
    //沒有 sibling攀痊,回到這個(gè)節(jié)點(diǎn)的父親桐腌,看看有沒有sibling
    current = current.return
  }
}

函數(shù) performUnitOfWorkworkInProgress 樹接收一個(gè) Fiber 節(jié)點(diǎn),并通過調(diào)用 beginWork 函數(shù)啟動(dòng)工作苟径。這個(gè)函數(shù)將啟動(dòng)所有 Fiber 執(zhí)行工作所需要的活動(dòng)案站。出于演示的目的,我們只 log 出 Fiber 節(jié)點(diǎn)的名稱來表示工作已經(jīng)完成棘街。函數(shù) beginWork 始終返回指向要在循環(huán)中處理的下一個(gè)子節(jié)點(diǎn)的指針或 null蟆盐。
如果有下一個(gè)子節(jié)點(diǎn),它將被賦值給 workLoop 函數(shù)中的變量 nextUnitOfWork遭殉。但是石挂,如果沒有子節(jié)點(diǎn),React 知道它到達(dá)了分支的末尾险污,因此它可以完成當(dāng)前節(jié)點(diǎn)痹愚。一旦節(jié)點(diǎn)完成,它將需要為同層的其他節(jié)點(diǎn)執(zhí)行工作蛔糯,并在完成后回溯到父節(jié)點(diǎn)拯腮。這是 completeUnitOfWork 函數(shù)執(zhí)行的代碼:

function completeUnitOfWork(workInProgress) {
    while (true) {
        let returnFiber = workInProgress.return;
        let siblingFiber = workInProgress.sibling;

        nextUnitOfWork = completeWork(workInProgress);

        if (siblingFiber !== null) {
            // If there is a sibling, return it
            // to perform work for this sibling
            return siblingFiber;
        } else if (returnFiber !== null) {
            // If there's no more work in this returnFiber,
            // continue the loop to complete the parent.
            workInProgress = returnFiber;
            continue;
        } else {
            // We've reached the root.
            return null;
        }
    }
}

你可以看到函數(shù)的核心就是一個(gè)大的 while 的循環(huán)。當(dāng) workInProgress 節(jié)點(diǎn)沒有子節(jié)點(diǎn)時(shí)蚁飒,React 會(huì)進(jìn)入此函數(shù)动壤。完成當(dāng)前 Fiber 節(jié)點(diǎn)的工作后,它就會(huì)檢查是否有同層節(jié)點(diǎn)淮逻。如果找的到狼电,React 退出該函數(shù)并返回指向該同層節(jié)點(diǎn)的指針蜒灰。它將被賦值給 nextUnitOfWork 變量,React將從這個(gè)節(jié)點(diǎn)開始執(zhí)行分支的工作肩碟。我們需要著重理解的是强窖,在當(dāng)前節(jié)點(diǎn)上,React 只完成了前面的同層節(jié)點(diǎn)的工作削祈。它尚未完成父節(jié)點(diǎn)的工作翅溺。只有在完成以子節(jié)點(diǎn)開始的所有分支后,才能完成父節(jié)點(diǎn)和回溯的工作髓抑。
從實(shí)現(xiàn)中可以看出咙崎,performUnitOfWorkcompleteUnitOfWork 主要用于迭代目的,而主要活動(dòng)則在beginWorkcompleteWork 函數(shù)中進(jìn)行吨拍。

function completeWork(fiber) {
  const parent = fiber.return

  // 到達(dá)頂端
  if (parent == null || fiber === topWork) {
    pendingCommit = fiber
    return
  }

  if (fiber.effectTag != null) {
    if (parent.nextEffect) {
      parent.nextEffect.nextEffect = fiber
    } else {
      parent.nextEffect = fiber
    }
  } else if (fiber.nextEffect) {
    parent.nextEffect = fiber.nextEffect
  }
}

2褪猛、 completeWork階段

completeWork的工作主要是通過新老節(jié)點(diǎn)的prop或tag等,收集節(jié)點(diǎn)的effect-list羹饰。然后向上一層一層循環(huán)伊滋,merge每個(gè)節(jié)點(diǎn)的effect-list,當(dāng)?shù)竭_(dá)根節(jié)點(diǎn)#hostRoot時(shí)队秩,節(jié)點(diǎn)上包含所有的effect-list笑旺。并把effect-list傳給pendingcommit,進(jìn)入commit階段馍资。

1203274-20180831173358256-1834919628.jpg

當(dāng)回溯完筒主,有了 pendingCommit,則 commitAllwork 會(huì)被調(diào)用鸟蟹。它做的工作就是循環(huán)遍歷根節(jié)點(diǎn)的 effets 數(shù)據(jù)乌妙,里面保存著所有要更新的內(nèi)容。commitWork 就是執(zhí)行具體更新的函數(shù)建钥,這里就不展開了(因?yàn)檫@篇主要想講的是 fiber 更新的調(diào)度算法)藤韵。

所以你們看遍歷 dom 數(shù) diff 的過程是可以被打斷并且在后續(xù)的時(shí)間片上接著干,只是最后一步 commitAllwork 是同步的不能打斷的锦针。這樣 react 使用新的調(diào)度算法優(yōu)化了更新過程中執(zhí)行時(shí)間過長導(dǎo)致的頁面卡頓現(xiàn)象。
接下來就是將所有打了 Effect 標(biāo)記的節(jié)點(diǎn)串聯(lián)起來置蜀,這個(gè)可以在completeWork中做, 例如:

completeWork
function completeWork(fiber) {
  const parent = fiber.return

  // 到達(dá)頂端
  if (parent == null || fiber === topWork) {
    pendingCommit = fiber
    return
  }

  if (fiber.effectTag != null) {
    if (parent.nextEffect) {
      parent.nextEffect.nextEffect = fiber
    } else {
      parent.nextEffect = fiber
    }
  } else if (fiber.nextEffect) {
    parent.nextEffect = fiber.nextEffect
  }
}

commitAllWork

function commitAllWork(fiber) {
  let next = fiber
  while(next) {
    if (fiber.effectTag) {
      // 提交奈搜,偷一下懶,這里就不展開了
      commitWork(fiber)
    }
    next = fiber.nextEffect
  }

  // 清理現(xiàn)場
  pendingCommit = nextUnitOfWork = topWork = null
}

commit (提交階段)

commit階段可以理解為就是將 Diff 的結(jié)果反映到真實(shí) DOM 的過程盯荤。這一階段從函數(shù) completeRoot 開始馋吗。在這個(gè)階段,React 更新 DOM 并調(diào)用變更生命周期之前及之后方法的地方秋秤。

當(dāng) React 進(jìn)入這個(gè)階段時(shí)宏粤,它有 2 棵樹和副作用列表脚翘。第一個(gè)樹表示當(dāng)前在屏幕上渲染的狀態(tài),然后在 render 階段會(huì)構(gòu)建一個(gè)備用樹绍哎。它在源代碼中稱為 finishedWorkworkInProgress来农,表示需要映射到屏幕上的狀態(tài)。此備用樹會(huì)用類似的方法通過 childsibling 指針鏈接到 current 樹崇堰。

然后沃于,有一個(gè)副作用列表 -- 它是 finishedWork 樹的節(jié)點(diǎn)子集,通過 nextEffect 指針進(jìn)行鏈接海诲。需要記住的是繁莹,副作用列表是運(yùn)行 render 階段的結(jié)果。渲染的重點(diǎn)就是確定需要插入特幔、更新或刪除的節(jié)點(diǎn)咨演,以及哪些組件需要調(diào)用其生命周期方法。這就是副作用列表告訴我們的內(nèi)容蚯斯,它頁正是在 commit 階段迭代的節(jié)點(diǎn)集合薄风。

出于調(diào)試目的,可以通過 Fiber 根的屬性 current訪問 current 樹溉跃〈迮伲可以通過 current 樹中 HostFiber 節(jié)點(diǎn)的 alternate 屬性訪問 finishedWork 樹。

commit 階段運(yùn)行的主要函數(shù)是 commitRoot 撰茎。它執(zhí)行如下下操作:

  • 在標(biāo)記為 Snapshot 副作用的節(jié)點(diǎn)上調(diào)用 getSnapshotBeforeUpdate 生命周期

  • 在標(biāo)記為 Deletion 副作用的節(jié)點(diǎn)上調(diào)用 componentWillUnmount 生命周期

  • 執(zhí)行所有 DOM 插入嵌牺、更新、刪除操作

  • finishedWork 樹設(shè)置為 current

  • 在標(biāo)記為 Placement 副作用的節(jié)點(diǎn)上調(diào)用 componentDidMount 生命周期

  • 在標(biāo)記為 Update 副作用的節(jié)點(diǎn)上調(diào)用 componentDidUpdate 生命周期

在調(diào)用變更前方法 getSnapshotBeforeUpdate 之后龄糊,React 會(huì)在樹中提交所有副作用逆粹,這會(huì)通過兩波操作來完成。第一波執(zhí)行所有 DOM(宿主)插入炫惩、更新僻弹、刪除和 ref 卸載。然后 React 將 finishedWork 樹賦值給 FiberRoot他嚷,將 workInProgress 樹標(biāo)記為 current 樹蹋绽。這是在提交階段的第一波之后、第二波之前完成的筋蓖,因此在 componentWillUnmount 中前一個(gè)樹仍然是 current卸耘,在 componentDidMount/Update 期間已完成工作是 current。在第二波粘咖,React 調(diào)用所有其他生命周期方法和引用回調(diào)蚣抗。這些方法單獨(dú)傳遞執(zhí)行,從而保證整個(gè)樹中的所有放置瓮下、更新和刪除能夠被觸發(fā)執(zhí)行翰铡。

以下是運(yùn)行上述步驟的函數(shù)的要點(diǎn):

function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}

這些子函數(shù)中都實(shí)現(xiàn)了一個(gè)循環(huán)钝域,該循環(huán)遍歷副作用列表并檢查副作用的類型。當(dāng)它找到與函數(shù)目的相關(guān)的副作用時(shí)锭魔,就會(huì)執(zhí)行例证。

在 commit 階段,在 commitRoot 里會(huì)根據(jù) effecteffectTag赂毯,具體 effectTag 見源碼 战虏,進(jìn)行對(duì)應(yīng)的插入、更新党涕、刪除操作烦感,根據(jù) tag 不同,調(diào)用不同的更新方法膛堤。并把這些更新提交到當(dāng)前節(jié)點(diǎn)的父親手趣。當(dāng)遍歷完這顆樹的時(shí)候,再通過 return 回溯到根節(jié)點(diǎn)肥荔。這個(gè)過程中把所有的更新全部帶到根節(jié)點(diǎn)绿渣,再一次更新到真實(shí)的 dom 中去。如下圖所示:

image

從根節(jié)點(diǎn)開始:

?? 1. div1 通過 child 到 div2燕耿。
?? 2. div2 和自己的 alternate 比較完把更新 commit1 通過 return 提交到 div1中符。
?? 3. div2 通過 sibling 到 ul1。
?? 4. ul1 和自己的 alternate 比較完把更新 commit2 通過 return 提交到 div1誉帅。
?? 5. ul1 通過 child 到 li1淀散。
?? 6. li1 和自己的 alternate 比較完把更新 commit3 通過 return 提交到 ul1。
?? 7. li1 通過 sibling 到 li2蚜锨。
?? 8. li2 和自己的 alternate 比較完把更新 commit4 通過 return 提交到 ul1档插。
?? 9. 遍歷完整棵樹開始回溯,li2 通過 return 回到 ul1亚再。
?? 10. 把 commit3 和 commit4 通過 return 提交到 div1郭膛。
?? 11. ul1 通過 return 回到 div1。
?? 12. 獲取到所有更新 commit1-4氛悬,一次更新到真是的 dom 中去则剃。


雙緩沖原理

image.png

當(dāng) render 的時(shí)候有了這么一條單鏈表,當(dāng)調(diào)用 setState 的時(shí)候又是如何 Diff 得到 change 的呢如捅?
采用的是一種叫雙緩沖技術(shù)(double buffering)梢褐,這個(gè)時(shí)候就需要另外一顆樹:WorkInProgress Tree颠放,它反映了要刷新到屏幕的未來狀態(tài)霸奕。
WorkInProgress Tree 構(gòu)造完畢铭乾,得到的就是新的 Fiber Tree汛蝙,然后喜新厭舊(把 current 指針指向WorkInProgress Tree烈涮,丟掉舊的 Fiber Tree)就好了朴肺。
這樣做的好處:

能夠復(fù)用內(nèi)部對(duì)象(fiber),比如某顆子樹不需要變動(dòng)坚洽,React會(huì)克隆復(fù)用舊樹中的子樹戈稿。
節(jié)省內(nèi)存分配、GC的時(shí)間開銷讶舰,
就算運(yùn)行中有錯(cuò)誤鞍盗,也不會(huì)影響 View 上的數(shù)據(jù),比如當(dāng)一個(gè)節(jié)點(diǎn)拋出異常跳昼,仍然可以繼續(xù)沿用舊樹的節(jié)點(diǎn)般甲,避免整棵樹掛掉

每個(gè) Fiber上都有個(gè)alternate屬性,也指向一個(gè) Fiber鹅颊,創(chuàng)建 WorkInProgress 節(jié)點(diǎn)時(shí)優(yōu)先取alternate敷存,沒有的話就創(chuàng)建一個(gè)。
創(chuàng)建 WorkInProgress Tree 的過程也是一個(gè) Diff 的過程堪伍,Diff 完成之后會(huì)生成一個(gè) Effect List锚烦,這個(gè) Effect List 就是最終 Commit 階段用來處理副作用的階段。

Dan 在 Beyond React 16 演講中用了一個(gè)非常恰當(dāng)?shù)谋扔鞯酃停蔷褪荊it 功能分支涮俄,你可以將 WIP 樹想象成從舊樹中 Fork 出來的功能分支,你在這新分支中添加或移除特性尸闸,即使是操作失誤也不會(huì)影響舊的分支彻亲。當(dāng)你這個(gè)分支經(jīng)過了測試和完善,就可以合并到舊分支室叉,將其替換掉. 這或許就是’提交(commit)階段‘的提交一詞的來源吧睹栖?:

image.png

后記

本開始想一篇文章把 Fiber 講透的,但是寫著寫著發(fā)現(xiàn)確實(shí)太多了茧痕,想寫詳細(xì)野来,估計(jì)要寫幾萬字,所以我這篇文章的目的僅僅是在沒有涉及到源碼的情況下梳理了大致 React 的工作流程踪旷,對(duì)于細(xì)節(jié)曼氛,比如如何調(diào)度異步任務(wù)、如何去做 Diff 等等細(xì)節(jié)將以小節(jié)的方式一個(gè)個(gè)的結(jié)合源碼進(jìn)行分析令野。
說實(shí)話舀患,自己不是特別滿意這篇,感覺頭重腳輕气破,在之后的學(xué)習(xí)中會(huì)逐漸完善這篇文章聊浅。,這篇文章拖太久了,請(qǐng)繼續(xù)后續(xù)的文章低匙。

站在巨人肩上

??React 拾遺:React.createElement 與 JSX
?? React的React.createElement源碼解析(一)
?? React 組件Component,元素Element和實(shí)例Instance的區(qū)別
?? React Fiber 那些事: 深入解析新的協(xié)調(diào)算法
?? React-從源碼分析React Fiber工作原理
??淺談 React Fiber
?? React Fiber架構(gòu)
?? 完全理解React Fiber
?? 這可能是最通俗的 React Fiber(時(shí)間分片) 打開方式
?? Deep In React 之淺談 React Fiber 架構(gòu)(一)
?? React Fiber初探
??React Diff 算法
?? React16性能改善的原理(二)
?? 淺談React16框架 - Fiber
??React的第一次渲染過程淺析
?? react的更新機(jī)制
?? 【React進(jìn)階系列】 setState機(jī)制
??深入React技術(shù)棧之setState詳解
?? 【React深入】setState的執(zhí)行機(jī)制

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末旷痕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子顽冶,更是在濱河造成了極大的恐慌欺抗,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件强重,死亡現(xiàn)場離奇詭異绞呈,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)间景,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門佃声,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人倘要,你說我怎么就攤上這事秉溉。” “怎么了碗誉?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵召嘶,是天一觀的道長。 經(jīng)常有香客問我哮缺,道長弄跌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任尝苇,我火速辦了婚禮铛只,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘糠溜。我一直安慰自己淳玩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布非竿。 她就那樣靜靜地躺著蜕着,像睡著了一般。 火紅的嫁衣襯著肌膚如雪红柱。 梳的紋絲不亂的頭發(fā)上承匣,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音锤悄,去河邊找鬼韧骗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛零聚,可吹牛的內(nèi)容都是我干的袍暴。 我是一名探鬼主播些侍,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼政模!你這毒婦竟也來了娩梨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤览徒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后颂龙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體习蓬,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年措嵌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了躲叼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡企巢,死狀恐怖枫慷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浪规,我是刑警寧澤或听,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站笋婿,受9級(jí)特大地震影響誉裆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缸濒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一足丢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧庇配,春花似錦斩跌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至啸澡,卻和暖如春揭糕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锻霎。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工著角, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人旋恼。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓吏口,卻偏偏與公主長得像奄容,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子产徊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345