React源碼03 - React 中的更新

03 - React 中的更新

React 中創(chuàng)建更新的方式:
初次渲染:ReactDOM.render纲酗、ReactDOM.hydrate
后續(xù)更新:setState衰腌、forceUpdate

1. ReactDOM.render()

  • 先創(chuàng)建 ReactRoot 頂點(diǎn)對(duì)象
  • 然后創(chuàng)建 FiberRoot 和 RootFiber
  • 創(chuàng)建更新,使應(yīng)用進(jìn)入更新調(diào)度過程

這個(gè)部分觅赊,只要了解流程即可右蕊,不要陷入各種旁支末節(jié),否則很難再 “return”出來吮螺,劃不來饶囚,先點(diǎn)到為止帕翻。

寫 JSX 的時(shí)候,只是調(diào)用了 createElement 創(chuàng)建了 element 樹萝风,還需要 render 進(jìn)一步進(jìn)行渲染和處理熊咽。
ReactDOM 源碼在 react-dom/src/client 下面,而 server 對(duì)應(yīng)的是服務(wù)端闹丐,這里只研究客戶端横殴。

const ReactDOM: Object = {    
  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    return legacyRenderSubtreeIntoContainer(
      null, // 沒有父組件
      element,
      container,
      false, // 不調(diào)和
      callback,
    );
    },
  
  // hydrate 和 render 唯一區(qū)別就是是否會(huì)調(diào)和 DOM 節(jié)點(diǎn),是否會(huì)復(fù)用節(jié)點(diǎn)卿拴,服務(wù)端的時(shí)候會(huì)用到衫仑,暫且不表
  hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      true,
      callback,
    );
  },
  // ... 其他方法略
}

渲染子樹到 container 中:

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {
    
  let root: Root = (container._reactRootContainer: any); 
  if (!root) {
    // Initial mount 首次掛載時(shí) container 上自然沒有綁定過 _reactRootContainer
    // 接著就是根據(jù)傳入的 container 創(chuàng)建 ReactRoot 并順便綁定到 container 上,
    // 這個(gè) ReactRoot 對(duì)象中的 _internalRoot 是一個(gè) FiberRoot。
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    // 首次渲染不需要所謂的批量更新
    DOMRenderer.unbatchedUpdates(() => {
      if (parentComponent != null) {
        root.legacy_renderSubtreeIntoContainer(
          parentComponent,
          children,
          callback,
        );
      } else {
        // 一般來說 parentComponnent 就是 null堕花,所以會(huì)走到這里提交更新
        root.render(children, callback); // 具體見后面的代碼塊
      }
    });
  } else {
    // 下次更新文狱,除了不再放入 DOMRenderer.unbatchedUpdates 回調(diào)中執(zhí)行,其他和首次渲染一樣
    // 略
  }
  return DOMRenderer.getPublicRootInstance(root._internalRoot);
}

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): Root {
    
  // 內(nèi)部通過判斷傳入的 root 節(jié)點(diǎn)是否有子節(jié)點(diǎn)來決定是否進(jìn)行調(diào)和缘挽。
  // 非服務(wù)端的話瞄崇,不涉及 hydrate 調(diào)和,接下來就是清空傳入的 root dom 下面的子節(jié)點(diǎn)壕曼,因?yàn)榻觼硐?react 要掛載自己的 dom 到 root 上苏研。
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content. 
  // 清空 container dom 下的子節(jié)點(diǎn)
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }
  }

  // Legacy roots are not async by default.
  const isConcurrent = false;
  // 
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

ReactRoot 是外層包裹,里面的 _internalRoot 才是 FiberRoot:

function ReactRoot(
  container: Container,
  isConcurrent: boolean,
  hydrate: boolean,
) {
  const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}

react-reconciler 包下腮郊,創(chuàng)建 FiberRoot:

export function createContainer(
  containerInfo: Container,
  isConcurrent: boolean,
  hydrate: boolean,
): OpaqueRoot {
  return createFiberRoot(containerInfo, isConcurrent, hydrate);
}

在前面的 legacyRenderSubtreeIntoContainer 中的 root.render(children, callback):


ReactRoot.prototype.render = function(
  children: ReactNodeList,
  callback: ?() => mixed,
): Work {
  const root = this._internalRoot;
  const work = new ReactWork();
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    work.then(callback);
  }
  DOMRenderer.updateContainer(children, root, null, work._onCommit);
  return work;
};

DOMRenderer.updateContainer 內(nèi)部的深層調(diào)用摹蘑。createUpdate() 創(chuàng)建 update 對(duì)象,把要更新的 element 添加到 update 上轧飞,然后 update 進(jìn)入更新隊(duì)列衅鹿,然后開始調(diào)度更新的工作:

function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
   
  const update = createUpdate(expirationTime);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  // 被調(diào)用的 element 作為 update 對(duì)象的載荷
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    warningWithoutStack(
      typeof callback === 'function',
      'render(...): Expected the last optional `callback` argument to be a ' +
        'function. Instead received: %s.',
      callback,
    );
    update.callback = callback;
  }
  // 把 update 加入更新隊(duì)列
  enqueueUpdate(current, update);
    // 開始調(diào)用更新
  scheduleWork(current, expirationTime);
  return expirationTime;
}

在 ReactRoot 中會(huì)創(chuàng)建 FiberRoot 然后賦值到 this._internalRoot ,this 就是指 ReactRoot 實(shí)例过咬。然后順便把內(nèi)部創(chuàng)建出來的 ReactRoot 對(duì)象綁定到最初傳入的 root dom 節(jié)點(diǎn)(通常是個(gè)div)的 _reactRootContainer 屬性上大渤,見下圖:

image.png
image.png

在 React 17中 ReactDOM.render() 不再能夠用來 hydrate 調(diào)和服務(wù)端渲染的 container,會(huì)被廢棄掸绞。有此需求應(yīng)直接使用 ReactDOM.hydrate() 泵三。

Using ReactDOM.render() to hydrate a server-rendered container is deprecated and will be removed in React 17. Use hydrate() instead.

2. FiberRoot

Fiber 解決了單線程計(jì)算量過大時(shí)交互、動(dòng)畫卡頓的問題集漾,常說的 “虛擬DOM” 就是指 Fiber 樹切黔。

FiberRoot:

  • 整個(gè)應(yīng)用的起點(diǎn)
  • 包含應(yīng)用掛載的目標(biāo)節(jié)點(diǎn)
  • 記錄整個(gè)應(yīng)用更新過程的各種信息

React.createElement() 創(chuàng)建出 ReactElement 節(jié)點(diǎn),組成 element 樹具篇, 每一個(gè)的 element 也都有對(duì)應(yīng)的 Fiber 節(jié)點(diǎn)纬霞,組成 Fiber 樹。
**FiberRoot **中的一些屬性:

  • containerInfo: root 節(jié)點(diǎn)驱显,即 ReactDOM.render() 方法接收到的第二個(gè)參數(shù)诗芜。
  • current:記錄了當(dāng)前入口 dom 節(jié)點(diǎn)所對(duì)應(yīng)的 Fiber 節(jié)點(diǎn)瞳抓,即 RootFiber(涉及到雙緩存/雙buff/double-buff 機(jī)制)。
  • finishedWork:一次更新渲染過程中完成了的那個(gè)更新任務(wù)伏恐。更新完成之后讀取該屬性孩哑,渲染至 dom 上。
  • nextExpirationTimeToWorkOn:下次更新時(shí)要執(zhí)行的那個(gè)任務(wù)翠桦,react 會(huì)遍歷 fiber 樹横蜒,讀取每個(gè) fiber 節(jié)點(diǎn)上的 ExpirationTIme,在 FiberRoot 上用該屬性記錄最高優(yōu)先級(jí)的那個(gè)任務(wù)销凑。
  • expirationTime: 當(dāng)前更新對(duì)應(yīng)的過期時(shí)間丛晌。
  • nextScheduledRoot 存在多個(gè) root 掛載點(diǎn)時(shí),會(huì)有多個(gè) FiberRoot斗幼,而這些 FiberRoot 會(huì)組成單向鏈表澎蛛,因此該屬性就是指向鏈表中下一個(gè) root 節(jié)點(diǎn)的“指針”。這個(gè)屬性蜕窿,也體現(xiàn)了谋逻,為什么在入口 dom 節(jié)點(diǎn)所對(duì)應(yīng)的 Fiber 節(jié)點(diǎn)上,還需要一層結(jié)構(gòu)桐经,即 FiberRoot毁兆。

3. Fiber

  • 每一個(gè) ReactElement 對(duì)應(yīng)一個(gè) Fiber 對(duì)象。
  • Fiber 對(duì)象上記錄了節(jié)點(diǎn)的各種狀態(tài)次询,包括 state 和 props荧恍。Fiber 更新完成之后瓷叫,state 和 props 才被更新到 class 組件的 this 上屯吊,也為 hooks 的實(shí)現(xiàn)提供了根基,因?yàn)闋顟B(tài)并不是靠 function 函數(shù)本身來維持的摹菠。
  • 串聯(lián)整個(gè)應(yīng)用形成樹結(jié)構(gòu)盒卸。

Fiber 樹遍歷時(shí)根據(jù) child、sibling次氨、return(parent)蔽介,Fiber 部分屬性如下:

  • tag: 標(biāo)記不同的組件類型
  • elementType: ReactElement.type,也就是我們調(diào)用 createElement() 的第一個(gè)參數(shù)煮寡。
  • stateNode:記錄組件實(shí)例虹蓄,如 class 組件的實(shí)例、原生 dom 實(shí)例幸撕,而 function 組件沒有實(shí)例薇组,因此該屬性是空。如 state坐儿、props 等狀態(tài)完成更新任務(wù)后律胀,react 會(huì)通過該屬性宋光,更新組件實(shí)例。需要強(qiáng)調(diào)的是炭菌,RootFiber 的 stateNode 屬性指向 FiberRoot罪佳,和 FiberRoot 上的 current 屬性相呼應(yīng)。
  • penndingProps: 新的 props
  • memorizedProps: 老的 props(上次渲染完成之后的 props)
  • memorizedState:老的 state(上次渲染完成之后的 state)
  • updateQueue: 該 Fiber 對(duì)應(yīng)的組件產(chǎn)生的 update 會(huì)存放于該隊(duì)列(類似于單向鏈表)中黑低。該過程產(chǎn)出的新的 state 會(huì)用來更新 memorizedState赘艳。
  • expirationTime: 代表任務(wù)在未來的哪個(gè)時(shí)間點(diǎn)應(yīng)該被完成,不包括他的子樹產(chǎn)生的任務(wù)克握。
  • childExpirationTime: 子樹中優(yōu)先級(jí)最高的過期時(shí)間第练,即最先的過期時(shí)間,用于快速確定子樹中是否有不在等待的變化玛荞。
  • alternate: 在 Fiber 樹每次更新時(shí)娇掏,每個(gè) FIber 都會(huì)有一個(gè)與其對(duì)應(yīng)的 Fiber,稱為“current <--> workInProgress”勋眯。React 應(yīng)用的根節(jié)點(diǎn)(FiberRoot)通過 current 指針在不同 Fiber 樹間進(jìn)行切換毒涧,從而兩個(gè) Fiber 樹輪流復(fù)用(雙緩存機(jī)制)检痰,而不是每次更新都創(chuàng)建新的 Fiber 樹。其中很多 workInProgress fiber 的創(chuàng)建可以復(fù)用 current Fiber 樹對(duì)應(yīng)的節(jié)點(diǎn)數(shù)據(jù)(因?yàn)槊總€(gè) Fiber 節(jié)點(diǎn)都有 alternate 指向?qū)?yīng)的節(jié)點(diǎn)),這個(gè)決定是否復(fù)用 current Fiber 樹對(duì)應(yīng)節(jié)點(diǎn)數(shù)據(jù)的過程就是 Diff 算法睬罗。
  • mode: 用來描述當(dāng)前 Fiber 和其子樹的模式(后面會(huì)提到)

// Effect 系列

  • effectTag: SideEffectTag。用來記錄 SideEffect休建。
  • nextEffect: Fiber | null袍镀。單鏈表用來快速查找下一個(gè)side effect。
  • firstEffect: Fiber | null辆琅。 子樹中第一個(gè)side effect漱办。
  • lastEffect: Fiber | null。子樹中最后一個(gè)side effect婉烟。

TODO: 補(bǔ)一張 Fiber 樹圖娩井。

Fiber.tag

export type WorkTag =
  | 0
  | 1
  | 2
  | 3
  | 4
  | 5
  | 6
  | 7
  | 8
  | 9
  | 10
  | 11
  | 12
  | 13
  | 14
  | 15
  | 16
  | 17
  | 18;

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;

4. update 和 updateQueue

Update:

  • 用于記錄組件狀態(tài)的改變
  • 存放于 UpdateQueue 中
  • 多個(gè) Update 可以同時(shí)存在(因?yàn)榉旁陉?duì)列中)
export type Update<State> = {
  // 更新的過期時(shí)間
  expirationTime: ExpirationTime,

  // export const UpdateState = 0;
  // export const ReplaceState = 1;
  // export const ForceUpdate = 2;
  // export const CaptureUpdate = 3;
  // 指定更新的類型,值為以上幾種
  tag: 0 | 1 | 2 | 3,
  // 更新內(nèi)容似袁,比如`setState`接收的第一個(gè)參數(shù)
  payload: any,
  // 對(duì)應(yīng)的回調(diào)洞辣,`setState`,`render`都有
  callback: (() => mixed) | null,

  // 指向下一個(gè)更新
  next: Update<State> | null,
  // 指向下一個(gè)`side effect`
  nextEffect: Update<State> | null,
};

export type UpdateQueue<State> = {
  // 每次操作完更新之后的`state`
  baseState: State,

  // 隊(duì)列中的第一個(gè)`Update`
  firstUpdate: Update<State> | null,
  // 隊(duì)列中的最后一個(gè)`Update`
  lastUpdate: Update<State> | null,

  // 第一個(gè)捕獲類型的`Update`
  firstCapturedUpdate: Update<State> | null,
  // 最后一個(gè)捕獲類型的`Update`
  lastCapturedUpdate: Update<State> | null,

  // 第一個(gè)`side effect`
  firstEffect: Update<State> | null,
  // 最后一個(gè)`side effect`
  lastEffect: Update<State> | null,

  // 第一個(gè)和最后一個(gè)捕獲產(chǎn)生的`side effect`
  firstCapturedEffect: Update<State> | null,
  lastCapturedEffect: Update<State> | null,
};

Update:

  • expirationTime: 更新的過期時(shí)間昙衅。
  • payload:首次渲染 payload 是整個(gè) element 樹扬霜,而后續(xù)如 setState 觸發(fā)更新,則 payload 是 setState 傳入的參數(shù)而涉,即 state 對(duì)象或者函數(shù)著瓶。
  • tag: 0 | 1 | 2 | 3 指定更新的類型,值為:UpdateState | ReplaceState | ForceUpdate | CaptureUpdate)
  • callback: 對(duì)應(yīng)的回調(diào)婴谱, setState 或者 render 都有蟹但。
  • next: 指向下一個(gè)更新躯泰。
  • nextEffect: 指向下一個(gè) side effect。

UpdateQueue:

  • baseState:每次操作完更新之后的 state华糖,作為下次更新 state 時(shí)的計(jì)算依據(jù)麦向。
  • firstUpdate: 更新隊(duì)列中第一個(gè) Update
  • lastUpdate: 更新隊(duì)列中最后一個(gè) Update
  • firstCapturedUpdate
  • lastCapturedUpdate
  • firstEffect
  • lastEffect
  • firstCapturedEffect
  • lastCapturedEffect

上面說過 ReatDOM.render() 時(shí)創(chuàng)建 Update 并添加到 UpdateQueue 中。enqueueUpdate() (位于 react-reconciler/ReactUpdateQueue.js)用于初始化 Fiber 對(duì)象上的 updateQueue客叉,以及如果已經(jīng)存在時(shí)則更新這個(gè)隊(duì)列诵竭。在此過程中,保持雙 Fiber 的 updateQueue 的首尾 queue 一致兼搏。

enqueueUpdate() :

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // Update queues are created lazily.
  const alternate = fiber.alternate;
  let queue1;
  let queue2;
  
 // 創(chuàng)建或更新隊(duì)列卵慰,若兩個(gè)隊(duì)列都不存在,則各自創(chuàng)建一個(gè)隊(duì)列佛呻;
 // 若其中一個(gè)隊(duì)列存在時(shí)裳朋,則 clone 出另一個(gè)隊(duì)列,會(huì)共享三個(gè)屬性:baseState吓著、firstUpdate鲤嫡、lastUpdate
 if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // There are two owners.
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // Neither fiber has an update queue. Create new ones.
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(
          alternate.memoizedState,
        );
      } else {
        // Only one fiber has an update queue. Clone to create a new one.
        queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
      }
    } else {
      if (queue2 === null) {
        // Only one fiber has an update queue. Clone to create a new one.
        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
      } else {
        // Both owners have an update queue.
      }
    }
  }
  
  // 2. 調(diào)用 appendUpdateToQueue() 將 update 添加到隊(duì)列鏈表中
  if (queue2 === null || queue1 === queue2) {
    // There's only a single queue.
    appendUpdateToQueue(queue1, update);
  } else {
    // There are two queues. We need to append the update to both queues,
    // while accounting for the persistent structure of the list — we don't
    // want the same update to be added multiple times.
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
      // One of the queues is not empty. We must add the update to both queues.
      appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // Both queues are non-empty. The last update is the same in both lists,
      // because of structural sharing. So, only append to one of the lists.
      appendUpdateToQueue(queue1, update);
      // But we still need to update the `lastUpdate` pointer of queue2.
      queue2.lastUpdate = update;
    }
  }
}

appendUpdateToQueue() ,UpdateQueue 顯然是一個(gè)基于鏈表的隊(duì)列绑莺,看情況更新首尾指針即可:

function appendUpdateToQueue<State>(
  queue: UpdateQueue<State>,
  update: Update<State>,
) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}

cloneUpdateQueue :

function cloneUpdateQueue<State>(
  currentQueue: UpdateQueue<State>,
): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    // 克隆隊(duì)列時(shí)這三個(gè)屬性是共享的
    baseState: currentQueue.baseState,
    firstUpdate: currentQueue.firstUpdate,
    lastUpdate: currentQueue.lastUpdate,

    // TODO: With resuming, if we bail out and resuse the child tree, we should
    // keep these effects.
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,

    firstEffect: null,
    lastEffect: null,

    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}

5. ExpirationTime

尤其對(duì)于異步任務(wù)來說暖眼,過期時(shí)間是某個(gè)更新任務(wù)告訴 react 在過期時(shí)間未到之前,自己可以被打斷纺裁。但如果過期時(shí)間已經(jīng)到了诫肠,而更新任務(wù)依舊未得到執(zhí)行,則會(huì)被強(qiáng)制執(zhí)行欺缘。

  • currentTime:簡單理解當(dāng)前時(shí)間距 JS 加載完成時(shí)的時(shí)間
    • 在一次渲染中產(chǎn)生的更新需要使用相同的時(shí)間
    • 一次批處理的更新應(yīng)該得到相同的時(shí)間
    • 掛起任務(wù)用于記錄的時(shí)候應(yīng)該相同
  • expirationTime:過期時(shí)間

react-reconciler/ReactFiberReconciler.js
updateContainer() :

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current;
  // 獲取 currentTime
  const currentTime = requestCurrentTime();
  // 根據(jù) currentTime 計(jì)算過期時(shí)間(其實(shí)并不是直接計(jì)算栋豫,而是先調(diào)用)
  const expirationTime = computeExpirationForFiber(currentTime, current);
  // 然后就是上面剛剛說過的創(chuàng)建 update 和 updateQueue 的過程
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

requestCurrentTime() :
同一事件中的兩個(gè)更新計(jì)劃應(yīng)被處理為同時(shí)發(fā)生,即使它們的時(shí)鐘時(shí)間必然有先有后浪南。因?yàn)?expirationTime 決定了如何處理批量更新笼才,所以這里出于性能考慮,在同一事件中络凿,類似優(yōu)先級(jí)的更新任務(wù)會(huì)得到相同的 currentTime,從而后面計(jì)算出相同的 expirationTime昂羡,這些任務(wù)在某一時(shí)刻同時(shí)更新絮记,避免短期內(nèi)多次頻繁更新崩潰:

function requestCurrentTime() {
  // requestCurrentTime is called by the scheduler to compute an expiration
  // time.
  //
  // Expiration times are computed by adding to the current time (the start
  // time). However, if two updates are scheduled within the same event, we
  // should treat their start times as simultaneous, even if the actual clock
  // time has advanced between the first and second call.

  // In other words, because expiration times determine how updates are batched,
  // we want all updates of like priority that occur within the same event to
  // receive the same expiration time. Otherwise we get tearing.
  //
  // We keep track of two separate times: the current "renderer" time and the
  // current "scheduler" time. The renderer time can be updated whenever; it
  // only exists to minimize the calls performance.now.
  //
  // But the scheduler time can only be updated if there's no pending work, or
  // if we know for certain that we're not in the middle of an event.

  if (isRendering) {
    // We're already rendering. Return the most recently read time.
    return currentSchedulerTime;
  }
  // Check if there's pending work.
  findHighestPriorityRoot();
  if (
    nextFlushedExpirationTime === NoWork ||
    nextFlushedExpirationTime === Never
  ) {
    // If there's no pending work, or if the pending work is offscreen, we can
    // read the current time without risk of tearing.
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;
    return currentSchedulerTime;
  }
  // There's already pending work. We might be in the middle of a browser
  // event. If we were to read the current time, it could cause multiple updates
  // within the same event to receive different expiration times, leading to
  // tearing. Return the last read time. During the next idle callback, the
  // time will be updated.
  return currentSchedulerTime;
}

computeExpirationForFiber() 方法更多信息下一小節(jié)再說,其中涉及到的 expirationTime 計(jì)算過程:

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';

export type ExpirationTime = number;

export const NoWork = 0;
export const Sync = 1;
export const Never = MAX_SIGNED_31_BIT_INT;

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 2;

// 1 unit of expiration time represents 10ms.
export function msToExpirationTime(ms: number): ExpirationTime {
  // Always add an offset so that we don't clash with the magic number for NoWork.
  return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
}

export function expirationTimeToMs(expirationTime: ExpirationTime): number {
  return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE;
}

function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET +
    ceiling(
      currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}

export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;

export function computeAsyncExpiration(
  currentTime: ExpirationTime,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  );
}

// We intentionally set a higher expiration time for interactive updates in
// dev than in production.
//
// If the main thread is being blocked so long that you hit the expiration,
// it's a problem that could be solved with better scheduling.
//
// People will be more likely to notice this and fix it with the long
// expiration time in development.
//
// In production we opt for better UX at the risk of masking scheduling
// problems, by expiring fast.
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;

export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  );
}

過期時(shí)間 = 當(dāng)前時(shí)間 + 延遲
延遲的時(shí)間長度如下(ms):

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150; // 高優(yōu)先級(jí)任務(wù)的過期時(shí)間基礎(chǔ)偏移量
export const HIGH_PRIORITY_BATCH_SIZE = 100;

export const LOW_PRIORITY_EXPIRATION = 5000; // 高優(yōu)先級(jí)任務(wù)的過期時(shí)間基礎(chǔ)偏移量
export const LOW_PRIORITY_BATCH_SIZE = 250;

最終計(jì)算出的 expirationTime 的精度是 10ms(高優(yōu)先級(jí)) 或者 25ms(低優(yōu)先級(jí))虐先,即 expirationTime 會(huì)是 10 或者 25 的整數(shù)倍怨愤。

bucketSIzeMs / UNIT_SIZE 精度在這里的意義
如果在一個(gè)操作內(nèi)多次調(diào)用了 setState,即便前后調(diào)用的時(shí)間差距可能很小蛹批,但毫秒級(jí)別還是有差距撰洗,那么計(jì)算出的 expirationTime 也就不一樣篮愉,任務(wù)優(yōu)先級(jí)也就不一樣,導(dǎo)致 react 更新多次差导,導(dǎo)致整個(gè)應(yīng)用性能下降试躏。
而有了 精度/粒度 的控制,使得非常詳盡的兩次更新设褐,即使具有微小的 currentTime 差異颠蕴,也會(huì)得到相同的 expirationTime,從而到時(shí)候在一次更新中一起完成(批量更新)助析。

currentTime 和 expirationTime 在各自計(jì)算過程中犀被,為了性能都在**保證在一個(gè)批量更新中產(chǎn)生的同類型的更新,應(yīng)具有相同的過期時(shí)間外冀。 **否則全部用當(dāng)前時(shí)間加上固定的延遲作為未來的過期時(shí)間就用不著計(jì)算這么麻煩了寡键。

6. 不同的 ExpirationTime

  • NoWork: 代表沒有更新
  • Sync: 代表同步執(zhí)行,不會(huì)被調(diào)度也不會(huì)被打斷
  • async: 異步模式下計(jì)算出來的過期時(shí)間雪隧,一個(gè)時(shí)間戳昌腰,會(huì)被調(diào)度,同時(shí)還可能被打斷
export const NoWork = 0;
export const Sync = 1;
export const Never = MAX_SIGNED_31_BIT_INT;

上一小節(jié)中提到膀跌,得到 currentTime 后遭商,會(huì)調(diào)用 computeExpirationForFiber() 然后返回 expirationTime,還涉及到過期時(shí)間的復(fù)雜的計(jì)算公式捅伤,但有些過期時(shí)間的計(jì)算其實(shí)不需要調(diào)用計(jì)算公式:
比如后面提到的 flushSync 中把 expirationContext 改為 Sync劫流,直接進(jìn)入下面第一個(gè)條件判斷,最終得到的過期時(shí)間直接就是 Sync丛忆,也就是 0ms:

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  if (expirationContext !== NoWork) {
    // An explicit expiration context was set;
    expirationTime = expirationContext;
  } else if (isWorking) {
    if (isCommitting) {
      // Updates that occur during the commit phase should have sync priority
      // by default.
      expirationTime = Sync;
    } else {
      // Updates during the render phase should expire at the same time as
      // the work that is being rendered.
      expirationTime = nextRenderExpirationTime;
    }
  } else {
    // No explicit expiration context was set, and we're not currently
    // performing work. Calculate a new expiration time.
    if (fiber.mode & ConcurrentMode) {
      if (isBatchingInteractiveUpdates) {
        // This is an interactive update
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        // This is an async update
        expirationTime = computeAsyncExpiration(currentTime);
      }
      // If we're in the middle of rendering a tree, do not update at the same
      // expiration time that is already rendering.
      if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
        expirationTime += 1;
      }
    } else {
      // This is a sync update
      expirationTime = Sync;
    }
  }
  if (isBatchingInteractiveUpdates) {
    // This is an interactive update. Keep track of the lowest pending
    // interactive expiration time. This allows us to synchronously flush
    // all interactive updates when needed.
    if (expirationTime > lowestPriorityPendingInteractiveExpirationTime) {
      lowestPriorityPendingInteractiveExpirationTime = expirationTime;
    }
  }
  return expirationTime;
}
  • 通過外部來強(qiáng)制某一個(gè)更新必須使用哪一種 expirationTime(指定 expirationContext):

比如使用 ReactDOM.flushSync() (該方法實(shí)際存在于 react-reconciler/ReactFiberScheduler.js 中) 可以指定 expirationTime 為 1ms祠汇,意味著同步更新:

import { flushSync } from 'react-dom';
// ...
    handleClick = () => {
    flushSync(() => {
      this.setState({ text: '666' });
    });
  };
// ...
let expirationContext: ExpirationTime = NoWork;

// ...

function flushSync<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return syncUpdates(fn, a);
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    performSyncWork();
  }
}

function syncUpdates<A, B, C0, D, R>(
  fn: (A, B, C0, D) => R,
  a: A,
  b: B,
  c: C0,
  d: D,
): R {
  const previousExpirationContext = expirationContext;
  expirationContext = Sync; // 設(shè)置為 1
  try {
    return fn(a, b, c, d); // 傳入 flushSync 的回調(diào)函數(shù)在這里被執(zhí)行
  } finally {
    expirationContext = previousExpirationContext; // 把 expirationContext 恢復(fù)成 NoWork
  }
}
  • isWorking/isCommitting,即有任務(wù)更新的時(shí)候:

同樣也不需要什么計(jì)算公式熄诡。具體留待后面涉及更新的時(shí)候再說可很。

  • 處于 ConcurrentMode 模式下才需要異步更新,即需要用到計(jì)算公式:

對(duì)于大部分的 react 事件系統(tǒng)產(chǎn)生的更新凰浮,這里的 isBatchingInteractiveUpdates 會(huì)是 true 我抠,也就是高優(yōu)先級(jí)的任務(wù),過期時(shí)間會(huì)更短袜茧。

  if (fiber.mode & ConcurrentMode) {
        // 大部分的 react 事件產(chǎn)生的更新中 isBatchingInteractiveUpdates 會(huì)是 true 菜拓,
      // 也就是高優(yōu)先級(jí)的任務(wù),過期時(shí)間會(huì)更短笛厦。
      if (isBatchingInteractiveUpdates) {
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        expirationTime = computeAsyncExpiration(currentTime);
      }
      // 正在渲染樹時(shí)纳鼎,新加入的更新的過期時(shí)間+1 以遍不會(huì)和當(dāng)前更新一起更新。后續(xù)講更新時(shí)再細(xì)說
      if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
        expirationTime += 1;
      }
    } else {
      expirationTime = Sync;
    }

至于 fiber.mode & ConcurrentMode 這種按位操作的表達(dá)式,其實(shí)就是使用位運(yùn)算進(jìn)行屬性的讀寫贱鄙。
使用一個(gè)若干位的二進(jìn)制數(shù)表達(dá)(存儲(chǔ))若干個(gè)布爾屬性劝贸,設(shè)置屬性使用按位異或 ^ ,查詢屬性使用按位與 & 逗宁。
Fiber 上的 mode:

export type TypeOfMode = number;

export const NoContext = 0b000;
export const ConcurrentMode = 0b001;
export const StrictMode = 0b010;
export const ProfileMode = 0b100;

7. setState 和 forceUpdate

在 react 中能合理產(chǎn)生更新的方式映九,同時(shí)也是 react 推崇的方式有以下幾種:

  • ReactDOM.render() 首次渲染
  • setState(class 組件)
  • forceUpdate (class 組件)這個(gè)其實(shí)也很少使用
  • useState (函數(shù)式組件中的 hooks)

ReactDOM.render 創(chuàng)建的更新是放在 RootFiber 上面,是整體的初始化渲染疙剑。
是針對(duì)setState 和 forceUpdate 是為節(jié)點(diǎn)的 Fiber 創(chuàng)建更新氯迂,是針對(duì)某一個(gè) class component 而言。

和之前的 ReactDOM.render() 內(nèi)部會(huì)調(diào)用的 updateContaine() 方法很像言缤,在 enqueueSetState()enqueueForceUpdate() 中:

  • 拿到 fiber嚼蚀,得到 currentTime,一起作為 computeExpirationForFibe() 的參數(shù)算出 expirationTime管挟。
  • 然后創(chuàng)建 update 對(duì)象轿曙,然后 enqueueUpdate() 時(shí)看情況創(chuàng)建或更新隊(duì)列,然后進(jìn)行調(diào)度僻孝。

如之前說的导帝,update 對(duì)象上的 payload 載荷在 ReactDOM.render 時(shí)是 element 樹,而在 setState 或 forceUpdate 時(shí)是傳入的新的 state 對(duì)象(可能是局部的 state 對(duì)象)穿铆。

forceUpdate 和 setState 進(jìn)行 enqueue 時(shí)唯一不同點(diǎn)在于 forceUpdate 所創(chuàng)建的 update 對(duì)象上的 tag 會(huì)是 ForceUpdate 而不是默認(rèn)的 UpdateState 您单。

react-reconciler/src/ReactFiberClassComponent.js 中:

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.tag = ReplaceState;
    update.payload = payload;

    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'replaceState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueForceUpdate(inst, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'forceUpdate');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
};

可見,在 react 中創(chuàng)建更新的過程基本一樣荞雏,虐秦。而更多的技術(shù)細(xì)節(jié)會(huì)在整體的 Scheduler 調(diào)度方面。

ReactDOM.render/setState/forceUpdate 最終都會(huì)創(chuàng)建 update 對(duì)象凤优,掛載 payload 載荷悦陋,并添加到各自 Fiber 節(jié)點(diǎn)上的 updateQueue 中,然后即將進(jìn)入下一環(huán)節(jié)筑辨,開始 scheduleWork 即調(diào)度工作俺驶。
下一篇就來分析創(chuàng)建更新隊(duì)列之后,react 如何進(jìn)行統(tǒng)一調(diào)度棍辕。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末暮现,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子痢毒,更是在濱河造成了極大的恐慌送矩,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哪替,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡菇怀,警方通過查閱死者的電腦和手機(jī)凭舶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門晌块,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人帅霜,你說我怎么就攤上這事匆背。” “怎么了身冀?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵钝尸,是天一觀的道長。 經(jīng)常有香客問我搂根,道長珍促,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任剩愧,我火速辦了婚禮猪叙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仁卷。我一直安慰自己穴翩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布锦积。 她就那樣靜靜地躺著芒帕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪丰介。 梳的紋絲不亂的頭發(fā)上背蟆,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音基矮,去河邊找鬼淆储。 笑死,一個(gè)胖子當(dāng)著我的面吹牛家浇,可吹牛的內(nèi)容都是我干的本砰。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼钢悲,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼点额!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起莺琳,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤还棱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后惭等,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體珍手,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了琳要。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寡具。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖稚补,靈堂內(nèi)的尸體忽然破棺而出童叠,到底是詐尸還是另有隱情,我是刑警寧澤课幕,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布厦坛,位于F島的核電站,受9級(jí)特大地震影響乍惊,放射性物質(zhì)發(fā)生泄漏杜秸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一污桦、第九天 我趴在偏房一處隱蔽的房頂上張望亩歹。 院中可真熱鬧,春花似錦凡橱、人聲如沸小作。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽顾稀。三九已至,卻和暖如春坝撑,著一層夾襖步出監(jiān)牢的瞬間静秆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國打工巡李, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抚笔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓侨拦,卻偏偏與公主長得像殊橙,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子狱从,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348