寫給本身看的React源碼解析(一):你的React代碼是怎麼渲染成DOM的?

前言

最近開始深刻學習React的原理了,後面會出一系列關於React原理的文章,基本都是我學習其餘前輩的React源碼分析以及跟隨他們閱讀源碼時的一些思考和記錄,內容大部分非原創,但我會用我本身的方式去總結原理以及相關的流程,並加以補充,看成本身的學習總結。html

本系列內容偏向底層源碼實現,若是你是React新手,不建議你細看。react

本文的內容是關於React首次渲染的流程。git

首次渲染中,React代碼變成DOM的流程

這裏主要有兩個步驟,第一個是從JSX代碼通過React.createElement方法變成一個虛擬DOM,第二個步驟是經過ReactDOM.render方法把虛擬DOM變成真實DOMgithub

React.createElement

對於 React.createElement方法,不少新人可能並不知道,由於咱們在通常的業務邏輯中,比較少會直接使用這個方法。其實在React官網中,就已經有寫明瞭。數組

JSX 會被編譯爲 React.createElement(), React.createElement() 將返回一個叫做「React Element」的 JS 對象。babel

咱們在babel官網裏能夠寫個JSX試一下。 markdown

JSX在通過babel的編譯以後,變成了嵌套的React.createElementJSX 的本質其實就是React.createElement這個 JavaScript 調用的語法糖。 有了JSX 語法糖的存在,咱們可使用咱們最爲熟悉的類 HTML 標籤語法來建立虛擬 DOM,在下降學習成本的同時,也提高了研發效率與研發體驗。數據結構

接下來,咱們來看下createElement的源碼架構

export function createElement(type, config, children) {
  // propName 變量用於儲存後面須要用到的元素屬性
  let propName; 
  // props 變量用於儲存元素屬性的鍵值對集合
  const props = {}; 
  // key、ref、self、source 均爲 React 元素的屬性
  let key = null;
  let ref = null; 
  let self = null; 
  let source = null; 

  // config 對象中存儲的是元素的屬性
  if (config != null) { 
    // 進來以後作的第一件事,是依次對 ref、key、self 和 source 屬性賦值
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    // 此處將 key 值字符串化
    if (hasValidKey(config)) {
      key = '' + config.key; 
    }
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 接着就是要把 config 裏面的屬性都一個一個挪到 props 這個以前聲明好的對象裏面
    for (propName in config) {
      if (
        // 篩選出能夠提進 props 對象裏的屬性
        hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) 
      ) {
        props[propName] = config[propName]; 
      }
    }
  }
  // childrenLength 指的是當前元素的子元素的個數,減去的 2 是 type 和 config 兩個參數佔用的長度
  const childrenLength = arguments.length - 2; 
  // 若是拋去type和config,就只剩下一個參數,通常意味着文本節點出現了
  if (childrenLength === 1) { 
    // 直接把這個參數的值賦給props.children
    props.children = children; 
    // 處理嵌套多個子元素的狀況
  } else if (childrenLength > 1) { 
    // 聲明一個子元素數組
    const childArray = Array(childrenLength); 
    // 把子元素推動數組裏
    for (let i = 0; i < childrenLength; i++) { 
      childArray[i] = arguments[i + 2];
    }
    // 最後把這個數組賦值給props.children
    props.children = childArray; 
  } 

  // 處理 defaultProps
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) { 
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 最後返回一個調用ReactElement執行方法,並傳入剛纔處理過的參數
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}
複製代碼

總結一下這個函數作的一些事情app

  • 1.二次處理key,ref,self,source四個值(將key字符串化,將config中的ref賦值給refselfsource暫不太清楚其功能,能夠忽略,17版本的jsx方法直接刪除了這兩個參數
  • 2.遍歷config,篩選出能夠賦值到props裏的屬性
  • 3.提取子元素,賦值到props.children中(若是隻有一個子元素,就直接賦值,若是子元素大於一個,就以數組形式存儲)
  • 4.格式化defaultProps(若是沒有傳入相關的propsprops就取設置的默認值)
  • 5.返回一個ReactElement方法,並傳入剛纔處理的參數

createElement其實就是一個數據處理器,把從JSX獲取到的內容進行格式化,再傳入ReactElement方法中。

注意:在React 17版本當中,createElement會被替換成jsx(源碼地址)方法。

import React from 'react'; // 在17版本中,能夠不引入這句
function App() {
  return <h1>Hello World</h1>;
}
// createElement
function App() {
  return React.createElement('h1', null, 'Hello world');
}
// 17版本中的jsx
import {jsx as _jsx} from 'react/jsx-runtime'; // 由編譯器引入
function App() {
  // 子元素將直接編譯成config對象裏的children屬性,jsx再也不接收單獨的子元素入參
  return _jsx('h1', { children: 'Hello world' });
}
複製代碼

ReactElement

咱們來看看ReactElement方法作了哪些事,源碼地址

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT_ELEMENT_TYPE是一個常量,用來標識該對象是一個ReactElement
    $$typeof: REACT_ELEMENT_TYPE,

    // 內置屬性賦值
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 記錄創造該元素的組件
    _owner: owner,
  };

  // 
  if (__DEV__) {
    // 省略這些沒必要要的代碼
  }

  return element;
};
複製代碼

ReactElement方法也很簡單,其實就是經過這些傳入的參數,建立了一個對象,並把它返回出來。這個ReactElement對象實例就是createElement方法最終返回出的內容,它就是React虛擬DOM的一個節點。虛擬DOM其實本質上就是一個存儲了不少用來描述DOM的屬性的對象。

這裏你要注意,由於每一個節點都會被createElement方法調用,因此最終返回回來的應該是一個虛擬DOM的樹。

const App = (
  <div className="App"> <h2 className="title">title</h2> <p className="text">text</p> </div>
);
console.log(App);
複製代碼

如上圖所示,全部的節點都會被編譯成ReactElement對象實例(虛擬DOM)。

ReactDOM.render

虛擬DOM有了,但咱們最終的目的仍是要把內容渲染到頁面上,因此咱們還須要經過ReactDOM.render方法把虛擬DOM渲染成真實的DOM。這一塊內容比較多,我會一個函數一個函數的分開討論。

關於爲何要使用虛擬DOM,虛擬DOM有哪些優點,這些內容不在本文的討論範圍內容,網上相關的文章不少,你們能夠自行去了解一下。

三種React啓動方式

React的16版本以及17版本中,一直都有三種啓動方式

  • legacy 模式,ReactDOM.render(<App />, rootNode)。目前經常使用的模式,渲染過程是同步的
  • blocking 模式,ReactDOM.createBlockingRoot(rootNode).render(<App />)。過渡的模式,基本不多用
  • concurrent 模式,ReactDOM.createRoot(rootNode).render(<App />)。異步渲染的模式,還可使用一些新的特性,目前還在實驗中。

官方文檔

咱們是解析ReactDOM.render的渲染流程,因此其實分析的是一個同步的流程。關於concurrent的異步渲染流程,時間分片以及優先級,其實也是在同步渲染的基礎上作的一些修改,這些內容我會在本系列後續的文章中再總結。

雖說是一個同步的流程,可是在React 16版本的時候,已經把整個的渲染鏈路重構成了Fiber的結構。Fiber架構在 React中並不可以和異步渲染畫嚴格的等號,它是一種同時兼容了同步渲染與異步渲染的設計。

首次render的三個階段

ReactDOM.render 方法對應的調用棧很深,涉及到的函數方法也不少,不過咱們能夠只須要看一些關鍵的邏輯,瞭解大體的流程便可。

首次render能夠大致上分紅三個階段

  • 初始化階段,完成Fiber樹中基本實體的建立。從調用ReactDOM.render開始,到scheduleUpdateOnFiber方法調用performSyncWorkOnRoot結束。
  • render階段,構建和完善Fiber樹。從performSyncWorkOnRoot方法開始,到commitRoot方法結束。
  • commit階段, 遍歷Fiber樹,把Fiber節點映射爲DOM節點並渲染到頁面上。從commitRoot方法開始,到渲染結束。

上面幾個方法如今看不懂不要緊,接下來會一步步帶你瞭解。只須要有個大體的印象,知道這三個階段分別完成的事情。

初始化階段

如今咱們開始初始化階段,在這個階段,上文中已經說,主要就是完成Fiber樹中基本實體的建立。可是咱們須要知道什麼是基本實體?有哪些?咱們從源碼中去尋找答案。

legacyRenderSubtreeIntoContainer

咱們只看關鍵的邏輯,咱們先來看看ReactDOM.render中調用的legacyRenderSubtreeIntoContainer方法(源碼地址)。

// ReactDOM.render中的調用
  return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);

  // legacyRenderSubtreeIntoContainer源碼
  function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
  // container 對應的是咱們傳入的真實 DOM 對象
  var root = container._reactRootContainer;
  // 初始化 fiberRoot 對象
  var fiberRoot;
  // DOM 對象自己不存在 _reactRootContainer 屬性,所以 root 爲空
  if (!root) {
    // 若 root 爲空,則初始化 _reactRootContainer,並將其值賦值給 root
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
    // legacyCreateRootFromDOMContainer 建立出的對象會有一個 _internalRoot 屬性,將其賦值給 fiberRoot
    fiberRoot = root._internalRoot;

    // 這裏處理的是 ReactDOM.render 入參中的回調函數,你瞭解便可
    if (typeof callback === 'function') {
      var originalCallback = callback;
      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    } // Initial mount should not be batched.
    // 進入 unbatchedUpdates 方法
    unbatchedUpdates(function () {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    // else 邏輯處理的是非首次渲染的狀況(即更新),其邏輯除了跳過了初始化工做,與樓上基本一致
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      var _originalCallback = callback;
      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);
        _originalCallback.call(instance);
      };
    } // Update

    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}
複製代碼

這個函數主要作了下面幾步

  • 1.調用legacyCreateRootFromDOMContainer方法建立了container._reactRootContainer並賦值給root
  • 2.將root_internalRoot屬性賦值給fiberRoot
  • 3.將fiberRoot與一些其餘參數傳入updateContainer方法
  • 4.把updateContainer的回調內容做爲參數傳入unbatchedUpdates方法

這裏的fiberRoot的本質是一個FiberRootNode對象,它的關聯對象是真實DOM的容器節點,這個對象裏有一個current對象 如上圖,這個current對象是一個FiberNode實例,其實它就是一個Fiber節點,並且她仍是當前Fiber樹的頭部節點。fiberRoot和它下面的current對象這兩個節點,將是後續整棵Fiber樹構建的起點。

unbatchedUpdates

接下來咱們看看unbatchedUpdates方法(源碼地址)。

function unbatchedUpdates(fn, a) {
  // 這裏是對上下文的處理,沒必要糾結
  var prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  try {
    // 重點在這裏,直接調用了傳入的回調函數 fn,對應當前鏈路中的 updateContainer 方法
    return fn(a);
  } finally {
    // finally 邏輯裏是對回調隊列的處理,此處不用太關注
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}
複製代碼

這個方法比較簡單,其實就是直接調用了傳入的回調函數fn。而fn,是在legacyRenderSubtreeIntoContainer中傳入的

unbatchedUpdates(function () {
  updateContainer(children, fiberRoot, parentComponent, callback);
});
複製代碼

因此咱們再來看updateContainer方法

updateContainer

先來看源碼(源碼地址),我會刪除不少無關的邏輯。

function updateContainer(element, container, parentComponent, callback) {
  // 這個 current 就是以前說的當前`Fiber`樹的頭部節點
  const current = container.current;
  // 這是一個 event 相關的入參,此處沒必要關注
  var eventTime = requestEventTime();
  // 這是一個比較關鍵的入參,lane 表示優先級
  var lane = requestUpdateLane(current);
  // 結合 lane(優先級)信息,建立 update 對象,一個 update 對象意味着一個更新
  var update = createUpdate(eventTime, lane); 

  // update 的 payload 對應的是一個 React 元素
  update.payload = {
    element: element
  };

  // 處理 callback,這個 callback 其實就是咱們調用 ReactDOM.render 時傳入的 callback
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    {
      if (typeof callback !== 'function') {
        error('render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback);
      }
    }
    update.callback = callback;
  }

  // 將 update 入隊
  enqueueUpdate(current, update);
  // 調度 fiberRoot 
  scheduleUpdateOnFiber(current, lane, eventTime);
  // 返回當前節點(fiberRoot)的優先級
  return lane;
}
複製代碼

這個方法裏的邏輯有點複雜,總的來講能夠分爲三點

  • 1.請求當前Fiber節點的lane(優先級)
  • 2.結合lane(優先級),建立當前Fiber節點的update對象,並將其入隊
  • 3.調度當前節點(rootFiber)進行更新

不過由於本文講解的首次渲染鏈路是同步的,優先級意義不大,因此咱們能夠直接看看調度節點的方法scheduleUpdateOnFiber

scheduleUpdateOnFiber

這個方法內容有點長,我只列出關鍵邏輯(源碼地址)。

// 若是是同步的渲染,將進入這個條件。若是是異步渲染的模式,將進入它的else邏輯中 
 // React 是經過 fiber.mode 來區分不一樣的渲染模式
 if (lane === SyncLane) {
    if (
      // 判斷當前是否運行在 unbatchedUpdates 方法裏
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // 判斷當前是否已經 render
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      schedulePendingInteractions(root, lane);

      // 咱們要關注的關鍵步驟,從這個方法開始。開啓 render 階段
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);
      if (executionContext === NoContext) {
        // Flush the synchronous work now, unless we're already working or inside
        // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
        // scheduleCallbackForFiber to preserve the ability to schedule a callback
        // without immediately flushing it. We only do this for user-initiated
        // updates, to preserve historical behavior of legacy mode.
        resetRenderTimer();
        flushSyncCallbackQueue();
      }
    }
  }
複製代碼

在以前的步驟中,React已經完成Fiber樹中基本實體的建立,其實就是以前幾節說的fiberRoot和它下面的current對象這兩個節點。在這個方法中,咱們只須要關注performSyncWorkOnRoot方法,從它開始,咱們將進入render階段。

render階段

render階段要作的事情是構建和完善Fiber樹,其實就是以fiberRoot和它下面的current對象這兩個節點爲頂節點,不斷的遍歷,把他們的子元素的Fiber樹構建出來。

咱們先來看performSyncWorkOnRoot方法。

performSyncWorkOnRoot

performSyncWorkOnRoot源碼地址

這裏重點看兩個邏輯

exitStatus = renderRootSync(root, lanes);
...
commitRoot(root);
複製代碼

renderRootSync方法是render階段開始的標誌,而下面的commitRootcommit階段開始的標誌。咱們先進入的是render階段,因此咱們先看renderRootSync裏的流程。

renderRootSync源碼地址

這個方法裏須要看兩個邏輯

prepareFreshStack(root, lanes);
...
workLoopSync();
複製代碼

咱們先走prepareFreshStack的流程,等它走完了,再進入workLoopSync的遍歷流程。

prepareFreshStack源碼地址

prepareFreshStack的做用是重置一個新的堆棧環境,咱們也只須要關注一個邏輯

workInProgress = createWorkInProgress(root.current, null);
複製代碼

createWorkInProgress是一個比較重要的方法,咱們詳細看一下。

createWorkInProgress

精簡後的源碼以下,源碼地址

// 這裏入參中的 current 傳入的是現有樹結構中的 rootFiber 對象
function createWorkInProgress(current, pendingProps) {
  var workInProgress = current.alternate;
  // ReactDOM.render 觸發的首屏渲染將進入這個邏輯
  if (workInProgress === null) {
    // 這是須要你關注的第一個點,workInProgress 是 createFiber 方法的返回值
    workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    // 這是須要你關注的第二個點,workInProgress 的 alternate 將指向 current
    workInProgress.alternate = current;
    // 這是須要你關注的第三個點,current 的 alternate 將反過來指向 workInProgress
    current.alternate = workInProgress;
  } else {
    // else 的邏輯此處先不用關注
  }

  // 如下省略大量 workInProgress 對象的屬性處理邏輯
  // 返回 workInProgress 節點
  return workInProgress;
}
複製代碼

這裏事先說明一下,入參current就是以前的fiberRoot對象下的current對象。

總結一下createWorkInProgress方法作的事情

  • 1.調用createFiberworkInProgresscreateFiber方法的返回值
  • 2.把workInProgressalternate將指向current
  • 3.把currentalternate將反過來指向workInProgress
  • 4.最終返回一個workInProgress節點

這裏的createFiber方法,顧名思義,就是用來建立一個Fiber節點的方法。入參都是current的值,因此,workInProgress節點其實就是current節點的副本。這時候整顆樹的結構應該以下所示:

workInProgress樹頂點建立完成了,如今運行以前renderRootSync方法裏第二個關鍵邏輯workLoopSync

workLoopSync

這個方法很簡單,就是個遍歷的功能

function workLoopSync() {
  // 若 workInProgress 不爲空
  while (workInProgress !== null) {
    // 針對它執行 performUnitOfWork 方法
    performUnitOfWork(workInProgress);
  }
}
複製代碼

由於後面列出的方法,都是workLoopSync中不斷遍歷的,因此在解析performUnitOfWork方法及其子方法以前,我要先對整個遍歷的流程作一個大體的總結,有了一個大體的瞭解以後再去分析裏面的方法。

workLoopSync作的事情就是經過while循環反覆判斷workInProgress是否爲空,並在不爲空的狀況下針對它執行performUnitOfWork函數。而 performUnitOfWork函數將觸發beginWork的調用,建立新的Fiber節點。若beginWork所建立的Fiber節點不爲空,則performUniOfWork會用這個新的Fiber節點來更新workInProgress的值,爲下一次循環作準備。

workInProgress爲空時,意味着已經完成對整棵Fiber樹的構建。

在這個過程當中,每個被建立出來的新Fiber節點,都會掛載爲以前的workInProgress樹的後代節點。咱們一步步來看一下。

performUnitOfWork

源碼地址

next = beginWork(current, unitOfWork, subtreeRenderLanes);
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
複製代碼

performUnitOfWork裏其實存在有兩個流程,一個是beginWork流程(建立新的Fiber節點),還有一個completeWork流程(當beginWork遍歷到當前分支的葉子節點時,next === null,運行completeWork流程),來負責處理Fiber節點到DOM節點的映射邏輯。

咱們先來看beginWork流程

beginWork

beginWork代碼有400多行,實在太多了,只取一些關鍵邏輯。源碼地址

function beginWork(current, workInProgress, renderLanes) {
  ......
  // current 節點不爲空的狀況下,會加一道辨識,看看是否有更新邏輯要處理
  if (current !== null) {
    // 獲取新舊 props
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;

    // 若 props 更新或者上下文改變,則認爲須要"接受更新"
    if (oldProps !== newProps || hasContextChanged() || (
     workInProgress.type !== current.type )) {
      // 打個更新標
      didReceiveUpdate = true;
    } else if (xxx) {
      // 不須要更新的狀況 A
      return A
    } else {
      if (須要更新的狀況 B) {
        didReceiveUpdate = true;
      } else {
        // 不須要更新的其餘狀況,這裏咱們的首次渲染就將執行到這一行的邏輯
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  } 
  ......
  // 這坨 switch 是 beginWork 中的核心邏輯,原有的代碼量至關大
  switch (workInProgress.tag) {
    ......
    // 這裏省略掉大量形如"case: xxx"的邏輯
    // 根節點將進入這個邏輯
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes)
    // dom 標籤對應的節點將進入這個邏輯
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes)

    // 文本節點將進入這個邏輯
    case HostText:
      return updateHostText(current, workInProgress)
    ...... 
    // 這裏省略掉大量形如"case: xxx"的邏輯
  }
}
複製代碼

beginWork的核心邏輯是根據fiber節點(workInProgress樹下的節點)的tag屬性(表明當前fiber屬於什麼類型的標籤)的不一樣,調用不一樣的節點建立函數。

這些節點建立函數,最終都會經過調用reconcileChildren方法,生成當前節點的子節點。

reconcileChildren(beginWork流程)

這個方法也比較簡單

function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
  // 判斷 current 是否爲 null
  if (current === null) {
    // 若 current 爲 null,則進入 mountChildFibers 的邏輯
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    // 若 current 不爲 null,則進入 reconcileChildFibers 的邏輯
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  }
}
複製代碼

上面的兩個方法,咱們也能夠找到賦值的地方

var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);
複製代碼

這兩個方法都是經過ChildReconciler方法建立出來的,只是入參有所區別

ChildReconciler(beginWork流程)

ChildReconciler的代碼量也很大,代碼就不放了,源碼地址

這個方法裏包含了不少關於Fiber節點的建立、增長、刪除、修改等操做的函數,用來給其餘函數調用。返回值是一個名爲reconcileChildFibers的函數,這個函數是一個邏輯分發器,它將根據入參的不一樣,執行不一樣的Fiber節點操做,最終返回不一樣的目標Fiber節點。

還有一個很重要的邏輯,這個方法會根據入參shouldTrackSideEffects來決定「是否須要追蹤反作用」,reconcileChildFibersmountChildFibers的不一樣,主要在於對反作用的處理不一樣。shouldTrackSideEffectstrue的話,會給新建立的這個Fiber節點添加一個flags屬性(17版本以前,這個屬性名是effectTag),並賦值一個常量。

若是是根節點,會賦值一個Placement常量,這是一個二進制常量,目的是在渲染真實DOM的時候告訴渲染器,處理這個fiber節點時是須要新增DOM節點的。 這種類型的常量還有不少,源碼地址

這裏先給一個demo,後續的編譯都會以這個demo來實現。

function App() {
    return (
      <div className="App"> <div className="container"> <h1>我是標題</h1> <p>我是第一段話</p> <p>我是第二段話</p> </div> </div>
    );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
複製代碼

回到咱們剛纔的渲染鏈路中來,由於本次循環是第一次,處理的是current樹和workInProgress樹的頂部節點,因此current是存在的,會進入reconcileChildFibers方法中,它是容許追蹤反作用的。由於當前的workInProgress是頂部節點,它是沒有一個確切的ReactElement與之映射,因此它會做爲是JSX中根組件的父節點,就是這個App組件的父節點。而後會基於App組件的ReactElement(jsx編譯後的虛擬DOM)對象信息,建立其對應的FiberNode,並給它打上Placement(新增)的反作用標記, 返回給workInProgress.child

這樣就將JSX根組件的Fiber與以前建立的Fiber樹頂點關聯起來了,以下圖。

這樣,第一次循環完成,由於App還有子元素,因此beginWork中返回的workInProgress不爲null(workInProgress其實就是這些jsx節點編譯以後的fiber節點),workLoopSync還會繼續循環。最終的樹結構以下

咱們來看看這些標籤的fiber節點對象。

上圖分別是App節點,兩個div子節點,以及p標籤的節點。能夠看到,每個非文本類型的ReactElement都有了它對應的Fiber節點。

這些節點之間都是相互有聯繫的,它們是經過childreturnsibling這 3 個屬性創建關係,其中 childreturn記錄的是父子節點關係,而sibling記錄的則是兄弟節點關係(sibling 指向的是當前節點的第 1 個兄弟節點)。

具體看下圖:

以上即是workInProgress Fiber樹的最終形態了。從圖中能夠看出,雖然人們習慣上仍然將眼前的這個產物稱爲Fiber樹,但它的數據結構本質其實已經從樹變成了鏈表。

下面來看另外一個completeWork流程。

completeUnitOfWork(completeWork流程)

上文曾經說過,performUnitOfWork中在beginWork流程遍歷到葉子節點以後,next就會變成null,當次beginWork流程結束,進入相對應的``completeWork`流程。

從新引用一下上面用到的performUnitOfWork方法裏的代碼

next = beginWork(current, unitOfWork, subtreeRenderLanes);
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
複製代碼

completeUnitOfWork是一個遍歷循環的方法,將會遍歷循環下面幾件事

  • 1.調用completeWork方法
  • 2.將當前節點的反作用鏈(EffectList)插入到其父節點的反作用鏈(EffectList)中
  • 3.以當前節點爲起點,循環遍歷其兄弟節點及其父節點。當遍歷到兄弟節點時,將return掉當前調用,觸發兄弟節點對應的performUnitOfWork邏輯;而遍歷到父節點時,則會直接進入下一輪循環,也就是重複 一、2 的邏輯

咱們先來看completeWork方法。

completeWork(completeWork流程)

completeWork源碼地址

completeWork也是一個體量比較大的函數,咱們只抽離關鍵的邏輯

function completeWork(current, workInProgress, renderLanes) {
  // 取出 Fiber 節點的屬性值,存儲在 newProps 裏
  var newProps = workInProgress.pendingProps;

  // 根據 workInProgress 節點的 tag 屬性的不一樣,決定要進入哪段邏輯
  switch (workInProgress.tag) {
    ......
    // h1 節點的類型屬於 HostComponent,所以這裏爲你講解的是這段邏輯
    case HostComponent:
      {
        popHostContext(workInProgress);
        var rootContainerInstance = getRootHostContainer();
        var type = workInProgress.type;
        // 判斷 current 節點是否存在,由於目前是掛載階段,所以 current 節點是不存在的
        if (current !== null && workInProgress.stateNode != null) {
          updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
          if (current.ref !== workInProgress.ref) {
            markRef(workInProgress);
          }
        } else {
          // 針對異常狀況進行 return 處理
          ......
          // 接下來就爲 DOM 節點的建立作準備了
          var currentHostContext = getHostContext();
          // _wasHydrated 是一個與服務端渲染有關的值,這裏不用關注
          var _wasHydrated = popHydrationState(workInProgress);

          // 判斷是不是服務端渲染
          if (_wasHydrated) {
           ......
          } else {
            // 這一步很關鍵, createInstance 的做用是建立 DOM 節點
            var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
            // appendAllChildren 會嘗試把上一步建立好的 DOM 節點掛載到 DOM 樹上去
            appendAllChildren(instance, workInProgress, false, false);
            // stateNode 用於存儲當前 Fiber 節點對應的 DOM 節點
            workInProgress.stateNode = instance; 

            // finalizeInitialChildren 用來爲 DOM 節點設置屬性
            if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
              markUpdate(workInProgress);
            }
          }
          ......
        }
        return null;
      }
    case HostText:
      {
        ......
      }
    case SuspenseComponent:
      {
        ......
      }
    case HostPortal:
      ......
      return null;
    case ContextProvider:
      ......
      return null;
    ......
  }
}
複製代碼

首先咱們須要知道,進入這個completeWork的參數是什麼,咱們知道只有當beginWork結束,也就是遍歷到第一個葉子節點的時候,纔會進入completeWork方法。因此,第一次運行的時候的參數,實際上是demo中的h1標籤相對應的fiber節點對象。這也是completeWork的一個特色,是嚴格自底向上運行的。

而後咱們再來看completeWork方法幾個功能要點

  • 1.completeWork的核心邏輯是一段體量巨大的switch語句,在這段switch語句中,completeWork將根據workInProgress節點的tag屬性的不一樣,進入不一樣的DOM節點的建立、處理邏輯。
  • 2.在Demo示例中,h1節點的tag屬性對應的類型應該是HostComponent,也就是原生DOM元素類型。
  • 3.completeWork中的currentworkInProgress就是以前說的current樹和workInProgress樹上面的節點。

其中workInProgress樹表明的是「當前正在render中的樹」,而current樹則表明「已經存在的樹」。

workInProgress節點和current節點之間用alternate屬性相互鏈接。在組件的掛載階段,current樹只有一個頂部節點,並無其餘內容。所以h1這個workInProgress節點對應的current節點是null

帶着這個前提,咱們再來看看completeWork方法,咱們能夠總結出

completeWork其實就是負責處理Fiber節點到DOM節點的映射邏輯。經過三個步驟

  • 1.建立DOM節點(CreateInstance
  • 2.將DOM節點插入到 DOM 樹中(AppendAllChildren),賦值給workInProgress節點的stateNode屬性(並且當前節點運行AppendAllChildren時,會逐個向下查找本身的後代子 Fiber 節點,並把所對應的 DOM 節點掛載到其父 Fiber 節點所對應的 DOM 節點裏去,因此最上級的節點裏的stateNode屬性,就是一個完整的dom樹
  • 3.爲DOM節點設置屬性(FinalizeInitialChildren

completeUnitOfWork第2,3步(completeWork流程)

先來看第三步的代碼實現

以當前節點爲起點,循環遍歷其兄弟節點及其父節點。當遍歷到兄弟節點時,將return掉當前調用,觸發兄弟節點對應的performUnitOfWork邏輯;而遍歷到父節點時,則會直接進入下一輪循環,也就是重複 一、2 的邏輯

do {
  ......
  // 這裏省略步驟 1 和步驟 2 的邏輯 

  // 獲取當前節點的兄弟節點
  var siblingFiber = completedWork.sibling;

  // 若兄弟節點存在
  if (siblingFiber !== null) {
    // 將 workInProgress 賦值爲當前節點的兄弟節點
    workInProgress = siblingFiber;
    // 將正在進行的 completeUnitOfWork 邏輯 return 掉
    return;
  } 

  // 若兄弟節點不存在,completeWork 會被賦值爲 returnFiber,也就是當前節點的父節點
  completedWork = returnFiber; 
    // 這一步與上一步是相輔相成的,上下文中要求 workInProgress 與 completedWork 保持一致
  workInProgress = completedWork;
} while (completedWork !== null);
複製代碼

功能比較簡單,按demo來講,由於beginWork流程是一個深度優先遍歷,當遍歷到h1標籤時,遍歷中斷,開始執行completedWork流程。h1的兄弟節點p標籤,其實連beginWork流程尚未運行過,因此須要從新調用performUnitOfWork邏輯。

咱們再來講一下第二步

將當前節點的反作用鏈(EffectList)插入到其父節點的反作用鏈(EffectList)中。

這一步的目標其實就是找出界面中須要處理的更新。由於在實際的操做中,並非全部的節點上都會產生須要處理的更新。好比在掛載階段,對整棵workInProgress樹遞歸完畢後,React會發現實際只須要對App節點執行一個掛載操做就能夠了;而在更新階段,這種現象更爲明顯。

怎樣作才能讓渲染器又快又好地定位到那些真正須要更新的節點呢?這就是反作用鏈(effectList)的功能。

每一個Fiber節點都維護着一個屬於它本身的effectListeffectList在數據結構上以鏈表的形式存在,鏈表內的每個元素都是一個Fiber節點。這些Fiber節點須要知足兩個共性:

  • 都是當前Fiber節點的後代節點(並不是它自身的更新,而是其須要更新的後代節點)
  • 都有待處理的反作用

這個effectList鏈表在Fiber節點中是經過firstEffectlastEffect來維護。firstEffect表示effectList的第一個節點,而lastEffect則記錄最後一個節點。

由於completeWork是自底向上執行的,因此在頂部節點上能夠拿到一個存儲了當前Fiber樹全部effect Fiber

按demo來講,只有頂部的節點纔會存在反作用鏈(App組件的fiber節點),對於App組件內的全部子節點都不存在反作用鏈。當首次渲染或者更新的時候,渲染器只會去處理反作用鏈上的App fiber節點(App做爲一個最小的更新組件,已經包含了內部子元素的dom節點)。固然若是App裏面還引用了其餘組件,App組件的fiber中也會包含該組件的反作用鏈。

commit階段

commit會在performSyncWorkOnRoot中被調用,它是一個絕對同步的過程。

commitRoot(root);
複製代碼

源碼地址

從流程上來講,commi共分爲 3 個階段:before mutationmutationlayout

  • before mutation 階段,這個階段DOM節點尚未被渲染到界面上去,過程當中會觸發 getSnapshotBeforeUpdate,也會處理useEffect鉤子相關的調度邏輯。

  • mutation,這個階段負責DOM節點的渲染。在渲染過程當中,會遍歷effectList,根據 flags(effectTag)的不一樣,執行不一樣的DOM操做。

  • layout,這個階段處理DOM渲染完畢以後的收尾邏輯。好比調用 componentDidMount/componentDidUpdate,調用useLayoutEffect鉤子函數的回調等。除了這些以外,它還會把fiberRootcurrent指針指向workInProgress Fiber樹。

感謝

若是本文對你有所幫助,請幫忙點個贊,感謝!

相關文章
相關標籤/搜索