<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React 是如何創(chuàng)建 vdom 和 fiber tree

          共 7799字,需瀏覽 16分鐘

           ·

          2020-12-06 13:11

          • 作者:linxiangjun
          • 原文鏈接:https://www.linxiangjun.com/react-render-source.html#JSX

          前言

          本篇文章作為react源碼分析與優(yōu)化寫作計(jì)劃的第一篇,分析了react是如何創(chuàng)建vdom和fiber tree的。本篇文章通過閱讀react 16.8及以上版本源碼以及參考大量分析文章寫作而成,react框架本身算法以及架構(gòu)層也是不斷的在優(yōu)化,所以源碼中存在很多legacy的方法,不過這并不影響我們對于react設(shè)計(jì)思想的學(xué)習(xí)和理解。

          閱讀源碼一定要帶著目的性的去展開,這樣就會減少過程中的枯燥感,而寫作能夠提煉和升華自己的學(xué)習(xí)和理解,這也是本篇以及后續(xù)文章的動力所在。如果寫作的文章還能夠幫助到其他開發(fā)者,那就更好了。

          JSX

          首先,來看一個(gè)簡單的 React 組件。

          import React from 'react';

          export default function App({
            return (
              <div className="App">
                <h1>Hello Reacth1>

              div>
            );
          }

          上面常用的語法稱之為 JSX,是 React.createElement 方法的語法糖,使用 JSX 能夠直觀的展現(xiàn) UI 及其交互,實(shí)現(xiàn)關(guān)注點(diǎn)分離。

          每個(gè) react 組件的頂部都要導(dǎo)入 React,因?yàn)?JSX 實(shí)際上依賴 Babel(@babel/preset-react)來對語法進(jìn)行轉(zhuǎn)換,最終生成React.createElemnt的嵌套語法。

          下方能夠直觀的看到 JSX 轉(zhuǎn)換后的渲染結(jié)果。

          function App({
            return React.createElement(
              'div',
              {
                className'App',
              },
              React.createElement('h1'null'Hello React')
            );
          }

          createElement

          createElement()方法定義如下:

          React.createElement(type, [props], [...children]);

          createElement()接收三個(gè)參數(shù),分別是元素類型、屬性值以及子元素,它最終會生成 Virtual DOM。

          我們將上面的 組件內(nèi)容打印到控制臺中。

          可以看到 Virtual DOM 本質(zhì)上是 JS 對象,將節(jié)點(diǎn)信息通過鍵值對的方式存儲起來,同時(shí)使用嵌套來表示節(jié)點(diǎn)間的層級關(guān)系。使用 VDOM 能夠避免頻繁的進(jìn)行 DOM 操作,同時(shí)也為后面的 React Diff 算法創(chuàng)造了條件。現(xiàn)在回到createElement()方法,來看一下它究竟是如何生產(chǎn) VDOM 的。

          createElement()方法精簡版(v16.8)

          createElement

          首先,createElement()方法會先通過遍歷config獲取所有的參數(shù),然后獲取其子節(jié)點(diǎn)以及默認(rèn)的props的值。然后將值傳遞給ReactElement()調(diào)用并返回 JS 對象。

          ReactElement

          值得注意的是,每個(gè) react 組件都會使用$$typeof來標(biāo)識,它的值使用了Symbol數(shù)據(jù)結(jié)構(gòu)來確保唯一性。

          ReactDOM.render

          到目前為止,我們得到了 VDOM,react通過協(xié)調(diào)算法(reconciliation)去比較更新前后的VDOM,從而找到需要更新的最小操作,減少了瀏覽器多次操作DOM的成本。但是,由于使用遞歸的方式來遍歷組件樹,當(dāng)組件樹越來越大,遞歸遍歷的成本就越高。這樣,由于持續(xù)占用主線程,像布局、動畫等任務(wù)無法立即得到處理,就會出現(xiàn)丟幀的現(xiàn)象。所以,為不同類型的任務(wù)賦予優(yōu)先級,同時(shí)支持任務(wù)的暫停、中止與恢復(fù),是非常有必要的。

          為了解決上面存在的問題,React團(tuán)隊(duì)給出了React Fiber算法以及fiber tree數(shù)據(jù)結(jié)構(gòu)(基于單鏈表的樹結(jié)構(gòu)),而ReactDOM.render方法就是實(shí)現(xiàn)React Fiber算法以及構(gòu)建fiber tree的核心API。

          render()方法定義如下:

          ReactDOM.render(element, container[, callback])

          這里重點(diǎn)從源碼層面講解下ReactDOM.render是如何構(gòu)建fiber tree的。

          ReactDOM.render實(shí)際調(diào)用了legacyRenderSubtreeIntoContainer方法,調(diào)用過程以及傳參如下:

          ReactDOM = {
            render(element, container, callback) {
              return legacyRenderSubtreeIntoContainer(
                null,
                element,
                container,
                false,
                callback
              );
            },
          };

          其中的elementcontainer我們都很熟悉了,而callback是用來渲染完成后需要執(zhí)行的回調(diào)函數(shù)。再來看看該方法的定義。

          function legacyRenderSubtreeIntoContainer(
            parentComponent,
            children,
            container,
            forceHydrate,
            callback
          {
            let root = container._reactRootContainer;
            let fiberRoot;
            // 初次渲染
            if (!root) {
              // 初始化掛載,獲得React根容器對象
              root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
                container,
                forceHydrate
              );
              fiberRoot = root._internalRoot;

              // 初始化安裝不需要批量更新,需要盡快完成
              unbatchedUpdates(() => {
                updateContainer(children, fiberRoot, parentComponent, callback);
              });
            } else {
              fiberRoot = root._internalRoot;

              updateContainer(children, fiberRoot, parentComponent, callback);
            }
            return getPublicRootInstance(fiberRoot);
          }

          上面是簡化后的源碼。先來看傳參,因?yàn)槭菕燧droot,所以parentComponent設(shè)置為null。另外一個(gè)參數(shù)forceHydrate代表是否是服務(wù)端渲染,因?yàn)檎{(diào)用的render()方法為客服端渲染,所以默認(rèn)為false。另外callback使用少,所以關(guān)于它的處理過程就省略了。

          因?yàn)槭鞘状螔燧d,所以rootcontainer._reactRootContainer獲取不到值,就會創(chuàng)建FiberRoot對象。在FiberRoot對象創(chuàng)建過程中考慮到了服務(wù)端渲染的情況,并且函數(shù)之間相互調(diào)用非常多,所以這里直接展示其最終調(diào)用的核心方法。

          // 創(chuàng)建fiberRoot和rootFiber并相互引用
          function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks{
            const root = new FiberRootNode(containerInfo, tag, hydrate);
            if (enableSuspenseCallback) {
              root.hydrationCallbacks = hydrationCallbacks;
            }

            // 創(chuàng)建fiber tree的根節(jié)點(diǎn),即rootFiber
            const uninitializedFiber = createHostRootFiber(tag);
            root.current = uninitializedFiber;
            uninitializedFiber.stateNode = root;

            initializeUpdateQueue(uninitializedFiber);

            return root;
          }

          在該方法中containerInfo就是root節(jié)點(diǎn),而tagFiberRoot節(jié)點(diǎn)的標(biāo)記,這里為LegacyRoot。另外兩個(gè)參數(shù)和服務(wù)端渲染有關(guān)。這里使用FiberRootNode方法創(chuàng)建了FiberRoot對象,并使用createHostRootFiber方法創(chuàng)建RootFiber對象,使FiberRoot中的current指向RootFiberRootFiberstateNode指向FiberRoot,形成相互引用。

          下面的兩個(gè)構(gòu)造函數(shù)是展現(xiàn)出了fiberRoot以及rootFiber的部分重要的屬性。

          FiberRootNode部分屬性:

          function FiberRootNode(containerInfo, tag, hydrate{
            // 用于標(biāo)記fiberRoot的類型
            this.tag = tag;
            // 指向當(dāng)前激活的與之對應(yīng)的rootFiber節(jié)點(diǎn)
            this.current = null;
            // 和fiberRoot關(guān)聯(lián)的DOM容器的相關(guān)信息
            this.containerInfo = containerInfo;
            // 當(dāng)前的fiberRoot是否處于hydrate模式
            this.hydrate = hydrate;
            // 每個(gè)fiberRoot實(shí)例上都只會維護(hù)一個(gè)任務(wù),該任務(wù)保存在callbackNode屬性中
            this.callbackNode = null;
            // 當(dāng)前任務(wù)的優(yōu)先級
            this.callbackPriority = NoPriority;
          }

          Fiber Node構(gòu)造函數(shù)的部分屬性:

          function FiberNode(tag, pendingProps, key, mode{
            // rootFiber指向fiberRoot,child fiber指向?qū)?yīng)的組件實(shí)例
            this.stateNode = null;
            // return屬性始終指向父節(jié)點(diǎn)
            this.return = null;
            // child屬性始終指向第一個(gè)子節(jié)點(diǎn)
            this.child = null;
            // sibling屬性始終指向第一個(gè)兄弟節(jié)點(diǎn)
            this.sibling = null;
            // 表示更新隊(duì)列,例如在常見的setState操作中,會將需要更新的數(shù)據(jù)存放到updateQueue隊(duì)列中用于后續(xù)調(diào)度
            this.updateQueue = null;
            // 表示當(dāng)前更新任務(wù)的過期時(shí)間,即在該時(shí)間之后更新任務(wù)將會被完成
            this.expirationTime = NoWork;
          }

          最終生成的fiber tree結(jié)構(gòu)示意圖如下:

          fiber樹結(jié)構(gòu)示意圖

          React Diff 算法

          react 并不會比原生操作 DOM 快,但是在大型應(yīng)用中,往往不需要每次全部重新渲染,這時(shí) react 通過 VDOM 以及 diff 算法能夠只更新必要的 DOM。react 將 VDOM 與 diff 算法結(jié)合起來并對其進(jìn)行優(yōu)化,提供了高性能的 React Diff 算法,通過一系列的策略,將傳統(tǒng)的 diff 算法復(fù)雜度 O(n^3)優(yōu)化為 O(n)的復(fù)雜度,極大的提升了渲染性能。

          這里不展開探究 React Diff 的具體實(shí)現(xiàn)原理,而先了解下它到底的基于什么策略來實(shí)現(xiàn)的。

          1. Web UI 中 DOM 節(jié)點(diǎn)跨層級的移動操作特別少,可以忽略不計(jì)。
          2. 擁有相同類的兩個(gè)組件將會生成相似的樹形結(jié)構(gòu),擁有不同類的兩個(gè)組件將會生成不同的樹形結(jié)構(gòu)。
          3. 對于同一層級的一組子節(jié)點(diǎn),它們可以通過唯一 id 進(jìn)行區(qū)分。

          基于這三個(gè)策略,react 在 tree diff 和 component diff 中,兩棵樹只會對同層次的節(jié)點(diǎn)進(jìn)行比較。如果同層級的樹發(fā)生了更新,則會將該節(jié)點(diǎn)及其子節(jié)點(diǎn)同時(shí)進(jìn)行更新,這樣避免了遞歸遍歷更加深入的節(jié)點(diǎn)的操作。在后面渲染性能優(yōu)化部分,對于同一類型的組件如果能夠準(zhǔn)確的知道 VDOM 是否變化,使用shouldComponentUpdate來判斷該組件是否需要 diff,能夠節(jié)省大量的 diff 運(yùn)算時(shí)間。

          當(dāng) react 進(jìn)行 element diff 操作中,在元素中添加唯一的key來進(jìn)行區(qū)分,對其進(jìn)行算法優(yōu)化。所以像大數(shù)據(jù)量的列表之類的組件中最好添加key屬性,能夠帶來一定的性能提升。

          交流討論

          歡迎關(guān)注公眾號「前端試煉」,公眾號平時(shí)會分享一些實(shí)用或者有意思的東西,發(fā)現(xiàn)代碼之美。專注深度和最佳實(shí)踐,希望打造一個(gè)高質(zhì)量的公眾號。

          公眾號后臺回復(fù)「加群」,拉你進(jìn)交流劃水聊天群,有看到好文章/代碼都會發(fā)在群里。

          如果你不想加群,只是想加我也是可以。

          如果覺得這篇文章還不錯(cuò),來個(gè)【轉(zhuǎn)發(fā)、收藏、在看】三連吧,讓更多的人也看到~


          ?? 順手點(diǎn)個(gè)在看唄 ↓?

          瀏覽 79
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  五月亚洲精品成人片一区 | 乱伦免费小说黄色电影 | 九一国产在线 | 久草免费在线 | 国产精品第一操逼视频 |