從React源碼分析渲染更新流程

前言

轉前端一年半了,平時接觸最多的框架就是React。在熟悉了其用法以後,避免不了想深刻了解其實現原理,網上相關源碼分析的文章挺多的,可是總感受不如本身閱讀理解來得深入。因而話了幾個週末去了解了一下經常使用的流程。也是經過這篇文章將本身的我的理解分享出來。html

在具體的源碼流程分析以前,根據我的理解,結合網上比較好的文章,先來分析一些概念性的東西。後續再分析具體的流程邏輯。前端

基礎概念

React 15

架構分層

React 15版本(Fiber之前)整個更新渲染流程分爲兩個部分:node

  • Reconciler(協調器); 負責找出變化的組件
  • Renderer(渲染器); 負責將變化的組件渲染到頁面上

Reconciler

React中能夠經過setStateforceUpdateReactDOM.render來觸發更新。每當有更新發生時,Reconciler會作以下工做:react

  1. 調用組件的render方法,將返回的JSX轉化爲虛擬DOM
  2. 將虛擬DOM和上次更新時的虛擬DOM對比
  3. 經過對比找出本次更新中變化的虛擬DOM
  4. 通知Renderer將變化的虛擬DOM渲染到頁面上

Renderer

在對某個更新節點執行玩Reconciler以後,會通知Renderer根據不一樣的"宿主環境"進行相應的節點渲染/更新。git

React 15的缺陷

React 15diff過程是 遞歸執行更新 的。因爲是遞歸,一旦開始就"沒法中斷" 。當層級太深或者diff邏輯(鉤子函數裏的邏輯)太複雜,致使遞歸更新的時間過長,Js線程一直卡主,那麼用戶交互和渲染就會產生卡頓。看個例子: count-demogithub

<button>        click     <button>
<li>1<li>        ->       <li>2<li>
<li>2<li>        ->       <li>4<li>
<li>3<li>        ->       <li>6<li>
複製代碼

當點擊button後,列表從左邊的一、二、3變爲右邊的二、四、6。每一個節點的更新過程對用戶來講基本是同步,但實際上他們是順序遍歷的。具體步驟以下:算法

  1. 點擊button,觸發更新
  2. Reconciler檢測到<li1>須要變動爲<li2>,則馬上通知Renderer更新DOM。列表變成二、二、3
  3. Reconciler檢測到<li2>須要變動爲<li4>,通知Renderer更新DOM。列表變成二、四、3
  4. Reconciler檢測到<li3>須要變動爲<li6>,則馬上通知Renderer更新DOM。列表變成二、四、6

今後可見 ReconcilerRenderer是交替工做 的,當第一個節點在頁面上已經變化後,第二個節點再進入Reconciler。因爲整個過程都是同步的,因此在用戶看來全部節點是同時更新的。若是中斷更新,則會在頁面上看見更新不徹底的新的節點樹!api

假如當進行到第2步的時候,忽然由於其餘任務而中斷當前任務,致使第三、4步沒法進行那麼用戶就會看到:數組

<button>        click     <button>
<li>1<li>        ->       <li>2<li>
<li>2<li>        ->       <li>2<li>
<li>3<li>        ->       <li>3<li>
複製代碼

這種狀況是React絕對不但願出現的。可是這種應用場景又是十分必須的。想象一下,用戶在某個時間點進行了輸入事件,此時應該更新input內的內容,可是由於一個不在當前可視區域的列表的更新致使用戶的輸入更新被滯後,那麼給用戶的體驗就是卡頓的。所以React團隊須要尋找一個辦法,來解決這個缺陷。瀏覽器

React 16

架構分層

React15架構不能支撐異步更新以致於須要重構,因而React16架構改爲分爲三層結構:

  • Scheduler(調度器);調度任務的優先級,高優任務優先進入Reconciler
  • Reconciler(協調器);負責找出變化的組件
  • Renderer(渲染器);負責將變化的組件渲染到頁面上

Scheduler

React 15React 16提出的需求是Diff更新應爲可中斷的,那麼此時又出現了兩個新的兩個問題:中斷方式和判斷標準;

React團隊採用的是 合做式調度,即主動中斷和控制器出讓判斷標準爲超時檢測。同時還須要一種機制來告知中斷的任務在什麼時候恢復/從新執行。 React 借鑑了瀏覽器的requestIdleCallback接口,當瀏覽器有剩餘時間時通知執行

因爲一些緣由React放棄使用rIdc,而是本身實現了功能更完備的polyfill,即Scheduler。除了在空閒時觸發回調的功能外,Scheduler還提供了多種調度優先級供任務設置。

Reconciler

React 15Reconciler是遞歸處理Virtual DOM的。而React16使用了一種新的數據結構:FiberVirtual DOM樹由以前的從上往下的樹形結構,變化爲基於多向鏈表的"圖"。

更新流程從遞歸變成了能夠中斷的循環過程。每次循環都會調用shouldYield()判斷當前是否有剩餘時間。源碼地址

function workLoopConcurrent() {
    // Perform work until Scheduler asks us to yield
    while (workInProgress !== null && !shouldYield()) {
        workInProgress = performUnitOfWork(workInProgress);
    }
}
複製代碼

前面有分析到React 15中斷執行會致使頁面更新不徹底,緣由是由於ReconcilerRenderer是交替工做的,所以在React 16中,ReconcilerRenderer再也不是交替工做。當Scheduler將任務交給Reconciler後,Reconciler只是會爲變化的Virtual DOM打上表明增/刪/更新的標記,而不會發生通知Renderer去渲染。相似這樣:

export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
複製代碼

只有當全部組件都完成Reconciler的工做,纔會統一交給Renderer進行渲染更新。

Renderer(Commit)

Renderer根據ReconcilerVirtual DOM打的標記,同步執行對應的渲染操做。

對於咱們在上一節使用過的例子,在React 16架構中整個更新流程爲:

  1. setState產生一個更新,更新內容爲:state.count1變爲2
  2. 更新被交給SchedulerScheduler發現沒有其餘更高優先任務,就將該任務交給Reconciler
  3. Reconciler接到任務,開始遍歷Virtual DOM,判斷哪些Virtual DOM須要更新,爲須要更新的Virtual DOM打上標記
  4. Reconciler遍歷完全部Virtual DOM,通知Renderer
  5. Renderer根據Virtual DOM的標記執行對應節點操做

其中步驟二、三、4隨時可能因爲以下緣由被中斷:

  • 有其餘更高優先任務須要先更新
  • 當前幀沒有剩餘時間

因爲SchedulerReconciler的工做都在內存中進行,不會更新頁面上的節點,因此用戶不會看見更新不徹底的頁面。

Diff原則

React的Diff是有必定的 前提假設 的,主要分爲三點:

  • DOM跨層級移動的狀況少,對 Virtual DOM 樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。
  • 不一樣類型的組件,樹形結構不同。相同類型的組件樹形結構類似
  • 同一層級的一組子節點操做無外乎 更新、移除、新增 ,能夠經過 惟一ID 區分節點

不管是JSX格式仍是React.createElement建立的React組件最終都會轉化爲Virtual DOM,最終會根據層級生成相應的Virtual DOM樹形結構。React 15 每次更新會成新的Virtual DOM,而後通 遞歸 的方式對比新舊Virtual DOM的差別,獲得對比後的"更新補丁",最後映射到真實的DOM上。React 16 的具體流程後續會分析到

源碼分析

React源碼很是多,並且16之後的源碼一直在調整,目前Github上最新源碼都是保留xxx.new.jsxxx.old.js兩份代碼。react源碼 是採用Monorepo結構來進行管理的,不一樣的功能分在不一樣的package裏,惟一的壞處可能就是方法地址索引發來不是很方便,若是不是對源碼比較熟悉的話,某個功能點可能須要經過關鍵字全局查詢而後去一個個排查。開始以前,能夠先閱讀下官方的這份閱讀指南

由於源碼實在是太多太複雜了,全部我這裏儘量的最大到小,從面到點的一個個分析。大體的流程以下:

  1. 首先得知道經過JSX或者createElement編碼的代碼到底會轉成啥
  2. 而後分析應用的入口ReactDOM.render
  3. 接着進一步分析setState更新的流程
  4. 最後再具體分析SchedulerReconcilerRenderer的大體流程

觸發渲染更新的操做除了ReactDOM.rendersetState外,還有forceUpdate。可是實際上是差很少的,最大差別在於forceUpdate不會走shouldComponentUpdate鉤子函數。

JSX與React.createElement

先來看一個最簡單的JSX格式編碼的組件,這裏藉助babel進行代碼轉換,代碼看這

// JSX
class App extends React.Component {
    render() {
        return <div />
    }
}

// babel
var App = /*#__PURE__*/function (_React$Component) {
    _inherits(App, _React$Component);

    var _super = _createSuper(App);

    function App() {
        _classCallCheck(this, App);

        return _super.apply(this, arguments);
    }

    _createClass(App, [{
        key: "render",
        value: function render() {
            return /*#__PURE__*/React.createElement("div", null);
        }
    }]);

    return App;
}(React.Component);
複製代碼

關鍵點在於render方法其實是調用了React.createElement方法。那麼接下來咱們只須要分析createElement作了啥便可。咱們先看看ReactElement的結構:

let REACT_ELEMENT_TYPE = 0xeac7;
if (typeof Symbol === 'function' && Symbol.for) {
    REACT_ELEMENT_TYPE = Symbol.for('react.element');
}

const ReactElement = function (type, key, ref, props) {
    const element = {
        // 惟一地標識爲React Element,防止XSS,JSON裏不能存Symbol
        $$typeof: REACT_ELEMENT_TYPE,

        type: type,
        key: key,
        ref: ref,
        props: props,
    }
    return element;
}
複製代碼

很簡單的一個數據結構,每一個屬性的做用都一目瞭然,就不一一解釋了。而後分析React.createElement源碼。

這裏多提一句,源碼註釋裏爲何說$$typeof可以有效防護JSX呢?通常來講,咱們編碼的DOM都會轉爲ReactElement的對象,可是React提供了dangerouslySetInnerHTML來做爲innerHTML的替代方案,當攻擊者在服務端的資源中插入一段dangerouslySetInnerHTMLJSON的時候,會由於沒有$$typeof而被React判斷爲無效。由於Symbol沒法JSON化呀;具體檢測的源碼看這裏

const hasOwnProperty = Object.prototype.hasOwnProperty;
const RESERVED_PROPS = {
    key: true,
    ref: true,
    __self: true,
    __source: true,
};

function createElement(type, config, children) {
    let propName;

    // Reserved names are extracted
    const props = {};

    let key = null;
    let ref = null;

    if (config !== null) {
        if (hasValidRef(config)) {
            ref = config.ref;
        }
        if (hasValidKey(config)) {
            key = '' + config.key;
        }
    }

    // 過濾React保留的關鍵字
    for (propName in config) {
        if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
            props[propName] = config[propName];
        }
    }

    // 遍歷children
    const childrenLength = arguments.length - 2;
    if (childrenLength === 1) {
        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 = childArray;
    }

    // 設置默認props
    if (type && type.defaultProps) {
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
            if (props[propName] === undefined) {
                props[propName] = defaultProps[propName];
            }
        }
    }

    return ReactElement(type, key, ref, props);
}
複製代碼

註釋應該已經夠清楚了哈。總結下來就是根據參數來生成一個ReactElement對象,並綁定對應的propskeyref等;

數據結構

Fiber

開始正式流程分析以前,但願你對Fiber有過必定的瞭解。若是沒有,建議你先看看這則視頻。而後,先來熟悉下ReactFiber的大概結構。

export type Fiber = {
    // 任務類型信息;
    // 好比ClassComponent、FunctionComponent、ContextProvider
    tag: WorkTag,
    key: null | string,
    // reactElement.type的值,用於reconciliation期間的保留標識。
    elementType: any,
    // fiber關聯的function/class
    type: any,
    // any類型!! 通常是指Fiber所對應的真實DOM節點或對應組件的實例
    stateNode: any,
    // 父節點/父組件
    return: Fiber | null,
    // 第一個子節點
    child: Fiber | null,
    // 下一個兄弟節點
    sibling: Fiber | null,
    // 變動狀態,好比刪除,移動
    effectTag: SideEffectTag,
    // 用於連接新樹和舊樹;舊->新,新->舊
    alternate: Fiber | null,
    // 開發模式
    mode: TypeOfMode,
    // ...
  };
複製代碼

FiberRoot

每一次經過ReactDom.render渲染的一棵樹或者一個應用都會初始化一個對應的FiberRoot對象做爲應用的起點。其數據結構以下ReactFiberRoot

type BaseFiberRootProperties = {
  // The type of root (legacy, batched, concurrent, etc.)
  tag: RootTag,
  // root節點,ReactDOM.render()的第二個參數
  containerInfo: any,
  // 持久更新會用到。react-dom是整個應用更新,用不到這個
  pendingChildren: any,
  // 當前應用root節點對應的Fiber對象
  current: Fiber,
  // 當前更新對應的過時時間
  finishedExpirationTime: ExpirationTime,
  // 已經完成任務的FiberRoot對象,在commit(提交)階段只會處理該值對應的任務
  finishedWork: Fiber | null,
  // 樹中存在的最舊的未到期時間
  firstPendingTime: ExpirationTime,
  // 掛起任務中的下一個已知到期時間
  nextKnownPendingLevel: ExpirationTime,
  // 樹中存在的最新的未到期時間
  lastPingedTime: ExpirationTime,
  // 最新的過時時間
  lastExpiredTime: ExpirationTime,
  // ...
};
複製代碼

Fiber 類型

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // 不肯定類型;多是class或function
export const HostRoot = 3; // 樹的根
export const HostPortal = 4; // 一顆子樹
export const HostComponent = 5; // 原生節點;根據環境而定,瀏覽器環境就是div等
export const HostText = 6; // 純文本節點
export const Fragment = 7;
複製代碼

模式

React 16.13.1版本位置,內置的開發模式有以下幾種:

export type TypeOfMode = number;
// 普通模式,同步渲染,React15-16的生產環境用
export const NoMode = 0b0000;
// 嚴格模式,用來檢測是否存在廢棄API(會屢次調用渲染階段生命週期),React16-17開發環境使用
export const StrictMode = 0b0001;
// ConcurrentMode 模式的過渡版本
export const BlockingMode = 0b0010;
// 併發模式,異步渲染,React17的生產環境用
export const ConcurrentMode = 0b0100;
// 性能測試模式,用來檢測哪裏存在性能問題,React16-17開發環境使用
export const ProfileMode = 0b1000;
複製代碼

本文只分析 ConcurrentMode 模式

render流程

ReactDOM.render使用參考這裏

通常來講,使用React編寫應用,ReactDOM.render是咱們觸發的第一個函數。那麼咱們先從ReactDOM.render這個入口函數開始分析render的整個流程。

源碼中會頻繁出現針對hydrate的邏輯判斷和處理。這個是跟SSR結合客戶端渲染相關,不會作過多分析。源碼部分我都會進行省略

ReactDOM.render實際上對ReactDOMLegacy裏的render方法的引用,精簡後的邏輯以下:

export function render( // React.creatElement的產物 element: React$Element<any>, container: Container, callback: ?Function, ) {
    return legacyRenderSubtreeIntoContainer(
        null,
        element,
        container,
        false,
        callback,
    );
}
複製代碼

實際上調用的是legacyRenderSubtreeIntoContainer方法,再來看看這個咯

function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, // 通常爲null children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function, ) {

    let root: RootType = (container._reactRootContainer: any);
    let fiberRoot;
    if (!root) {
        // [Q]: 初始化容器。清空容器內的節點,並建立FiberRoot
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
            container,
            forceHydrate,
        );
        // FiberRoot; 應用的起點
        fiberRoot = root._internalRoot;
        if (typeof callback === 'function') {
            const originalCallback = callback;
            callback = function () {
                const instance = getPublicRootInstance(fiberRoot);
                originalCallback.call(instance);
            };
        }
        // [Q]: 初始化不能批量處理,即同步更新
        unbatchedUpdates(() => {
            updateContainer(children, fiberRoot, parentComponent, callback);
        });
    } else {
        // 省略... 更上面相似,差異是無需初始化容器和可批處理
        // [Q]:咦? unbatchedUpdates 有啥奧祕呢
        updateContainer(children, fiberRoot, parentComponent, callback);
    }
    return getPublicRootInstance(fiberRoot);
}
複製代碼

根據官網的使用文檔可知,在這一步會先清空容器裏現有的節點,若是有異步回調callback會先保存起來,並綁定對應FiberRoot引用關係,以用於後續傳遞正確的根節點。註釋裏我標註了兩個[Q]表明兩個問題。咱們先來仔細分析這兩個問題

Q: 初始化

從命名上看,legacyCreateRootFromDOMContainer是用來初始化根節點的。 將legacyCreateRootFromDOMContainer的返回結果賦值給container._reactRootContainer,而_reactRootContainer從代碼上看是做爲是否已經初始化的依據,也驗證了這一點。不信的話,打開你的React應用,查看下容器元素的_reactRootContainer屬性

function legacyCreateRootFromDOMContainer( container: Container, forceHydrate: boolean, ): RootType {
  // 省略 hydrate ...
  return createLegacyRoot(container, undefined);
}

export function createLegacyRoot( container: Container, options?: RootOptions, ): RootType {
  return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

function ReactDOMBlockingRoot( container: Container, tag: RootTag, options: void | RootOptions, ) {
  // !!! look here
  this._internalRoot = createRootImpl(container, tag, options);
}
複製代碼

一連串的函數調用,其實就是還回了一個ReactDOMBlockingRoot實例。其中重點在於屬性_internalRoot是經過createRootImpl建立的產物。

function createRootImpl( container: Container, tag: RootTag, options: void | RootOptions, ) {
  // 省略 hydrate ...
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);
  // 省略 hydrate ...
  return root;
}

export function createContainer( containerInfo: Container, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): OpaqueRoot {
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}

export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot {
  // 生成 FiberRoot
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  // 爲Root生成Fiber對象
  const uninitializedFiber = createHostRootFiber(tag);
  // 綁定 FiberRoot 與 Fiber 
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  // 生成更新隊列
  initializeUpdateQueue(uninitializedFiber);

  return root;
}

export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    baseQueue: null,
    shared: {
      pending: null,
    },
    effects: null,
  };
  fiber.updateQueue = queue;
}
複製代碼

大體邏輯就是生成了一個FiberRoot對象root。並生成了root對應的Fiber對象,同時生成了該fiber的更新隊列。從這裏清楚的知道了FiberRoot是在什麼時候初始化的,咱們得先記住這個FiberRoot,能夠認爲他是整個React應用的起點。

Q: unbatchedUpdates

源碼中的英文註釋說明這裏是無需批處理,應該當即執行。其傳入參數是一個執行updateContainer的包裝函數。 可是在else判斷中實際上也執行了updateContainer。那麼unbatchedUpdates有啥奧祕呢?

export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  try {
    return fn(a);
  } finally {
    // !!! look here
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      flushSyncCallbackQueue();
    }
  }
}

export function flushSyncCallbackQueue() {
  // 省略...
  flushSyncCallbackQueueImpl();
}

// 清空同步任務隊列
function flushSyncCallbackQueueImpl() {
  if (!isFlushingSyncQueue && syncQueue !== null) {
    isFlushingSyncQueue = true;
    let i = 0;
    try {
      const isSync = true;
      const queue = syncQueue;
      // 以最高優先級來清空隊列裏的任務
      runWithPriority(ImmediatePriority, () => {
        for (; i < queue.length; i++) {
          let callback = queue[i];
          do {
            callback = callback(isSync);
          } while (callback !== null);
        }
      });
      syncQueue = null;
    } catch (error) {
      // 移除錯誤的任務
      if (syncQueue !== null) {
        syncQueue = syncQueue.slice(i + 1);
      }
      // 在下一個執行單元恢復執行
      Scheduler_scheduleCallback(
        Scheduler_ImmediatePriority,
        flushSyncCallbackQueue,
      );
      throw error;
    } finally {
      isFlushingSyncQueue = false;
    }
  }
}
複製代碼

unbatchedUpdates中,其實就是多了一段finally中的邏輯。其中的邏輯主要是刷新同步任務隊列。想想,爲啥呢?那麼說明在fn(a)的執行過程當中確定產生了同步任務唄!那麼接下來繼續跟進到updateContainer中瞧一瞧。

注意,這裏updateContainer已是屬於Reconciler流程了哦。繼續跟進:

export function updateContainer( element: ReactNodeList, // 要渲染的組件 container: OpaqueRoot, // OpaqueRoot就是FiberRoot parentComponent: ?React$Component<any, any>, callback: ?Function, ): ExpirationTimeOpaque {
    // 根節點Fiber
    const current = container.current;
    const eventTime = requestEventTime();

    const suspenseConfig = requestCurrentSuspenseConfig();
    // TODO:計算這次任務的過時時間
    const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );

    const context = getContextForSubtree(parentComponent);
    if (container.context === null) {
        container.context = context;
    } else {
        container.pendingContext = context;
    }

    // 建立一個更新任務
    const update = createUpdate(eventTime, expirationTime, suspenseConfig);
    update.payload = { element };

    callback = callback === undefined ? null : callback;
    if (callback !== null) {
        update.callback = callback;
    }

    // 將任務插入Fiber的更新隊列
    enqueueUpdate(current, update);
    // 調度任務 scheduleWork爲scheduleUpdateOnFiber
    scheduleWork(current, expirationTime);

    return expirationTime;
}
複製代碼

這一步看上去代碼賊多,其實就是先計算出當前更新的過時時間,而後經過createUpdate建立了一個update更新任務,接着經過enqueueUpdate插入 循環任務隊列,最後使用scheduleUpdateOnFiber來調度任務。

從這裏開始,源碼中有同步和異步兩種處理方式,同步任務是不會通過Scheduer進行調度的。爲了分析的完整性,咱們只分析異步過程。後續頻繁提到的expirationTime,能夠暫且認爲其爲任務的"過時時間節點",是具體的"時間點",而不是"時間長度"。可是在不一樣的階段其意義是不同的。能夠肯定的是,組件的更新與否或者說更新的時間節點是由其來決定的。

export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTimeOpaque, ) {
  // 獲取FiberRoot,並更新子Fiber的過時時間(父組件更新觸發子組件更新)
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    return null;
  }
  if (expirationTime === Sync) {
    // 同步任務調度
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }
  // 省略...
}
複製代碼

scheduleUpdateOnFiber只是用於 更新以當前節點爲Root的整個"樹"的過時時間。 其中重點在ensureRootIsScheduled這個方法

// 此函數用於調度任務。 一個root(fiber節點)只能有一個任務在執行 
// 若是已經有任務在調度中,將檢查已有任務的到期時間與下一級別任務的到期時間相同。
// 每次更新和任務退出前都會調用此函數
// 注意:root是FiberRoot 
function ensureRootIsScheduled(root: FiberRoot) {
    // lastExpiredTime表明過時時間
    const lastExpiredTime = root.lastExpiredTime;
    if (lastExpiredTime !== NoWork) {
        // 特殊狀況:過時的工做應同步刷新
        root.callbackExpirationTime = Sync;
        root.callbackPriority = ImmediatePriority;
        root.callbackNode = scheduleSyncCallback(
            performSyncWorkOnRoot.bind(null, root),
        );
        return;
    }
    // TODO:從暫停或等待的任務中取出優先級最高的任務的過時時間
    // 就是從任務隊列中取出下次將執行的調度任務的過時時間?
    const expirationTime = getNextRootExpirationTimeToWorkOn(root);
    // root有正在處理的調度任務
    const existingCallbackNode = root.callbackNode;
    if (expirationTime === NoWork) {
        if (existingCallbackNode !== null) {
            root.callbackNode = null;
            root.callbackExpirationTime = NoWork;
            root.callbackPriority = NoPriority;
        }
        return;
    }

    // 計算當前任務的過時時間; 同一事件中發生的全部優先級相同的更新都收到相同的到期時間
    const currentTime = requestCurrentTimeForUpdate();
    // 根據下一次調度任務的過時時間與當前任務的過時時間計算出當前任務的優先級
    // 即currentTime小於expirationTime,那麼其優先級更高
    const priorityLevel = inferPriorityFromExpirationTime(
        currentTime,
        expirationTime,
    );

    // 若是當前正在處理的任務優先級基於這次任務,取消正在處理的任務!
    if (existingCallbackNode !== null) {
        const existingCallbackPriority = root.callbackPriority;
        const existingCallbackExpirationTime = root.callbackExpirationTime;
        if (
            // 任務必須具備徹底相同的到期時間。
            existingCallbackExpirationTime === expirationTime &&
            // 比較兩次任務的優先級
            existingCallbackPriority >= priorityLevel
        ) {
            return;
        }
        // 取消調度任務
        cancelCallback(existingCallbackNode);
    }

    // 更新到期時間與優先級
    root.callbackExpirationTime = expirationTime;
    root.callbackPriority = priorityLevel;

    let callbackNode;
    if (expirationTime === Sync) {
        // 省略...
        // 這裏會將任務推入同步任務隊列,前面分析到 flushSyncCallbackQueueImpl 清空的任務就是從這裏推入
    } else {
        // 將任務推入Scheduler調度隊列
        callbackNode = scheduleCallback(
            priorityLevel,
            // 綁定
            performConcurrentWorkOnRoot.bind(null, root),
            // 計算超時時間
            { timeout: expirationTimeToMs(expirationTime) - now() },
        );
    }

    // 更新Fiber的當前回調節點
    root.callbackNode = callbackNode;
}
複製代碼

ensureRootIsScheduled中的主要邏輯分三步:

  1. 計算這次任務的過時時間和優先級。
  2. 若是當前節點已有任務在調度中。若是到期時間相同,且已有任務的的優先級更高,則取消這次調度。不然取消已有任務。
  3. 將任務推入Scheduler中的調度隊列,並設置其優先級與任務過時時間

這段代碼每一段都是能夠去延伸開分析的。可是我這裏主要是分析大體流程,因此主要分析scheduleCallback相關的邏輯。其餘部分,之後有時間在進一步分析。

scheduleCallback是將任務的執行函數交由Scheduler來處理。那麼後續的流程須要等待Scheduler來觸發具體的執行函數performConcurrentWorkOnRoot。關於render的流程就先暫時分析到這裏爲止。

render流程總結

  1. render會調用legacyRenderSubtreeIntoContainer方法
  2. legacyRenderSubtreeIntoContainer中,若是是第一次渲染,會先初始化FiberRoot,其爲應用的起點。同時生成根節點的Fiber實例。這裏 FiberRoot.current = Fiber; Fiber.stateNode = FiberRoot
  3. 調用updateContainer會計算出這次更新的過時時間。並生成任務對象update,將其插入Fiber中的更新隊列,而後調用scheduleUpdateOnFiber觸發任務調度
  4. scheduleUpdateOnFiber會更新以該Fiber節點爲根節點的整棵Fiber樹的過時時間。而後調用ensureRootIsScheduled進行調度
  5. ensureRootIsScheduled中會綁定任務與具體執行函數。而後交由Scheduler處理

setState流程

在繼續分析後續的ReconcilerRenderer細節以前,咋們趁熱打鐵來熟悉下setState的流程。既然調用的時候是經過this.setState來調動的,那麼就從Component裏面去找咯。來look一下ReactBaseClasses

const emptyObject = {};
function Component(props, context, updater) {
    this.props = props;
    this.context = context;
    this.refs = emptyObject;
    // ReactNoopUpdateQueue 是一個沒啥意義的空對象
    this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.setState = function (partialState, callback) {
    this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼

Component的初始結構很簡單。咱們看到其setState方法就是調用了this.updater.enqueueSetState方法,可是update默認是空的無用對象,咱們通常也沒有在構造方法裏傳入一個update參數,那麼說明這個方法確定是後續注入的咯。與是我找啊找,找到了一個差很少的東西classComponentUpdater

const classComponentUpdater = {
    isMounted,
    enqueueSetState(inst, payload, callback) {
        const fiber = getInstance(inst);
        const currentTime = requestCurrentTimeForUpdate();
        const suspenseConfig = requestCurrentSuspenseConfig();
        const expirationTime = computeExpirationForFiber(
            currentTime,
            fiber,
            suspenseConfig,
        );
        // 生成這次setState的更新對象
        const update = createUpdate(expirationTime, suspenseConfig);
        update.payload = payload;
        if (callback !== undefined && callback !== null) {
            update.callback = callback;
        }
        // 更新任務入隊
        enqueueUpdate(fiber, update);
        scheduleWork(fiber, expirationTime);
    },
    enqueueReplaceState(inst, payload, callback) {
        // 同上相似
    },
    enqueueForceUpdate(inst, callback) {
        // 同上相似
    },
};
複製代碼

嘿嘿,是否是發現了enqueueSetState裏的邏輯有點似曾相識。其實就是咱們以前分析render流程中遇到的updateContainer的流程是同樣的啦。不記得的話回頭再看看咯。那麼接下來咱們只要分析下classComponentUpdater是怎麼注入爲Componentupdate屬性便可了。

前面分析render流程的時候,咱們還只分析到了生成任務分片並推入調度隊列,尚未對組件的初始化有過度析。從Component的構造函數中猜想是否是在初始化Component的時候React幫咱們注入的呢? 順着這個思路進行下一步的分析。首先咱們先來看beginWork方法中的一段代碼,beginWork方法在後面會具體分析。這裏先知道他是用於建立子組件的Fiber對象便可。

function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
    // 嘗試複用 current 節點
    if (current !== null) {
        // 省略...
    }
    // 不能複用則 update 或者 mount
    switch (workInProgress.tag) {
        // 省略...
        case ClassComponent: {
            const Component = workInProgress.type;
            const unresolvedProps = workInProgress.pendingProps;
            const resolvedProps =
                workInProgress.elementType === Component
                    ? unresolvedProps
                    : resolveDefaultProps(Component, unresolvedProps);
            return updateClassComponent(
                current,
                workInProgress,
                Component,
                resolvedProps,
                renderExpirationTime,
            );
        }
        // 省略...
    }
}
複製代碼

beginWork中的代碼分爲兩部分。分別用於處理mountupdate的邏輯。咱們分析的流程是第一次初始化,那麼走的是mount流程。beginWork會根據不一樣的tag調用不一樣的方法,這裏咱們先來看看updateClassComponent

function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {
    // 省略 context 的處理...

    // 組件的實例
    const instance = workInProgress.stateNode;
    let shouldUpdate;
    // instance爲null 說明組件第一次渲染
    if (instance === null) {
        if (current !== null) {
            // 重置current與wip的依賴(備份)
            current.alternate = null;
            workInProgress.alternate = null;
            // 標記爲新增節點
            workInProgress.effectTag |= Placement;
        }
        // 初始化組件實例
        constructClassInstance(workInProgress, Component, nextProps);
        // 掛載; 並調用相應的生命週期
        mountClassInstance(
            workInProgress,
            Component,
            nextProps,
            renderExpirationTime,
        );
        shouldUpdate = true;
    } else {
        // 省略更新邏輯...
    }
    // TODO:執行 render 新建子Fiber。
    const nextUnitOfWork = finishClassComponent(
        current,
        workInProgress,
        Component,
        shouldUpdate,
        hasContext,
        renderExpirationTime,
    );
    return nextUnitOfWork;
}
複製代碼
function constructClassInstance( workInProgress: Fiber, ctor: any, props: any, ): any {
    let context = emptyContextObject;
    // 省略 context 相關邏輯...

    const instance = new ctor(props, context);
    const state = (workInProgress.memoizedState =
        instance.state !== null && instance.state !== undefined
            ? instance.state
            : null);
    adoptClassInstance(workInProgress, instance);

    // 省略 context 相關邏輯...
    return instance;
}
複製代碼
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
    instance.updater = classComponentUpdater;
    workInProgress.stateNode = instance;
    // 綁定實例與Fiber,方便後續更新使用
    setInstance(instance, workInProgress);
}
複製代碼

能夠看到當instancenull的時候,會執行如下幾個流程

  1. 並標記當前的effectTagPlacement,表明爲新增節點
  2. 初始化生成實例,而後綁定到Fiber(workInProgress)上,並綁定update屬性
  3. 最後會調用mountClassInstance來掛載節點,並調用相關的生命週期。

至此,後續的更新流程就跟render流程一致的了,就不作重複分析啦~

Scheduler

SchedulerReact團隊針對任務調度單獨實現的一個rIdcpolyfillReact團隊其意圖不只僅侷限於React這一個應用場景,更想服務與更多的業務,成爲更普遍應用的一個工具。

最小優先隊列

既然任務具備不一樣的過時時間和優先級,那麼就須要一個數據結構來管理優先級任務。ReactexpirationTime越小的任務應該更優先處理,那麼這個數據結構顯然就是一個最小優先隊列啦。React是基於小頂堆來實現的最小優先隊列。仍是直接看代碼吧。SchedulerMinHeap

type Heap = Array<Node>;
type Node = {|
  id: number,
    sortIndex: number,
|};

// 插入到堆末尾
export function push(heap: Heap, node: Node): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

// 獲取堆頂任務,sortIndex/id 最小的任務
export function peek(heap: Heap): Node | null {
  const first = heap[0];
  return first === undefined ? null : first;
}

// 刪除堆頂任務
export function pop(heap: Heap): Node | null {
  const first = heap[0];
  if (first !== undefined) {
    const last = heap.pop();
    if (last !== first) {
      heap[0] = last;
      siftDown(heap, last, 0);
    }
    return first;
  } else {
    return null;
  }
}

// 向上維持小頂堆
function siftUp(heap, node, i) {
  let index = i;
  while (true) {
    // 位運算;對應根據節點求其父節點-> i / 2 - 1
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    if (parent !== undefined && compare(parent, node) > 0) {
      // parent 更大,交換位置
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      return;
    }
  }
}

// 向下維持小頂堆
function siftDown(heap, node, i) {
  let index = i;
  const length = heap.length;
  while (index < length) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    // // 若是左子節點或右子節點小於目標節點(父節點),則交換
    if (left !== undefined && compare(left, node) < 0) {
      if (right !== undefined && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (right !== undefined && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      return;
    }
  }
}

function compare(a, b) {
  // Compare sort index first, then task id.
  // 先比較sort index,再比較 task id
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

複製代碼

具體實現就是用數組模擬了一個最小堆的結構。能夠看到,每次任務的插入或者移除都會從新回覆最小堆結構,排序規則以sortIndextaskId爲輔。在React中sortIndex對應的其實就是過時時間,taskId則爲遞增任務序列。這一點後續會分析到。

開啓任務調度

前面有分析到在ensureRootIsScheduled中會生成一個任務節點,而後經過scheduleCallback將任務推入Scheduler中。那麼咱們先從這個任務進隊的方法來逐步分析

var taskIdCounter = 1;

// 目前Scheduler對外的api都是unstate_級別的,表示不是穩定版本
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 實際是調用performance.now() 或者 Date.now() 前者更精確
  var currentTime = getCurrentTime();

  var startTime;
  var timeout;
  // 根據是否有延遲來肯定開始時間
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
    // [Q1]:有超時配置直接用。不然根據優先級計算
    timeout =
      typeof options.timeout === 'number'
        ? options.timeout
        : timeoutForPriorityLevel(priorityLevel);
  } else {
    timeout = timeoutForPriorityLevel(priorityLevel);
    startTime = currentTime;
  }

  // 過時時間等於開始時間+超時時間
  var expirationTime = startTime + timeout;

  // 一個task的數據結構就是這樣啦。
  var newTask = {
    // 相同超時時間的任務會對比id,那就是先到先得咯
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  // [Q2]:下面出現了一個延遲隊列(timerQueue)和一個任務隊列(taskQueue)
  if (startTime > currentTime) {
    // This is a delayed task.
    // 說明這是一個延遲任務;即options.delay存在嘛
    newTask.sortIndex = startTime;
    // 若是開始時間大於當前時間,就將它 push 進這個定時器隊列,說明這個是一個等待隊列
    push(timerQueue, newTask);
    // 若是任務隊列爲空,說明全部任務都被延遲,且newTask是最先的延遲任務。
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      // 若是正在進行超時處理,先取消,後續再從新開始
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 發起一個超時處理
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    // 非延遲任務丟入任務隊列
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // 若是沒在調度中則開啓調度;
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      // [Q]開啓調度
      requestHostCallback(flushWork);
    }
  }
  // [A]:還回這個task的引用
  return newTask;
}
複製代碼

從這段代碼能夠看到一個調度任務的數據結構是怎樣的,以及任務的排序依據sortIndex其實就是任務的過時時間expirationTime,而id則是一個遞增序列。註釋中標註了幾個問題,下面一一具體分析

Q: timeout計算

// 當即執行
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// 用戶行爲阻塞
var USER_BLOCKING_PRIORITY = 250;
// 默認五秒過時時間
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// 永不過時, maxSigned31BitInt爲v8 32爲系統最大有效數值
var IDLE_PRIORITY = maxSigned31BitInt;

function timeoutForPriorityLevel(priorityLevel) {
  switch (priorityLevel) {
    case ImmediatePriority:
      return IMMEDIATE_PRIORITY_TIMEOUT;
    case UserBlockingPriority:
      return USER_BLOCKING_PRIORITY;
    case IdlePriority:
      return IDLE_PRIORITY;
    case LowPriority:
      return LOW_PRIORITY_TIMEOUT;
    case NormalPriority:
    default:
      return NORMAL_PRIORITY_TIMEOUT;
  }
}
複製代碼

能夠看到,這裏將優先級轉換成了常量級的具體時間,優先級越高的timeout時間越低。

Q taskQueue & timerQueue

startTime > currentTime的條件分支中,分別將任務推入了taskQueuetimerQueue。而這兩個隊列其實就是咱們前面分析到的一個最小堆的結構。taskQueue表明當前正在調度的任務,而timerQueue表明延遲任務隊列。在任務調度的過程當中,會不停的將timerQueue中的任務轉移到taskQueue中,這一步後續會分析到。

Q 調度的具體過程

咱們看到當任務插入調度隊列時,若是此時不在調度中,會調用requestHostCallback方法開啓調度,並傳入了一個flushwork做爲入參函數。

requestHostCallback = function(callback) {
  // 這裏將傳入的callback緩存起來了
  scheduledHostCallback = callback;
  // 是否在消息循環中
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    port.postMessage(null);
  }
};
複製代碼

從代碼看彷佛rHC的做用只是緩存了callbackflushwork這個入參函數。併發送了一個空的message。那麼重點就在與這個port是爲什麼物了。其實這裏就是React如何模擬requestIdleCallback的地方了。

MessageChannel 模擬 rIC 實現循環調度

不熟悉MessageChannel的能夠先了解一下。先來看看Scheduler中是如何用的。

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
複製代碼

能夠得知,當使用port.postMessage發生消息的時候,實際處理消息的函數爲performWorkUntilDeadline

let isMessageLoopRunning = false;
let scheduledHostCallback = null;

const performWorkUntilDeadline = () => {
  // scheduledHostCallback 具體是由 scheduledHostCallback 賦值的
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // [Q]:截止時間 = 當前時間 + yieldInterval
    deadline = currentTime + yieldInterval;
    const hasTimeRemaining = true;
    try {
      // 是否還有剩餘任務。scheduledHostCallback 多是 flushwork
      const hasMoreWork = scheduledHostCallback(
        hasTimeRemaining,
        currentTime,
      );
      if (!hasMoreWork) {
        // 沒有更多任務 中止循環,並清楚scheduledHostCallback引用
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      } else {
        // 若是還有任務,則繼續發消息。相似一個遞歸的操做
        port.postMessage(null);
      }
    } catch (error) {
      // 若是一個任務出錯了。直接跳過執行下一個任務,並拋出錯誤
      port.postMessage(null);
      throw error;
    }
  } else {
    // 重置循環狀態
    isMessageLoopRunning = false;
  }
  // [Q]: 目前不知道這是啥
  needsPaint = false;
};
複製代碼

老樣子,這裏有幾個問題須要仔細分析下。

Q: yieldInterval

從名字和使用方法上來看,我覺着應該是表明任務的執行時間

// 默認是5
let yieldInterval = 5;

forceFrameRate = function (fps) {
  // ??? 看不起我144hz
  if (fps < 0 || fps > 125) {
    console['error'](
      'forceFrameRate takes a positive int between 0 and 125, ' +
      'forcing framerates higher than 125 fps is not unsupported',
    );
    return;
  }
  if (fps > 0) {
    yieldInterval = Math.floor(1000 / fps);
  } else {
    yieldInterval = 5;
  }
};
複製代碼

forceFrameRate是一個對外提供的api接口,用於動態配置調度任務的執行週期。

Q: deadline & needsPaint

let deadline = 0;
let maxYieldInterval = 300;
let needsPaint = false;

if (
  enableIsInputPending &&
  navigator !== undefined &&
  navigator.scheduling !== undefined &&
  navigator.scheduling.isInputPending !== undefined
) {
  const scheduling = navigator.scheduling;
  shouldYieldToHost = function () {
    const currentTime = getCurrentTime();
    if (currentTime >= deadline) {
      // 沒有時間了。可能但願讓主線程讓出控制權,以便瀏覽器能夠執行高優先級任務,主要是繪製和用戶輸入
      // 所以若是有繪製或者用戶輸入行爲,則應該讓出,放回true
      // 若是二者都不存在,那麼能夠在保持響應能力的同時下降產量
      // 可是存在非`requestPaint`發起的繪製狀態更新或其餘主線程任務(如網絡事件)
      // 所以最終在某個臨界點仍是得讓出控制權
      if (needsPaint || scheduling.isInputPending()) {
        // 有待處理的繪製或用戶輸入
        return true;
      }
      // 沒有待處理的繪製或輸入。但在達到最大產量間隔時也須要釋放控制權
      return currentTime >= maxYieldInterval;
    } else {
      return false;
    }
  };

  requestPaint = function () {
    needsPaint = true;
  };
} else {
  shouldYieldToHost = function () {
    return getCurrentTime() >= deadline;
  };

  requestPaint = function () { };
}
複製代碼

首先須要明確的是shouldYieldToHostrequestPaintScheduler對外提供的接口函數。具體的使用後續會分析到位。

從代碼可知,deadline的用途是用於在shouldYieldToHost檢測調度是否超時。默認清空下是直接對比當前時間currentTimedeadline的值。可是,在支持navigator.scheduling的環境下,React會有更多的考慮,也就是瀏覽器繪製與用戶輸入要有限響應,不然能夠適當的延長調度時間

到這裏先總結下調度啓動的過程,省得腦子糊了。

  1. requestHostCallback準備好要執行的任務scheduledHostCallback
  2. requestHostCallback開啓任務調度循環
  3. MessageChannel接收消息,並調用performWorkUntilDeadline執行任務
  4. performWorkUntilDeadline中先計算這次調度的deadline。而後執行任務
  5. 在執行完一個任務後,會根據返回值來判斷是否有下一個任務。若是有則經過消息循環來達到遞歸執行performWorkUntilDeadline。不然結束消息循環

前面還只是分析了任務調度循環執行的邏輯。具體執行的任務是scheduledHostCallback的引用函數flushWork

任務執行

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if (currentTask !== null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        throw error;
      }
    } else {
      // No catch in prod codepath.
      // 官方註釋說,生成環境不會去catch workLoop拋出的錯誤
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}
複製代碼

flushWork的工做比較簡單。只是重置了一些標誌符,最終返回了workLoop的執行結果。那麼重點確定在這個函數了。

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // [Q]: 這是做甚?
  advanceTimers(currentTime);
  // 取出頂端任務。即最優先的任務
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    // debug 用的,無論
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      // 任務未過時,而且當前調度的deadline到了,將任務放到下次調度週期進行; shouldYieldToHost 
      currentTask.expirationTime > currentTime &&
      // 這兩個前面分析過了; hasTimeRemaining一直爲true,那還判斷有啥意義???
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }
    const callback = currentTask.callback;
    if (callback !== null) {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      // 計算當前任務是否已經超時
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      markTaskRun(currentTask, currentTime);
      // [Q]: 執行callback,好比前面render流程分析到的 performConcurrentWorkOnRoot
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // continuationCallback 成立,則取代當前任務的callback
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        // continuationCallback 不成立,從任務隊列彈出
        // 防止任務被其餘地方取出,得判斷一下
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      // em.... 又是它
      advanceTimers(currentTime);
    } else {
      // 任務被取消了,彈出任務
      // 回顧下ensureRootIsScheduled 中調用 cancelCallback 的狀況
      pop(taskQueue);
    }
    // 再次從頂端取任務
    // 注意:若是 continuationCallback 成立的話,是沒有pop當前任務的。這次取到的仍是當前任務
    currentTask = peek(taskQueue);
  }
  // performWorkUntilDeadline 中判斷 hasMoreWork 的邏輯就是這裏啦!
  if (currentTask !== null) {
    return true;
  } else {
    // [Q]:檢測延遲隊列中的任務是否是過時
    let firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}
複製代碼

大體流程註釋已經很詳細了。老規矩,分析標註的幾個問題。

Q: advanceTimers

function advanceTimers(currentTime) {
  // 遍歷 timerQueue 中的任務;將超時的任務轉移到 taskQueue 中去
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // 任務被取消
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // 超時任務轉移
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // 未過期的繼續掛起
      return;
    }
    timer = peek(timerQueue);
  }
}
複製代碼

wookLoop函數入口第一次調用advanceTimers是將任務從新梳理一下,刷新任務隊列。而以後每次在while調用是 由於任務的執行是須要消耗必定的時間的,全部在執行完後須要從新刷新任務隊列

Q: continuationCallback

首先continuationCallback的產生是有callback決定的。callback的返回值多是一個函數,這表明着當前任務應該被從新處理一次。這裏先留個問題,後續在分析callback的具體實現的時候,咱們再進一步分析

Q: requestHostTimeout & handleTimeout

wookLoop的結尾,當currentTask === null的時候,會去檢測延遲隊列中的任務是否已通過期。

requestHostTimeout = function (callback, ms) {
  taskTimeoutID = setTimeout(() => {
    callback(getCurrentTime());
  }, ms);
};

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  // 從新梳理任務隊列
  advanceTimers(currentTime);

  // isHostCallbackScheduled 爲true。說明有新任務進來了
  if (!isHostCallbackScheduled) {
    // 若是上面的 advanceTimers 梳理了過時的延遲任務到任務隊列中,則執行
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      // 不然遞歸調用該方法
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}
複製代碼

能夠看出,其實就是在任務隊列中的任務執行完成後。經過遞歸的方法從延遲隊列中查詢是否有過時任務,有的話則轉移到任務隊列中,並執行。

到這裏,Scheduler從任務入列,到循環調度,到任務執行的完整過程就已經分析完成了。作個簡單的流程總結:

  1. unstable_scheduleCallback建立任務,若是任務是延遲的則推入延遲隊列timerQueue,不然推入任務隊列taskQueue
  2. 若是建立的任務是延遲任務,則調用requestHostTimeout方法使用setTimeout遞歸檢測任務是否過時。不然直接發起任務調度requestHostCallback
  3. requestHostCallback經過MessageChannelport2發送消息給port1,具體的處理函數爲performWorkUntilDeadline
  4. performWorkUntilDeadline會計算這次調度的deadline,同時使用 消息循環 來遞歸執行任務
  5. 任務具體處理是由wookLoop執行。其將任務從任務隊列taskQueue堆頂依次取出執行。若是任務隊列清空,則調用requestHostTimeout開啓遞歸檢測。

Reconciler

分析完Scheduler的邏輯後,接下來接着分析Reconciler的邏輯。咱們老生常談的Diff更新的邏輯大部分就是發生在Reconciler階段,其中包含了大量的組件更新計算與優化。

上面分析了Scheduler的調度過程。而具體在Scheduler中的執行的callbackperformConcurrentWorkOnRoot。咱們來看一看

// 被Scheduler調用的入口函數
function performConcurrentWorkOnRoot(root, didTimeout) {
    // 重置
    currentEventTime = NoWork;

    if (didTimeout) {
        // 任務已經超時
        const currentTime = requestCurrentTimeForUpdate();
        // 將過時時間標記爲當前,以在單個批處理中同步處理已過時的工做。
        markRootExpiredAtTime(root, currentTime);
        // 調度一個同步任務
        ensureRootIsScheduled(root);
        return null;
    }

    // 獲取下一個到期(更新)時間. 將以此做爲本次渲染的執行必要性判斷
    const expirationTime = getNextRootExpirationTimeToWorkOn(root);
    if (expirationTime !== NoWork) {
        const originalCallbackNode = root.callbackNode;

        // TODO:刷新被動的Hooks
        flushPassiveEffects();

        // 若是根或到期時間已更改,則丟棄現有堆棧並準備新的堆棧。 不然,咱們將從中斷的地方繼續。
        if (
            root !== workInProgressRoot ||
            expirationTime !== renderExpirationTime
        ) {
            // [Q]: 重置數據;
            // 設置 renderExpirationTime 爲expirationTime
            // 複製 root.current 爲 workInProgress等
            prepareFreshStack(root, expirationTime);
            startWorkOnPendingInteractions(root, expirationTime);
        }

        if (workInProgress !== null) {
            // 省略...
            do {
                try {
                    workLoopConcurrent();
                    break;
                } catch (thrownValue) {
                    handleError(root, thrownValue);
                }
            } while (true);
            // 省略...
        }

        if (workInProgress !== null) {
            // 仍然有任務要作。說明是超時了,退出而不提交。
            stopInterruptedWorkLoopTimer();
        } else {
            stopFinishedWorkLoopTimer();

            const finishedWork: Fiber = ((root.finishedWork =
                root.current.alternate): any);
            root.finishedExpirationTime = expirationTime;
            // commit;開始 Renderer 流程
            finishConcurrentRender(
                root,
                finishedWork,
                workInProgressRootExitStatus,
                expirationTime,
            );
        }
    }
    return null;
}
複製代碼

首先會判斷任務是否超時,若是超時則以同步的方式執行該任務,防止任務被中斷。若是沒有超時,則先在prepareFreshStack中作一些初始化的工做。而後進入了workLoopConcurrent循環。

prepareFreshStack

// 本次渲染的到期時間
let renderExpirationTime: ExpirationTime = NoWork;

function prepareFreshStack(root, expirationTime) {
    // 省略...
    if (workInProgress !== null) {
        // workInProgress 不爲空說明以前有中斷的任務。放棄
        let interruptedWork = workInProgress.return;
        while (interruptedWork !== null) {
            unwindInterruptedWork(interruptedWork);
            interruptedWork = interruptedWork.return;
        }
    }
    workInProgressRoot = root;
    // 從current 複製 wip; 並重置effectList
    workInProgress = createWorkInProgress(root.current, null);
    // 設置renderExpirationTime爲下一個到期時間
    renderExpirationTime = expirationTime;
    // 省略...
}
複製代碼

若是當前wip不爲空,說明上次有中斷的任務,經過不停向上回溯直到root節點來取消中斷的任務。而後從 同時將前面從FiberRoot中獲取下一個任務的到期時間,賦值給renderExpirationTime做爲本次渲染的到期時間。

workLoopConcurrent

workLoopConcurrent的代碼在本文開頭就貼出來過,這裏從新看下

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    // 第一次入參workInProgress爲FiberRoot的Fiber
    // 後續將上一次返回值(子Fiber)做爲入參
    workInProgress = performUnitOfWork(workInProgress);
  }
}
複製代碼

workLoopConcurrent的工做主要是循環對比currentworkInProgress兩顆Fiber樹。在wip中爲變化的Fiber打上effectTag。同時會從下往上更新/建立DOM節點,構成一顆離屏DOM樹,最後交由Renderer處理。

基於循環的"遞歸"

在熟悉流程以前,先貼出一個刪減版的代碼流程。這裏不按套路出牌,先根據我的理解作個總結。這樣帶着大體的思路結構可能會更好的去理解後續的源碼。

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
    // 舊的 Fiber, 用於對比
    const current = unitOfWork.alternate;

    // 省略...
    // [Q]: 處理當前Fiber節點,還回下一個子節點Fiber
    let next = beginWork(current, unitOfWork, renderExpirationTime);

    unitOfWork.memoizedProps = unitOfWork.pendingProps;
    // 沒有子節點
    if (next === null) {
        next = completeUnitOfWork(unitOfWork);
    }

    ReactCurrentOwner.current = null;
    return next;
}

// 嘗試完成當前的Fiber,而後移至下一個同級。若是沒有更多的同級,返回父fiber。
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
    workInProgress = unitOfWork;
    do {
        // 舊的 Fiber, 用於對比
        const current = workInProgress.alternate;
        const returnFiber = workInProgress.return;

        // Check if the work completed or if something threw.
        if ((workInProgress.effectTag & Incomplete) === NoEffect) {
            // [Q]: 建立/更新當前Fiber對應的節點實例
            let next = completeWork(current, workInProgress, renderExpirationTime);
            stopWorkTimer(workInProgress);
            resetChildExpirationTime(workInProgress);

            if (next !== null) {
                // 產生了新的子節點
                return next;
            }

            // [Q]:後面是在構建 effectList 的單向鏈表
            // 先省略...
        } else {
            // 有異常拋出。根據是不是boundary來決策是捕獲仍是拋出異常
            // 省略...
        }

        const siblingFiber = workInProgress.sibling;
        // 是否存在兄弟節點
        if (siblingFiber !== null) {
            return siblingFiber;
        }
        workInProgress = returnFiber;
    } while (workInProgress !== null);

    if (workInProgressRootExitStatus === RootIncomplete) {
        workInProgressRootExitStatus = RootCompleted;
    }
    return null;
}
複製代碼

首先執行beginWork進行節點操做,以及建立子節點,子節點會返回成爲next,若是有next就返回。返回到workLoopConcurrent以後,workLoopConcurrent會判斷是否過時之類的,若是沒過時則再次調用該方法。

若是next不存在,說明當前節點向下遍歷子節點已經到底了,說明這個子樹側枝已經遍歷完,能夠完成這部分工做了。執行completeUnitOfWork,主要分一下幾個步驟

  1. completeUnitOfWork首先調用completeWork建立/更新當前Fiber對應的節點實例(如原生DOM節點)instance,同時將已經更新的子Fiber的實例插入到instance構成一顆離屏渲染樹。
  2. 存在當前Fiber節點存在effectTag則將其追加到effectList
  3. 查找是否有sibling兄弟節點,有則返回該兄弟節點,由於這個節點可能也會存在子節點,須要經過beginWork進行操做。
  4. 若是不存在兄弟節點。一直往上返回直到root節點或者在某一個節點發現有sibling兄弟節點。
  5. 若是到了root,那麼其返回也是null,表明整棵樹的遍歷已經結束了,能夠commit了。若是中間遇到兄弟節點則同於第3

文字表達可能不是很清楚,直接看一個例子:

workLoopConcurrent.png

workLoopConcurrent

執行順序爲:

文本節點「你好」 不會執行beginWork/completeWork,由於React針對只有單一文本子節點的Fiber,會特殊處理

1. App beginWork
2. div Fiber beginWork
3. span Fiber beginWork
4. span Fiber completeWork
5. div Fiber completeWork
6. p Fiber beginWork
7. p Fiber completeWork
8. App Fiber completeWork
複製代碼

beginWork

beginWork在前面分析setState的時候已經分析過其中mount階段對應的邏輯了。那麼這裏就只分析update的邏輯了。先來看下beginWork的大體工做。

/** * @param {*} current 舊的Fiber * @param {*} workInProgress 新的Fiber * @param {*} renderExpirationTime 下一次到期時間,即本次渲染有效時間 * @returns 子組件 Fiber */
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
    const updateExpirationTime = workInProgress.expirationTime;

    // 嘗試複用 current 節點
    if (current !== null) {
        // 省略...
        // 複用 current
        return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderExpirationTime,
        );
    }

    workInProgress.expirationTime = NoWork;

    // 不能複用則 update 或者 mount
    switch (workInProgress.tag) {
        // 省略...
        case ClassComponent: {
            const Component = workInProgress.type;
            const unresolvedProps = workInProgress.pendingProps;
            const resolvedProps =
                workInProgress.elementType === Component
                    ? unresolvedProps
                    : resolveDefaultProps(Component, unresolvedProps);
            return updateClassComponent(
                current,
                workInProgress,
                Component,
                resolvedProps,
                renderExpirationTime,
            );
        }
        case HostRoot:
            return updateHostRoot(current, workInProgress, renderExpirationTime);
        case HostComponent:
            return updateHostComponent(current, workInProgress, renderExpirationTime);
        case HostText:
            return updateHostText(current, workInProgress);
        // 省略... 
    }
}
複製代碼

咱們接着以前分析過的updateClassComponent來分析update的流程。

function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {

    // 提早處理context邏輯。省略....

    // 組件的實例
    const instance = workInProgress.stateNode;
    let shouldUpdate;
    if (instance === null) {
        // mount. wip.effectTag = Placement
        // 省略...
    } else {
        // update. wip.effectTag = Update | Snapshot
        // 調用 render 以前的生命週期,getDerivedStateFromProps | UNSAFE_componentWillReceiveProps(可能兩次)
        // 接着調用shouldComponentUpdate判斷是否須要更新
        // 最後更新props 和 state
        shouldUpdate = updateClassInstance(
            current,
            workInProgress,
            Component,
            nextProps,
            renderExpirationTime,
        );
    }
    // 執行 render 新建子Fiber。
    const nextUnitOfWork = finishClassComponent(
        current,
        workInProgress,
        Component,
        shouldUpdate,
        hasContext,
        renderExpirationTime,
    );
    return nextUnitOfWork;
}

function finishClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderExpirationTime: ExpirationTime, ) {
    // 引用應該更新,即便shouldComponentUpdate返回false
    markRef(current, workInProgress);

    const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;

    // 無需更新且沒有發送錯誤則直接複用current
    if (!shouldUpdate && !didCaptureError) {
        if (hasContext) {
            invalidateContextProvider(workInProgress, Component, false);
        }
        // 複用current
        return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderExpirationTime,
        );
    }

    const instance = workInProgress.stateNode;

    // Rerender
    ReactCurrentOwner.current = workInProgress;
    let nextChildren = instance.render();

    // PerformedWork 提供給 React DevTools 讀取
    workInProgress.effectTag |= PerformedWork;
    if (current !== null && didCaptureError) {
        // 出錯了。
        // 省略...
    } else {
        reconcileChildren(
            current,
            workInProgress,
            nextChildren,
            renderExpirationTime,
        );
    }

    workInProgress.memoizedState = instance.state;

    if (hasContext) {
        invalidateContextProvider(workInProgress, Component, true);
    }

    return workInProgress.child;
}

export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderExpirationTime: ExpirationTime, ) {
    if (current === null) {
        // mount的組件
        workInProgress.child = mountChildFibers(
            workInProgress,
            null,
            nextChildren,
            renderExpirationTime,
        );
    } else {
        // update的組件
        workInProgress.child = reconcileChildFibers(
            workInProgress,
            current.child,
            nextChildren,
            renderExpirationTime,
        );
    }
}
複製代碼

最後還回的就是workInProgress.child,跟beginWork同樣,根據current === null來區分mountupdate

實際上mountChildFibersreconcileChildFibers均指向同一個函數reconcileChildFibers。差異在於第二個參數currentFirstChild。若是爲null,則會去建立一個新的Fiber對象,不然複用並更新props。好比reconcileSingleElement用於處理只有單個節點的狀況。

completeWork

function completeWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
    const newProps = workInProgress.pendingProps;
    switch (workInProgress.tag) {
        //省略...
        case HostComponent: {
            popHostContext(workInProgress);
            const rootContainerInstance = getRootHostContainer();
            const type = workInProgress.type;
            // fiber節點對應的DOM節點是否存在
            // update
            if (current !== null && workInProgress.stateNode != null) {
                // 爲 wip 計算出新的 updateQueue
                // updateQueue 是一個奇數索引的值爲變化的prop key,偶數索引的值爲變化的prop value 的數組
                updateHostComponent(
                    current,
                    workInProgress,
                    type,
                    newProps,
                    rootContainerInstance,
                );

                if (current.ref !== workInProgress.ref) {
                    markRef(workInProgress);
                }
            } else {
                // mount
                if (!newProps) {
                    return null;
                }

                const currentHostContext = getHostContext();
                // 是否是服務端渲染
                let wasHydrated = popHydrationState(workInProgress);
                if (wasHydrated) {
                    // 省略...
                } else {
                    // 生成真實DOM
                    let instance = createInstance(
                        type,
                        newProps,
                        rootContainerInstance,
                        currentHostContext,
                        workInProgress,
                    );

                    // 將子孫DOM節點插入剛生成的DOM節點中,從下往上,構成一顆離屏DOM樹
                    appendAllChildren(instance, workInProgress, false, false);

                    workInProgress.stateNode = instance;

                    // 與updateHostComponent相似的處理 props
                    if (
                        finalizeInitialChildren(
                            instance,
                            type,
                            newProps,
                            rootContainerInstance,
                            currentHostContext,
                        )
                    ) {
                        markUpdate(workInProgress);
                    }
                }

                if (workInProgress.ref !== null) {
                    markRef(workInProgress);
                }
            }
            return null;
        }
        //省略...
    }

}
複製代碼

首先和beginWork同樣,根據current === null判斷是mount仍是update

update時,主要作了以下幾件事情,具體源碼diffProperties

  • 計算新的STYLE prop
  • 計算新的DANGEROUSLY_SET_INNER_HTML prop
  • 計算新的CHILDREN prop

每次計算出新的prop,都將其propKeynextProp成對的保存在數組updatePayload中。最後將updatePayload賦值給wip.updateQueue

mount時,處理的事情比較多,大體以下:

  • createInstance: 爲Fiber節點生成對應的真實DOM節點
  • appendAllChildren: 將子孫DOM節點插入剛生成的DOM節點中。以此從下往上構成完整的DOM
  • finalizeInitialChildren: 在setInitialProperties中處理事件註冊。在setInitialDOMProperties根據props初始化DOM屬性

值的注意的是appendAllChildren方法。因爲completeWork屬於向上回溯的過程,每次調用appendAllChildren時都會將已生成的子孫DOM節點插入當前生成的DOM節點下。那麼當回溯到根root節點時,整個DOM樹就都已經更新好了。

effectList

在每次completeWork後,表明某個節點已經處理完成。前面說過,Reconciler會爲發生改變的節點打上effectTag,用於在Renderer根據節點的effectTag的執行具體更新。

所以在completeWork的上層函數completeUnitOfWork中(也就是以前省略的代碼),每執行完completeWork會去維護一個effectList的單向鏈表。若是當前Fiber存在effectTag,則插入鏈表。

// 構建 effectList 的單向鏈表
if (
    returnFiber !== null &&
    (returnFiber.effectTag & Incomplete) === NoEffect
) {
    // firstEffect 爲鏈表頭結點
    if (returnFiber.firstEffect === null) {
        returnFiber.firstEffect = workInProgress.firstEffect;
    }
    // lastEffect 爲鏈表尾節點
    if (workInProgress.lastEffect !== null) {
        if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
        }
        returnFiber.lastEffect = workInProgress.lastEffect;
    }

    const effectTag = workInProgress.effectTag;

    // 跳過NoWork和PerformedWork tag。後者是提供給React Tools讀取
    if (effectTag > PerformedWork) {
        if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress;
        } else {
            returnFiber.firstEffect = workInProgress;
        }
        returnFiber.lastEffect = workInProgress;
    }
}
複製代碼

至此,Reconciler流程結束。回頭再看看開頭的總結,是否是清楚一些了呢~

Renderer(Commit)

Commit階段的代碼相對另外兩個來講是較爲簡單的。其入口在前面分析過的任務調度入口函數performConcurrentWorkOnRoot中的結尾finishConcurrentRender。最終調用的函數爲commitRootImpl。看看代碼:

let nextEffect: Fiber | null = null;

function commitRootImpl(root, renderPriorityLevel) {
    // 省略...
    const finishedWork = root.finishedWork;
    const expirationTime = root.finishedExpirationTime;
    if (finishedWork === null) {
        return null;
    }
    root.finishedWork = null;
    root.finishedExpirationTime = NoWork;

    // commit不可中斷。 老是同步完成。
    // 所以,如今能夠清除這些內容以容許安排新的回調。
    root.callbackNode = null;
    root.callbackExpirationTime = NoWork;
    root.callbackPriority = NoPriority;
    root.nextKnownPendingLevel = NoWork;
    // 省略...

    // 獲取effectList
    let firstEffect;
    if (finishedWork.effectTag > PerformedWork) {
        if (finishedWork.lastEffect !== null) {
            finishedWork.lastEffect.nextEffect = finishedWork;
            firstEffect = finishedWork.firstEffect;
        } else {
            firstEffect = finishedWork;
        }
    } else {
        firstEffect = finishedWork.firstEffect;
    }

    if (firstEffect !== null) {
        // 省略...
        nextEffect = firstEffect;
        do {
            // [Q]: 執行 snapshot = getSnapshotBeforeUpdate()
            // 結果賦值爲 Fiber.stateNode.instance.__reactInternalSnapshotBeforeUpdate = snapshot
            commitBeforeMutationEffects();
        } while (nextEffect !== null);
        // 省略...
        nextEffect = firstEffect;
        do {
            // [Q]: 根據Fiber.effectTag 執行具體的增刪改DOM操做
            // 若是是卸載組件,還會調用 componentWillUnmount()
            commitMutationEffects(root, renderPriorityLevel);
        } while (nextEffect !== null);
        // 省略...
        nextEffect = firstEffect;
        do {
            // [Q]: 調用 render 後的生命週期
            // current === null ? componentDidMount : componentDidUpdate
            commitLayoutEffects(root, expirationTime);
        } while (nextEffect !== null);
        stopCommitLifeCyclesTimer();

        nextEffect = null;

        // 告訴Scheduler在幀末尾中止調度,這樣瀏覽器就有機會繪製。
        requestPaint();
        // 省略...
    } else {
        // 省略...
    }
    // 省略...
    return null;
}
複製代碼

省略了許多的代碼,留下主要的內容。主要邏輯就是拿到Reconciler維護的effectList鏈表後,三次遍歷該鏈表,分別作的是:

  1. 獲取Snapsshot;用於componentDidUpdate的第三個參數
  2. 根據Fiber.effectTag對組件或DOM執行具體操做
  3. 調用全部組件的生命週期函數

commitBeforeMutationEffects

完整代碼看commitBeforeMutationLifeCycles,其中tai爲ClassComponent的組件主要邏輯以下:

const current = nextEffect.alternate;
finishedWork = nextEffect;
if (finishedWork.effectTag & Snapshot) {
    if (current !== null) {
        const prevProps = current.memoizedProps;
        const prevState = current.memoizedState;
        const instance = finishedWork.stateNode;
        const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
                ? prevProps
                : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
        );
        instance.__reactInternalSnapshotBeforeUpdate = snapshot;
    }
}
複製代碼

commitMutationEffects

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;

        if (effectTag & ContentReset) {
            // 把節點的文字內容設置爲空字符串
            commitResetTextContent(nextEffect);
        }

        if (effectTag & Ref) {
            const current = nextEffect.alternate;
            if (current !== null) {
                // 把ref置空,後續會設置ref,因此以前ref上的值須要先清空
                commitDetachRef(current);
            }
        }
        let primaryEffectTag =
            effectTag & (Placement | Update | Deletion | Hydrating);
        switch (primaryEffectTag) {
            case Placement: {
                commitPlacement(nextEffect);
                // 從effectTag中清除Placement標記
                nextEffect.effectTag &= ~Placement;
                break;
            }
            case PlacementAndUpdate: {
                // Placement
                commitPlacement(nextEffect);
                nextEffect.effectTag &= ~Placement;

                // Update
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            case Update: {
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            case Deletion: {
                // componentWillUnmount
                commitDeletion(root, nextEffect, renderPriorityLevel);
                break;
            }
            // 省略...
        }
        nextEffect = nextEffect.nextEffect;
    }
}
複製代碼

好像也沒啥好說的。值得注意的是,開始前會先調用commitDetachRefref的引用清除。而後針對不一樣的effectTag執行不一樣的DOM操做。

  • commitPlacement; 新增節點。其中節點插入位置的計算算法能夠看下;
  • commitWork; 根據ReconcilerdiffProperties計算出來的updateQueue數組進行DOM更新
  • commitDeletion; 這一步會從上往下依次調用該子樹下每一個組件的componentWillUnmount函數

commitLayoutEffects

function commitLayoutEffects( root: FiberRoot, committedExpirationTime: ExpirationTime, ) {
    while (nextEffect !== null) {
        setCurrentDebugFiberInDEV(nextEffect);

        const effectTag = nextEffect.effectTag;

        if (effectTag & (Update | Callback)) {
            recordEffect();
            const current = nextEffect.alternate;
            commitLayoutEffectOnFiber(
                root,
                current,
                nextEffect,
                committedExpirationTime,
            );
        }

        if (effectTag & Ref) {
            recordEffect();
            commitAttachRef(nextEffect);
        }

        resetCurrentDebugFiberInDEV();
        nextEffect = nextEffect.nextEffect;
    }
}

function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedExpirationTime: ExpirationTime, ): void {
    switch (finishedWork.tag) {
        // ...
        case ClassComponent: {
            const instance = finishedWork.stateNode;
            if (finishedWork.effectTag & Update) {
                if (current === null) {
                    instance.componentDidMount();
                } else {
                    const prevProps =
                        finishedWork.elementType === finishedWork.type
                            ? current.memoizedProps
                            : resolveDefaultProps(finishedWork.type, current.memoizedProps);
                    const prevState = current.memoizedState;
                    instance.componentDidUpdate(
                        prevProps,
                        prevState,
                        instance.__reactInternalSnapshotBeforeUpdate,
                    );
                }
            }
            const updateQueue = finishedWork.updateQueue;
            if (updateQueue !== null) {
                // 調用setState註冊的回調函數
                commitUpdateQueue(finishedWork, updateQueue, instance);
            }
            return;
        }
        // ...
    }
}
複製代碼

仍是遍歷每一個Fiber節點。若是是ClassComponent,須要調用生命週期方法。同時對於更新的ClassComponent,須要判斷調用的setState是否有回調函數,若是有的話須要在這裏一塊兒調用。最後會調用commitAttachRef更新ref引用。

Commit階段的流程到這裏也就結束了。

說實話,React的源碼是在是真的多。想完徹底全細節分析到每個點,須要大量的時間和精力。本文也只是分析了一個大體的流程,不少細節之處沒有分析到位。後續會再花點時間針對一些細節問題作下探索。說到底,目前也只從面到面,而沒有達到從面到點分析的效果。許多觀點是我的的理解,寫出來是以供學習交流,有不妥之處,還請提提意見。

相關文章
相關標籤/搜索