日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
一文讀懂React組件渲染核心原理

引言

相信大家對(duì) React 都已經(jīng)非常熟悉了,像 React,Vue 這樣的現(xiàn)代前端框架已經(jīng)是我們?nèi)粘i_(kāi)發(fā)離不開(kāi)的工具了,這篇文章主要是從源碼的角度剖析 React 的核心渲染原理。我們將從用戶編寫的組件代碼開(kāi)始,一步一步分析 React 是如何將它們變成真實(shí) DOM ,這個(gè)過(guò)程主要可以分成兩個(gè)階段:render 階段和 commit 階段。文章的核心內(nèi)容也正是對(duì)這兩個(gè)階段的分析。

專業(yè)從事成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、外貿(mào)網(wǎng)站建設(shè),高端網(wǎng)站制作設(shè)計(jì),小程序開(kāi)發(fā),網(wǎng)站推廣的成都做網(wǎng)站的公司。優(yōu)秀技術(shù)團(tuán)隊(duì)竭力真誠(chéng)服務(wù),采用HTML5+CSS3前端渲染技術(shù),自適應(yīng)網(wǎng)站建設(shè),讓網(wǎng)站在手機(jī)、平板、PC、微信下都能呈現(xiàn)。建站過(guò)程建立專項(xiàng)小組,與您實(shí)時(shí)在線互動(dòng),隨時(shí)提供解決方案,暢聊想法和感受。

一、前置知識(shí)

聲明式渲染

『聲明式渲染』,顧名思義,就是讓使用者只需要「聲明或描述」我需要渲染的東西是什么,然后就把具體的渲染工作交給機(jī)器去做,與之相對(duì)的是『命令式渲染』。

『命令式渲染』則是由用戶去一步一步地命令機(jī)器下一步該怎么做。

舉個(gè)簡(jiǎn)單的例子:

如果我們需要在網(wǎng)頁(yè)上渲染一個(gè)有三個(gè)節(jié)點(diǎn)的列表,命令式的做法是手動(dòng)操作 dom,首先創(chuàng)建一個(gè)容器節(jié)點(diǎn),再利用循環(huán)每次先創(chuàng)建一個(gè)新節(jié)點(diǎn),填充內(nèi)容,然后將新節(jié)點(diǎn)新增到容器節(jié)點(diǎn)下,最后再將容器節(jié)點(diǎn)新增到 body 標(biāo)簽下:

 
 
 
 
  1. const list = [1,2,3];
  2. const container = document.createElement('div');
  3. for (let i = 0; i < list.length; i ++) {
  4.     const newDom = document.createElement('div');
  5.     newDom.innerHTML = list[i];
  6.     container.appendChild(newDom);
  7. }
  8. document.body.appendChild(container);

而聲明式的做法應(yīng)該是:

 
 
 
 
  1. const list = [1,2,3];
  2. const container = document.createElement('div');
  3. const Demo = () =>
  4. (
  5.     {list.map((item) => 
    {item}
    )}
)
  • ReactDom.render(, container);
  • 可以看到在這個(gè)例子中,聲明式寫法以 HTML 語(yǔ)法直接告訴機(jī)器,我需要的視圖應(yīng)該是長(zhǎng)這個(gè)樣子,然后具體的 DOM 操作全部交由機(jī)器去完成。開(kāi)發(fā)者只需要專注于業(yè)務(wù)邏輯的實(shí)現(xiàn)。

    這便是聲明式渲染。

    聲明式渲染是現(xiàn)代前端框架的比較普遍的設(shè)計(jì)思路。

    JSX 和 ReactElement

    相信大家最初學(xué) React 的時(shí)候都有這樣的疑問(wèn),為什么我們能夠以類似 HTML 的語(yǔ)法編寫組件,這個(gè)東西又是怎么轉(zhuǎn)換成 JavaScript 語(yǔ)法的?答案就是 Babel。根據(jù)官網(wǎng)介紹,這種語(yǔ)法被稱為 JSX,是一個(gè) JavaScript 的語(yǔ)法擴(kuò)展。能夠被 Babel 編譯成 React.createElement 方法。舉個(gè)例子:

    通過(guò)查閱源碼我們可以看到 「React.createElement」 方法。

     
     
     
     
    1. export function createElement(type, config, children) {
    2.   let propName;
    3.   // Reserved names are extracted
    4.   const props = {};
    5.   let key = null;
    6.   let ref = null;
    7.   let self = null;
    8.   let source = null;
    9.   ...
    10.   return ReactElement(
    11.     type,
    12.     key,
    13.     ref,
    14.     self,
    15.     source,
    16.     ReactCurrentOwner.current,
    17.     props,
    18.   );
    19. }
    20. const ReactElement = function(type, key, ref, self, source, owner, props) {
    21.   const element = {
    22.     // This tag allows us to uniquely identify this as a React Element
    23.     $typeof: REACT_ELEMENT_TYPE,
    24.     // Built-in properties that belong on the element
    25.     type: type,
    26.     key: key,
    27.     ref: ref,
    28.     props: props,
    29.     // Record the component responsible for creating this element.
    30.     _owner: owner,
    31.   };
    32.   ...
    33.   return element;
    34. }

    可以看到 React 是使用了 element 這種結(jié)構(gòu)來(lái)代表一個(gè)節(jié)點(diǎn),里面就只有簡(jiǎn)單的 6 個(gè)字段。我們可以看個(gè)實(shí)際的例子,下面 Count 組件對(duì)應(yīng)的 element 數(shù)據(jù)結(jié)構(gòu):

     
     
     
     
    1. function Count({count, onCountClick}) {
    2.   return  { onCountClick()}}>
    3.   count: {count}
    4.   
  • }
  • 可以看到,element 結(jié)構(gòu)只能反映出 jsx 節(jié)點(diǎn)的層級(jí)結(jié)構(gòu),而組件里的各種狀態(tài)或者返回 jsx 等都是不會(huì)記錄在 element 中。

    目前我們知道,我們編寫的 jsx 會(huì)首先被處理成 element 結(jié)構(gòu)。

    jsx -> element

    那 React 又是如何處理 element 的,如剛剛說(shuō)的,element 里包含的信息太少,只靠 element 顯然是不足以映射到所有真實(shí) DOM 的,因此我們還需要更精細(xì)的結(jié)構(gòu)。

    Fiber 樹(shù)結(jié)構(gòu)

    Fiber 這個(gè)單詞相信大家多多少少都有聽(tīng)過(guò),它是在 React 16 被引入,關(guān)于 Fiber 如何實(shí)現(xiàn)任務(wù)調(diào)度在這篇文章不會(huì)涉及,但是 Fiber 的引入不僅僅帶來(lái)了任務(wù)調(diào)度方面的能力,整個(gè) React 實(shí)現(xiàn)架構(gòu)也因此重構(gòu)了一遍,而我們之前經(jīng)常提到的虛擬 DOM 樹(shù)在新的 React 架構(gòu)下被稱為 Fiber 樹(shù),上面提到的每個(gè) element 都有一個(gè)所屬的 Fiber。

    首先我們先看看源碼中 Fiber 的構(gòu)造函數(shù):

     
     
     
     
    1. function FiberNode(
    2.   tag: WorkTag,
    3.   pendingProps: mixed,
    4.   key: null | string,
    5.   mode: TypeOfMode,
    6. ) {
    7.   // Instance
    8.   this.tag = tag;            // 標(biāo)識(shí)節(jié)點(diǎn)類型,例如函數(shù)組件、類組件、普通標(biāo)簽等
    9.   this.key = key;
    10.   this.elementType = null;  // 標(biāo)識(shí)具體 jsx 標(biāo)簽名
    11.   this.type = null;        // 類似 elementType
    12.   this.stateNode = null;  // 對(duì)應(yīng)的真實(shí) DOM 節(jié)點(diǎn)
    13.   // Fiber
    14.   this.return = null;    // 父節(jié)點(diǎn)
    15.   this.child = null;     // 第一個(gè)子節(jié)點(diǎn)
    16.   this.sibling = null;   // 第一個(gè)兄弟節(jié)點(diǎn)
    17.   this.index = 0;
    18.   this.ref = null;
    19.   this.pendingProps = pendingProps;  // 傳入的 props
    20.   this.memoizedProps = null;    
    21.   this.updateQueue = null;   // 狀態(tài)更新相關(guān)
    22.   this.memoizedState = null;
    23.   this.dependencies = null;
    24.   this.mode = mode;
    25.   // Effects
    26.   this.flags = NoFlags;
    27.   this.subtreeFlags = NoFlags;
    28.   this.deletions = null;
    29.   this.lanes = NoLanes;
    30.   this.childLanes = NoLanes;
    31.   this.alternate = null;
    32.   ...
    33. }

    可以看到 Fiber 節(jié)點(diǎn)中的屬性很多,其中不僅僅包含了 element 相關(guān)的實(shí)例信息,還包含了組成 Fiber 樹(shù)所需的一些“指針”,組件內(nèi)部的狀態(tài)(memorizedState),用于操作真實(shí) DOM 的副作用(effects)等等。

    我們以上面的 Count 組件為例看一下它對(duì)應(yīng)的 Fiber 結(jié)構(gòu):

    這里我們先主要介紹一下與形成 Fiber 樹(shù)相關(guān)的三個(gè)屬性:child, sibling 和 return。他們分別指向 Fiber 的第一個(gè)子 Fiber,下一個(gè)兄弟 Fiber 和父 Fiber。

    以下面的 jsx 代碼為例:

     
     
     
     
    1.      
    2. // App.jsx    
    3.     
    4.       
    5.         
    6.         

    7.           text
    8.         

    9.         
    10.       
    11.     
  •     
  • // Count.jsx
  • 最終形成的 Fiber 樹(shù)結(jié)構(gòu)為:

    總結(jié)一下,我們編寫的 jsx 首先會(huì)形成 element ,然后在 render 過(guò)程中每個(gè) element 都會(huì)生成對(duì)應(yīng)的 Fiber,最終形成 Fiber 樹(shù)。

    jsx -> element -> Fiber

    下面我們正式介紹一下 render 的過(guò)程,看看 Fiber 是如何生成并形成 Fiber 樹(shù)的。

    二、渲染(render)過(guò)程

    核心流程

    通常 React 運(yùn)行時(shí)會(huì)有兩個(gè) Fiber 樹(shù),一個(gè)是根據(jù)當(dāng)前最新組件狀態(tài)構(gòu)建出來(lái)的,另一個(gè)則是上一次構(gòu)建出來(lái)的 Fiber 樹(shù),當(dāng)然如果是首次渲染就沒(méi)有上一次的 Fiber 樹(shù),這時(shí)就只有一個(gè)了。簡(jiǎn)單來(lái)說(shuō),render 過(guò)程就是 React 「對(duì)比舊 Fiber 樹(shù)和新的 element」 然后「為新的 element 生成新 Fiber 樹(shù)」的一個(gè)過(guò)程。

    從源碼中看,React 的整個(gè)核心流程開(kāi)始于 「performSyncWorkOnRoot」 函數(shù),在這個(gè)函數(shù)里會(huì)先后調(diào)用 「renderRootSync」 函數(shù)和 「commitRoot」 函數(shù),它們兩個(gè)就是分別就是我們上面提到的 render 和 commit 過(guò)程。來(lái)看 renderRootSync 函數(shù),在 「renderRootSync」 函數(shù)里會(huì)先調(diào)用 「prepareFreshStack」 ,從函數(shù)名字我們不難猜出它主要就是為接下來(lái)的工作做前置準(zhǔn)備,初始化一些變量例如 workInProgress(當(dāng)前正在處理的 Fiber 節(jié)點(diǎn)) 等,接著會(huì)調(diào)用 「workLoopSync」 函數(shù)。(這里僅討論傳統(tǒng)模式,concurrent 模式留給 Fiber 任務(wù)調(diào)度分享),而在 「workLoopSync」 完成之后,「renderRootSync」 也基本上完成了,接下來(lái)就會(huì)調(diào)用 commitRoot 進(jìn)入 commit 階段。

    因此整個(gè) render 過(guò)程的重點(diǎn)在 「workLoopSync」 中,從 「workLoopSync」 簡(jiǎn)單的函數(shù)定義里我們可以看到,這里用了一個(gè)循環(huán)來(lái)不斷調(diào)用 「performUnitOfWork」 方法,直到 workInProgress 為 null。

     
     
     
     
    1. function workLoopSync() {
    2.   // Already timed out, so perform work without checking if we need to yield.
    3.   while (workInProgress !== null) {
    4.     performUnitOfWork(workInProgress);
    5.   }
    6. }

    而 「performUnitOfWork」 函數(shù)做的事情也很簡(jiǎn)單,簡(jiǎn)單來(lái)說(shuō)就是為傳進(jìn)來(lái)的 workInProgress 生成下一個(gè) Fiber 節(jié)點(diǎn)然后賦值給 workInProgress。通過(guò)不斷的循環(huán)調(diào)用 「performUnitOfWork」,直到把所有的 Fiber 都生成出來(lái)并連接成 Fiber 樹(shù)為止。

    現(xiàn)在我們來(lái)看 「performUnitOfWork」 具體是如何生成 Fiber 節(jié)點(diǎn)的。

    前面介紹 Fiber 結(jié)構(gòu)的時(shí)候說(shuō)過(guò),F(xiàn)iber 是 React 16 引入用于任務(wù)調(diào)度提升用戶體驗(yàn)的,而在此之前,render 過(guò)程是遞歸實(shí)現(xiàn)的,顯然遞歸是沒(méi)有辦法中斷的,因此 React 需要使用循環(huán)來(lái)模擬遞歸過(guò)程。

    「performUnitOfWork」 正是使用了 「beginWork」 和 「completeUnitOfWork」 來(lái)分別模擬這個(gè)“遞”和“歸”的過(guò)程。

    render 過(guò)程是深度優(yōu)先的遍歷,「beginWork」 函數(shù)則會(huì)為遍歷到的每個(gè) Fiber 節(jié)點(diǎn)生成他的所有子 Fiber 并返回第一個(gè)子 Fiber ,這個(gè)子 Fiber 將賦值給 workInProgress,在下一輪循環(huán)繼續(xù)處理,直到遍歷到葉子節(jié)點(diǎn),這時(shí)候就需要“歸”了。

    「completeUnitOfWork」 就會(huì)為葉子節(jié)點(diǎn)做一些處理,然后把葉子節(jié)點(diǎn)的兄弟節(jié)點(diǎn)賦值給 workInProgress 繼續(xù)“遞”操作,如果連兄弟節(jié)點(diǎn)也沒(méi)有的話,就會(huì)往上處理父節(jié)點(diǎn)。

    同樣以上面的 Fiber 樹(shù)例子來(lái)看,其中的 Fiber 節(jié)點(diǎn)處理順序應(yīng)該如下:

    beginWork

    在介紹概覽的時(shí)候說(shuō)過(guò),React 通常會(huì)同時(shí)存在兩個(gè) Fiber 樹(shù),一個(gè)是當(dāng)前視圖對(duì)應(yīng)的,一個(gè)則是根據(jù)最新?tīng)顟B(tài)正在構(gòu)建中的。這兩棵樹(shù)的節(jié)點(diǎn)一一對(duì)應(yīng),我們用 current 來(lái)代表前者,我們不難發(fā)現(xiàn),當(dāng)首次渲染的時(shí)候,current 必然指向 null。實(shí)際上在代碼中也確實(shí)都是通過(guò)這個(gè)來(lái)判斷當(dāng)前是首次渲染還是更新。

    「beginWork」 的目的很簡(jiǎn)單:

    • 更新當(dāng)前節(jié)點(diǎn)(workInProgress),獲取新的 children。
    • 為新的 children 生成他們對(duì)應(yīng)的 Fiber,并「最終返回第一個(gè)子節(jié)點(diǎn)(child)」。

    在 「beginWork」 執(zhí)行中,首先會(huì)判斷當(dāng)前是否是首次渲染。

    • 如果是首次渲染:
      • 則下來(lái)會(huì)根據(jù)當(dāng)前正在構(gòu)建的節(jié)點(diǎn)的組件類型做不同的處理,源碼中這塊邏輯使用了大量的 switch case。
     
     
     
     
    1. switch (workInProgress.tag) {
    2.     case FunctionComponent: {
    3.       ...
    4.     }
    5.     case ClassComponent: {
    6.       ...
    7.     }
    8.     case HostRoot: {
    9.       ...
    10.     }
    11.     case HostComponent: {
    12.       ...
    13.     }
    14.     ...
    15.   }
    • 如果非首次渲染:
      • React 會(huì)使用一些優(yōu)化手段,而符合優(yōu)化的條件則是「當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)組件的 props 和 context 沒(méi)有發(fā)生變化」并且**當(dāng)前節(jié)點(diǎn)的更新優(yōu)先級(jí)不夠,**如果這兩個(gè)條件均滿足的話可以直接復(fù)制 current 的子節(jié)點(diǎn)并返回。如果不滿足則同首次渲染走一樣的邏輯。
     
     
     
     
    1. if (current !== null) {
    2.     // 這里處理一些依賴
    3.     if (
    4.       enableLazyContextPropagation &&
    5.       !includesSomeLane(renderLanes, updateLanes)
    6.     ) {
    7.       const dependencies = current.dependencies;
    8.       if (dependencies !== null && checkIfContextChanged(dependencies)) {
    9.         updateLanes = mergeLanes(updateLanes, renderLanes);
    10.       }
    11.     }
    12.     const oldProps = current.memoizedProps;
    13.     const newProps = workInProgress.pendingProps;
    14.     if (
    15.       oldProps !== newProps ||
    16.       hasLegacyContextChanged() ||
    17.       // Force a re-render if the implementation changed due to hot reload:
    18.       (__DEV__ ? workInProgress.type !== current.type : false)
    19.     ) {
    20.       // 如果 props 或者 context 變了
    21.       didReceiveUpdate = true;
    22.     } else if (!includesSomeLane(renderLanes, updateLanes)) {
    23.       didReceiveUpdate = false;
    24.       // 走到這里則說(shuō)明符合優(yōu)化條件
    25.       switch (workInProgress.tag) {
    26.         case HostRoot:
    27.           ...
    28.           break;
    29.         case HostComponent:
    30.           ...
    31.           break;
    32.         case ClassComponent: {
    33.           ...
    34.           break;
    35.         }
    36.         case HostPortal:
    37.           ...
    38.           break;
    39.         case ContextProvider: {
    40.           ...
    41.           break;
    42.         }
    43.         ...
    44.         
    45.       }
    46.       return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    47.     } else {
    48.       ...
    49.       didReceiveUpdate = false;
    50.     }
    51.   } else {
    52.     didReceiveUpdate = false;
    53.   }

    更新優(yōu)化策略應(yīng)用

    開(kāi)發(fā)過(guò)程中我們常常希望利用 React 非首次渲染的優(yōu)化策略來(lái)提升性能,如下代碼,B 組件是個(gè)純展示組件且內(nèi)部沒(méi)有依賴任何 Demo 組件的數(shù)據(jù),因此有些同學(xué)可能會(huì)想當(dāng)然認(rèn)為當(dāng) Demo 重新渲染時(shí)這個(gè) B 組件是符合 React 優(yōu)化條件的。但結(jié)果是,每次 Demo 重新渲染都會(huì)導(dǎo)致 B 組件重新渲染。每次渲染時(shí) B 組件的 props 看似沒(méi)發(fā)生變化,但由于 Demo 重新執(zhí)行后會(huì)生成全新的 B 組件(下面會(huì)介紹),所以新舊 B 組件的 props 肯定也是不同的。

     
     
     
     
    1. function App() {
    2.     return 
    3. }
    4. function Demo() {
    5.     const [v, setV] = useState();
    6.     return (
    7.         
    8.             
    9.             
    10.         
      
  •     );
  • }
  • 那有什么辦法可以保持住 B 組件不變嗎,答案是肯定的,我們可以把 B 組件放到 Demo 組件外層,這樣一來(lái),B 組件是在 App 組件中生成并作為 props 傳入 Demo 的,因?yàn)椴还?Demo 組件狀態(tài)怎么變化都不會(huì)影響到 App 組件,因此 App 和 B 組件就只會(huì)在首次渲染時(shí)會(huì)執(zhí)行一遍,也就是說(shuō) Demo 獲取到的 props.children 的引用一直都是指向同一個(gè)對(duì)象,這樣一來(lái) B 組件的 props 也就不會(huì)變化了。

     
     
     
     
    1. function App() {
    2.     return 
    3.         
    4.     
    5. }
    6. function Demo(props) {
    7.     const [v, setV] = useState();
    8.     return (
    9.         
    10.             
    11.            {props.children}
    12.         
      
  •     );
  • }
  • 更新當(dāng)前節(jié)點(diǎn)

    通過(guò)上面的解析我們知道,當(dāng)不走優(yōu)化邏輯時(shí) 「beginWork」 使用大量的 switch...case 來(lái)分別處理不同類型的組件,下來(lái)我們以我們熟悉的 Function Component 為例。

    「核心就是通過(guò)調(diào)用函數(shù)組件,得到組件的返回的 element?!?/p>

    類似地,對(duì)于類組件,則是調(diào)用組件實(shí)例的 render 方法得到 element。

    而對(duì)于我們普通的組件,例如

     ,則是直接取 props.children 即可。

     
     
     
     
    1. function updateFunctionComponent(
    2.   current,
    3.   workInProgress,
    4.   Component,
    5.   nextProps: any,
    6.   renderLanes,
    7. ) {
    8.   let context;
    9.   if (!disableLegacyContext) {
    10.     const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
    11.     context = getMaskedContext(workInProgress, unmaskedContext);
    12.   }
    13.   let nextChildren;
    14.   prepareToReadContext(workInProgress, renderLanes);
    15.   // 執(zhí)行組件函數(shù)獲取返回的 element
    16.   nextChildren = renderWithHooks(
    17.     current,
    18.     workInProgress,
    19.     Component,
    20.     nextProps,
    21.     context,
    22.     renderLanes,
    23.   );
    24.   
    25.   // React DevTools reads this flag.
    26.   workInProgress.flags |= PerformedWork;
    27.   reconcileChildren(current, workInProgress, nextChildren, renderLanes);
    28.   return workInProgress.child;
    29. }

    得到組件返回的 element(s) 之后,下一步就是為他們生成 Fiber,我們查看源碼可以看到,不論是函數(shù)組件或是類組件或是普通組件,最后返回的 element(s) 都會(huì)作為參數(shù)傳入到 「reconcileChildren」 中。

    介紹 「reconcileChildren」 之前我們先用一張圖總結(jié)一下 「beginWork」 的大致流程:

    生成子節(jié)點(diǎn)

    經(jīng)過(guò)上一步得到 workInProgress 的 children 之后,接下來(lái)需要為這些 children element 生成 Fiber ,這就是 「reconcileChildFibers」 函數(shù)做的事情,這也是我們經(jīng)常提到的 diff 的過(guò)程。

    這個(gè)函數(shù)里主要分兩種情況處理,如果是 newChild(即 children element)是 object 類型,則進(jìn)入單節(jié)點(diǎn) diff 過(guò)程(「reconcileSingleElement」),如果是數(shù)組類型,則進(jìn)入多節(jié)點(diǎn) diff 過(guò)程(「reconcileChildrenArray」)。

     
     
     
     
    1. function reconcileChildFibers(
    2.     returnFiber: Fiber,
    3.     currentFirstChild: Fiber | null,
    4.     newChild: any,
    5.     lanes: Lanes,
    6.   ): Fiber | null {
    7.     if (typeof newChild === 'object' && newChild !== null) {
    8.       switch (newChild.$typeof) {
    9.         case REACT_ELEMENT_TYPE:
    10.           return placeSingleChild(
    11.             reconcileSingleElement(
    12.               returnFiber,
    13.               currentFirstChild,
    14.               newChild,
    15.               lanes,
    16.             ),
    17.           );
    18.         ...
    19.       }
    20.       if (isArray(newChild)) {
    21.         return reconcileChildrenArray(
    22.           returnFiber,
    23.           currentFirstChild,
    24.           newChild,
    25.           lanes,
    26.         );
    27.       }
    28.       throwOnInvalidObjectType(returnFiber, newChild);
    29.     }
    30.     
    31. }   

    單節(jié)點(diǎn)diff

     
     
     
     
    1. function reconcileSingleElement(
    2.     returnFiber: Fiber,
    3.     currentFirstChild: Fiber | null,
    4.     element: ReactElement,
    5.     lanes: Lanes,
    6.   ): Fiber {
    7.     const key = element.key;
    8.     let child = currentFirstChild;
    9.     while (child !== null) {
    10.     
    11.       // 首先比較 key 是否相同
    12.       if (child.key === key) {
    13.         const elementType = element.type;
    14.         ...
    15.            // 然后比較 elementType 是否相同
    16.           if (child.elementType === elementType) {
    17.             deleteRemainingChildren(returnFiber, child.sibling);
    18.             const existing = useFiber(child, element.props);
    19.             existing.ref = coerceRef(returnFiber, child, element);
    20.             existing.return = returnFiber;
    21.             return existing;
    22.           }
    23.         
    24.         // Didn't match.
    25.         deleteRemainingChildren(returnFiber, child);
    26.         break;
    27.       } else {
    28.         deleteChild(returnFiber, child);
    29.       }
    30.       // 遍歷兄弟節(jié)點(diǎn),看能不能找到 key 相同的節(jié)點(diǎn)
    31.       child = child.sibling;
    32.     }
    33.     if (element.type === REACT_FRAGMENT_TYPE) {
    34.       const created = createFiberFromFragment(
    35.         element.props.children,
    36.         returnFiber.mode,
    37.         lanes,
    38.         element.key,
    39.       );
    40.       created.return = returnFiber;
    41.       return created;
    42.     } else {
    43.       const created = createFiberFromElement(element, returnFiber.mode, lanes);
    44.       created.ref = coerceRef(returnFiber, currentFirstChild, element);
    45.       created.return = returnFiber;
    46.       return created;
    47.     }
    48.   }
    49.   

    本著盡可能復(fù)用舊節(jié)點(diǎn)的原則,在單節(jié)點(diǎn) diff 在這里,我們會(huì)遍歷舊節(jié)點(diǎn),對(duì)每個(gè)遍歷到的節(jié)點(diǎn)會(huì)做一下兩個(gè)判斷:

    • key 是否相同
    • key 相同的情況下,elementType 是否相同

    延伸下來(lái)有三種情況:

    • 如果 key 不相同,則直接調(diào)用 「deleteChild」 將這個(gè) child 標(biāo)記為刪除,但是我們不用灰心,可能只是我們還沒(méi)有找到那個(gè)對(duì)的節(jié)點(diǎn),所以要繼續(xù)執(zhí)行child = child.sibling;遍歷兄弟節(jié)點(diǎn),直到找到那個(gè)對(duì)的節(jié)點(diǎn)。
    • 如果 key 相同,elementType 相同,那就是最理想的情況,找到了可以復(fù)用的節(jié)點(diǎn),直接調(diào)用 「deleteRemainingChildren」 把剩余的兄弟節(jié)點(diǎn)標(biāo)記刪除,然后直接復(fù)用 child 返回。
    • 如果 key 相同,但 elementType 不同,這是最悲情的情況,我們找到了那個(gè)節(jié)點(diǎn),可惜的是這個(gè)節(jié)點(diǎn)的 elementType 已經(jīng)變了,那我們也不需要再找了,把 child 及其所有兄弟節(jié)點(diǎn)標(biāo)記刪除,跳出循環(huán)。直接創(chuàng)建一個(gè)新的節(jié)點(diǎn)。

    多節(jié)點(diǎn)diff

     
     
     
     
    1. function reconcileChildrenArray(
    2.     returnFiber: Fiber,
    3.     currentFirstChild: Fiber | null,
    4.     newChildren: Array<*>,
    5.     lanes: Lanes,
    6. ) {
    7.     let resultingFirstChild: Fiber | null = null;
    8.     let previousNewFiber: Fiber | null = null;
    9.     let oldFiber = currentFirstChild;
    10.     let lastPlacedIndex = 0;
    11.     let newIdx = 0;
    12.     let nextOldFiber = null;
    13.     for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    14.         const newFiber = updateSlot(
    15.         returnFiber,
    16.         oldFiber,
    17.         newChildren[newIdx],
    18.         lanes,
    19.         );
    20.         if (newFiber === null) {
    21.           break;
    22.         }
    23.           lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    24.           if (previousNewFiber === null) {
    25.             resultingFirstChild = newFiber;
    26.           } else {
    27.             previousNewFiber.sibling = newFiber;
    28.           }
    29.           previousNewFiber = newFiber;
    30.           oldFiber = nextOldFiber;
    31.     }
    32.     if (newIdx === newChildren.length) {
    33.         ...
    34.     }
    35.     if (oldFiber === null) {
    36.         ...
    37.     }
    38.     for (; newIdx < newChildren.length; newIdx++) {
    39.         ...
    40.     }
    41.     return resultingFirstChild;
    42. }
    43. function updateSlot(
    44.     returnFiber: Fiber,
    45.     oldFiber: Fiber | null,
    46.     newChild: any,
    47.     lanes: Lanes,
    48.   ): Fiber | null {
    49.     const key = oldFiber !== null ? oldFiber.key : null;
    50.     ...
    51.     if (newChild.key === key) {
    52.       return updateElement(returnFiber, oldFiber, newChild, lanes);
    53.     } else {
    54.       return null;
    55.     }
    56. }

    從源碼我們可以看到,在 「reconcileChildrenArray」 中,出現(xiàn)了兩個(gè)循環(huán)。

    第一輪循環(huán)中邏輯如下:

    • 同時(shí)遍歷 oldFiber 鏈和 newChildren,判斷 oldFiber 和 newChild 的 key 是否相同。
    • 如果 key 相同。
      • 判斷雙方 elementType 是否相同。
      • 如果相同則復(fù)用 oldFiber 返回。
      • 如果不同則新建 Fiber 返回。
    • 如果 key 不同則直接跳出循環(huán)。

    可以看到第一輪循環(huán)只要碰到新舊的 key 不一樣時(shí)就會(huì)跳出循環(huán),換句話說(shuō),第一輪循環(huán)里做的事情都是基于 key 相同,主要就是「更新」的工作。

    跳出循環(huán)后,要先執(zhí)行兩個(gè)判斷:

    • newChildren 已經(jīng)遍歷完了:這種情況說(shuō)明新的 children 全都已經(jīng)處理完了,只要把 oldFiber 和他所有剩余的兄弟節(jié)點(diǎn)刪除然后返回頭部的 Fiber 即可。
    • 已經(jīng)沒(méi)有 oldFiber :這種情況說(shuō)明 children 有新增的節(jié)點(diǎn),給這些新增的節(jié)點(diǎn)逐一構(gòu)建 Fiber 并鏈接上,然后返回頭部的 Fiber 即可。

    如果以上兩種情況都不是,則進(jìn)入第二輪循環(huán)。

    在執(zhí)行第二輪循環(huán)之前,先把剩下的舊節(jié)點(diǎn)和他們對(duì)應(yīng)的 key 或者 index 做成映射,方便查找。

    第二輪循環(huán)沿用了第一輪循環(huán)的 newIdx 變量,說(shuō)明第二輪循環(huán)是在第一輪循環(huán)結(jié)束的地方開(kāi)始再次遍歷剩下的 newChildren。

     
     
     
     
    1.   const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    2.   for (; newIdx < newChildren.length; newIdx++) {
    3.     const newFiber = updateFromMap(
    4.       existingChildren,
    5.       returnFiber,
    6.       newIdx,
    7.       newChildren[newIdx],
    8.       lanes,
    9.     );
    10.     if (newFiber !== null) {
    11.       if (shouldTrackSideEffects) {
    12.         if (newFiber.alternate !== null) {
    13.           existingChildren.delete(
    14.             newFiber.key === null ? newIdx : newFiber.key,
    15.           );
    16.         }
    17.       }
    18.       lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    19.       if (previousNewFiber === null) {
    20.         resultingFirstChild = newFiber;
    21.       } else {
    22.         previousNewFiber.sibling = newFiber;
    23.       }
    24.       previousNewFiber = newFiber;
    25.     }
    26.   }
    27.   
    28.   
    29.   function placeChild(
    30.   newFiber: Fiber,
    31.   lastPlacedIndex: number,
    32.   newIndex: number,
    33. ): number {
    34.   newFiber.index = newIndex;
    35.   if (!shouldTrackSideEffects) {
    36.     // Noop.
    37.     return lastPlacedIndex;
    38.   }
    39.   const current = newFiber.alternate;
    40.   if (current !== null) {
    41.     const oldIndex = current.index;
    42.     if (oldIndex < lastPlacedIndex) {
    43.       // This is a move.
    44.       newFiber.flags |= Placement;
    45.       return lastPlacedIndex;
    46.     } else {
    47.       // This item can stay in place.
    48.       return oldIndex;
    49.     }
    50.   } else {
    51.     // This is an insertion.
    52.     newFiber.flags |= Placement;
    53.     return lastPlacedIndex;
    54.   }
    55. }

    第二輪循環(huán)主要調(diào)用了 「updateFromMap」 來(lái)處理節(jié)點(diǎn),在這里需要用 newChild 的 key 去 existingChildren 中找對(duì)應(yīng)的 Fiber。

    • 能找到 key 相同的,則說(shuō)明這個(gè)節(jié)點(diǎn)只是位置變了,是可以復(fù)用的。
    • 找不到 key 相同的,則說(shuō)明這個(gè)節(jié)點(diǎn)應(yīng)該是新增的。

    不管是復(fù)用還是新增,「updateFromMap」 都會(huì)返回一個(gè) newFiber,然后我們需要為這個(gè) newFiber 更新一下它的位置(index),但是僅僅更新這個(gè) Fiber 的 index 還不夠,因?yàn)檫@個(gè) Fiber 有可能是復(fù)用的,如果是復(fù)用的就意味著它已經(jīng)有對(duì)應(yīng)的真實(shí) DOM 節(jié)點(diǎn)了,我們還需要復(fù)用它的真實(shí) DOM,因此需要對(duì)應(yīng)更新這個(gè) Fiber 的 flag,但是真的需要對(duì)每個(gè) Fiber 都去設(shè)置 flag 嗎,我們舉個(gè)例子:

     
     
     
     
    1. // 舊
    2. []
    3. // 新
    4. []

    如果按照我們剛剛說(shuō)的做法,這里的 a, b, c 都會(huì)被打上 flag,這樣一來(lái),在 commit 階段,這三個(gè) DOM 都會(huì)被移動(dòng),可是我們知道,這里顯然只需要移動(dòng)一個(gè)節(jié)點(diǎn)即可,退一萬(wàn)步說(shuō)我們移動(dòng)兩個(gè)節(jié)點(diǎn)也比移動(dòng)所有節(jié)點(diǎn)要來(lái)的聰明。

    其實(shí)在這個(gè)問(wèn)題上主要就是我們得區(qū)分一下到底哪個(gè)節(jié)點(diǎn)才是移動(dòng)了的,這就需要一個(gè)參照點(diǎn),我們要保證在參照點(diǎn)左邊都是已經(jīng)排好順序了的。而這個(gè)參照點(diǎn)就是 lastPlacedIndex。有了它,我們?cè)诒闅v newChildren 的時(shí)候可能會(huì)出現(xiàn)下面兩種情況:

    • 生成(或復(fù)用)的 Fiber 對(duì)應(yīng)的老 index < lastPlacedIndex,這就說(shuō)明這個(gè) Fiber 的位置不對(duì),因?yàn)?lastPlacedIndex 左邊的應(yīng)該全是已經(jīng)遍歷過(guò)的 newChild 生成的 Fiber。因此這個(gè) Fiber 是需要被移動(dòng)的,打上 flag。
    • 如果 Fiber 對(duì)應(yīng)的老 index >= lastPlacedIndex,那就說(shuō)明這個(gè) Fiber 的相對(duì)位置是 ok 的,可以不用移動(dòng),但是我們需要更新一下參照點(diǎn),把參照點(diǎn)更新成這個(gè) Fiber 對(duì)應(yīng)的老 index。

    我們舉一個(gè)例子:

           
     
     
     
    1. // 舊
    2. []
    3. // 新
    4. []

    lastPlacedIndex 初始值為 0,

    首先處理
    分享文章:一文讀懂React組件渲染核心原理
    網(wǎng)址分享:http://m.5511xx.com/article/dpjoepc.html