React源碼解析之scheduleWork(上)

從本篇開始,咱們就正式進入React的核心調度算法—Fiber調度機制。javascript

前言:
你須要知道:淺談React 16中的Fiber機制React源碼解析之RootFiberReact源碼解析之FiberRootphp

在以前的文章中講到,React更新的方式有三種:
(1)ReactDOM.render() || hydrate(ReactDOMServer渲染)
(2)setState()
(3)forceUpdate()java

createUpdate後就進入scheduleWork流程,接下來咱們就正式進入調度流程node

1、scheduleUpdateOnFiber()
做用:
調度update任務react

提示:
scheduleWorkscheduleUpdateOnFibergit

export const scheduleWork = scheduleUpdateOnFiber;
複製代碼

源碼:github

//scheduleWork
export function scheduleUpdateOnFiber(
  fiber: Fiber,
  expirationTime: ExpirationTime,
{
  //判斷是不是無限循環update
  checkForNestedUpdates();
  //測試環境用的,不看
  warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);
  //找到rootFiber並遍歷更新子節點的expirationTime
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }
  //NoWork表示無更新操做
  root.pingTime = NoWork;
  //判斷是否有高優先級任務打斷當前正在執行的任務
  checkForInterruption(fiber, expirationTime);
  //報告調度更新,測試環境用的,可不看
  recordScheduleUpdate();

  // TODO: computeExpirationForFiber also reads the priority. Pass the
  // priority as an argument to that function and this one.
  const priorityLevel = getCurrentPriorityLevel();
  //1073741823
  //若是expirationTime等於最大整型值的話
  //若是是同步任務的過時時間的話
  if (expirationTime === Sync) {
    //若是還未渲染,update是未分批次的,
    //也就是第一次渲染前
    if (
      // Check if we're inside unbatchedUpdates
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // Check if we're not already rendering
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      //跟蹤這些update,並計數、檢測它們是否會報錯
      schedulePendingInteractions(root, expirationTime);

      // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
      // root inside of batchedUpdates should be synchronous, but layout updates
      // should be deferred until the end of the batch.
      //批量更新時,render是要保持同步的,但佈局的更新要延遲到批量更新的末尾才執行

      //初始化root
      //調用workLoop進行循環單元更新
      let callback = renderRoot(root, Sync, true);
      while (callback !== null) {
        callback = callback(true);
      }
    }
    //render後
    else {
      //當即執行調度任務
      scheduleCallbackForRoot(root, ImmediatePriority, Sync);
      //當前沒有update時
      if (executionContext === NoContext) {
        // Flush the synchronous work now, wnless we're already working or inside
        // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
        // scheduleCallbackForFiber to preserve the ability to schedule a callback
        // without immediately flushing it. We only do this for user-initated
        // updates, to preserve historical behavior of sync mode.
        //刷新同步任務隊列
        flushSyncCallbackQueue();
      }
    }
  }
  //若是是異步任務的話,則當即執行調度任務
  else {
    scheduleCallbackForRoot(root, priorityLevel, expirationTime);
  }

  if (
    (executionContext & DiscreteEventContext) !== NoContext &&
    // Only updates at user-blocking priority or greater are considered
    // discrete, even inside a discrete event.
    // 只有在用戶阻止優先級或更高優先級的更新才被視爲離散,即便在離散事件中也是如此
    (priorityLevel === UserBlockingPriority ||
      priorityLevel === ImmediatePriority)
  ) {
    // This is the result of a discrete event. Track the lowest priority
    // discrete update per root so we can flush them early, if needed.
    //這是離散事件的結果。 跟蹤每一個根的最低優先級離散更新,以便咱們能夠在須要時儘早清除它們。
    //若是rootsWithPendingDiscreteUpdates爲null,則初始化它
    if (rootsWithPendingDiscreteUpdates === null) {
      //key是root,value是expirationTime
      rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
    } else {
      //獲取最新的DiscreteTime
      const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
      //更新DiscreteTime
      if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
        rootsWithPendingDiscreteUpdates.set(root, expirationTime);
      }
    }
  }
}
複製代碼

解析:
有點長,咱們慢慢看web

2、checkForNestedUpdates()
做用:
判斷是不是無限循環的update算法

源碼:sql

// Use these to prevent an infinite loop of nested updates
const NESTED_UPDATE_LIMIT = 50;
let nestedUpdateCount: number = 0;
//防止無限循環地嵌套更新
function checkForNestedUpdates({
  if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
    nestedUpdateCount = 0;
    rootWithNestedUpdates = null;
    invariant(
      false,
      'Maximum update depth exceeded. This can happen when a component ' +
        'repeatedly calls setState inside componentWillUpdate or ' +
        'componentDidUpdate. React limits the number of nested updates to ' +
        'prevent infinite loops.',
    );
  }
}
複製代碼

解析:
超過 50層嵌套update,就終止進行調度,並報出error

常見的形成死循環的爲兩種狀況:
① 在render()中無條件調用setState()
注意:
有條件調用setState()的話,是能夠放在render()中的

render(){
  if(xxx){
  this.setState({ yyy })
}
}
複製代碼

② 以下圖,在shouldComponentUpdate()componentWillUpdate()中調用setState()

setState死循環
setState死循環

3、markUpdateTimeFromFiberToRoot()
做用:
找到rootFiber並遍歷更新子節點的expirationTime

源碼:

//目標fiber會向上尋找rootFiber對象,在尋找的過程當中會進行一些操做
function markUpdateTimeFromFiberToRoot(fiber, expirationTime{
  // Update the source fiber's expiration time
  //若是fiber對象的過時時間小於 expirationTime,則更新fiber對象的過時時間

  //也就是說,當前fiber的優先級是小於expirationTime的優先級的,如今要調高fiber的優先級
  if (fiber.expirationTime < expirationTime) {
    fiber.expirationTime = expirationTime;
  }
  //在enqueueUpdate()中有講到,與fiber.current是映射關係
  let alternate = fiber.alternate;
  //同上
  if (alternate !== null && alternate.expirationTime < expirationTime) {
    alternate.expirationTime = expirationTime;
  }
  // Walk the parent path to the root and update the child expiration time.
  //向上遍歷父節點,直到root節點,在遍歷的過程當中更新子節點的expirationTime

  //fiber的父節點
  let node = fiber.return;
  let root = null;
  //node=null,表示是沒有父節點了,也就是到達了RootFiber,即最大父節點
  //HostRoot即樹的頂端節點root
  if (node === null && fiber.tag === HostRoot) {
    //RootFiber的stateNode就是FiberRoot
    root = fiber.stateNode;
  }
  //沒有到達FiberRoot的話,則進行循環
  else {
    while (node !== null) {
      alternate = node.alternate;
      //若是父節點的全部子節點中優先級最高的更新時間仍小於expirationTime的話
      //則提升優先級
      if (node.childExpirationTime < expirationTime) {
        //從新賦值
        node.childExpirationTime = expirationTime;
        //alternate是相對於fiber的另外一個對象,也要進行更新
        if (
          alternate !== null &&
          alternate.childExpirationTime < expirationTime
        ) {
          alternate.childExpirationTime = expirationTime;
        }
      }
      //別看差了是對應(node.childExpirationTime < expirationTime)的if
      else if (
        alternate !== null &&
        alternate.childExpirationTime < expirationTime
      ) {
        alternate.childExpirationTime = expirationTime;
      }
      //若是找到頂端rootFiber,結束循環
      if (node.return === null && node.tag === HostRoot) {
        root = node.stateNode;
        break;
      }
      node = node.return;
    }
  }
  //更新該rootFiber的最舊、最新的掛起時間
  if (root !== null) {
    // Update the first and last pending expiration times in this root
    const firstPendingTime = root.firstPendingTime;
    if (expirationTime > firstPendingTime) {
      root.firstPendingTime = expirationTime;
    }
    const lastPendingTime = root.lastPendingTime;
    if (lastPendingTime === NoWork || expirationTime < lastPendingTime) {
      root.lastPendingTime = expirationTime;
    }
  }

  return root;
}
複製代碼

解析:
markUpdateTimeFromFiberToRoot()主要作了如下事情
(1)更新fiber對象的expirationTime
(2)根據fiber.return向上遍歷尋找RootFiber(fiber的頂層對象)
(3)在向上遍歷的過程當中,更新父對象fiber.return子節點的childExpirationTime

關於RootFiber,請參考:React源碼解析之RootFiber

(4)找到RootFiber後,根據RootFiber.stateNode=FiberRoot的關係,找到FiberRoot

(5)更新該rootFiber的最舊、最新的掛起時間
(6)返回RootFiber

4、checkForInterruption()
做用:
判斷是否有高優先級任務打斷當前正在執行的任務

源碼:

//判斷是否有高優先級任務打斷當前正在執行的任務
function checkForInterruption(
  fiberThatReceivedUpdate: Fiber,
  updateExpirationTime: ExpirationTime,
{
  //若是任務正在執行,而且異步任務已經執行到一半了,
  //可是如今須要把執行權交給瀏覽器,去執行優先級更高的任務
  if (
    enableUserTimingAPI &&
    workInProgressRoot !== null &&
    updateExpirationTime > renderExpirationTime
  ) {
    //打斷當前任務,優先執行新的update
    interruptedBy = fiberThatReceivedUpdate;
  }
}
複製代碼

解析:
若是當前fiber的優先級更高,須要打斷當前執行的任務,當即執行該fiber上的update,則更新interruptedBy

5、getCurrentPriorityLevel()
做用:
獲取當前調度任務的優先級

源碼:

// Except for NoPriority, these correspond to Scheduler priorities. We use
// ascending numbers so we can compare them like numbers. They start at 90 to
// avoid clashing with Scheduler's priorities.
//除了90,用數字是由於這樣作,方便比較
//從90開始的緣由是防止和Scheduler的優先級衝突
export const ImmediatePriority: ReactPriorityLevel = 99;
export const UserBlockingPriority: ReactPriorityLevel = 98;
export const NormalPriority: ReactPriorityLevel = 97;
export const LowPriority: ReactPriorityLevel = 96;
export const IdlePriority: ReactPriorityLevel = 95;
// NoPriority is the absence of priority. Also React-only.
export const NoPriority: ReactPriorityLevel = 90;

//獲取當前調度任務的優先級
export function getCurrentPriorityLevel(): ReactPriorityLevel {
  switch (Scheduler_getCurrentPriorityLevel()) {
      //99
    case Scheduler_ImmediatePriority:
      return ImmediatePriority;
      //98
    case Scheduler_UserBlockingPriority:
      return UserBlockingPriority;
      //97
    case Scheduler_NormalPriority:
      return NormalPriority;
      //96
    case Scheduler_LowPriority:
      return LowPriority;
      //95
    case Scheduler_IdlePriority:
      return IdlePriority;
    default:
      invariant(false'Unknown priority level.');
  }
}
複製代碼

Scheduler_getCurrentPriorityLevel():

const {
  unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel,
} = Scheduler;

function unstable_getCurrentPriorityLevel({
  return currentPriorityLevel;
}
//當前調度優先級默認爲 NormalPriority
var currentPriorityLevel = NormalPriority;
複製代碼

解析:
記住scheduler優先級是90往上,而且默認是NormalPriority(97)


若是是同步任務,而且是初次render()的話,會先執行schedulePendingInteractions()


6、schedulePendingInteractions()
做用:
跟蹤須要同步執行的update們,並計數、檢測它們是否會報錯

源碼:
schedulePendingInteractions():

//跟蹤這些update,並計數、檢測它們是否會報錯
function schedulePendingInteractions(root, expirationTime) {
  // This is called when work is scheduled on a root.
  // It associates the current interactions with the newly-scheduled expiration.
  // They will be restored when that expiration is later committed.
  //當調度開始時就執行,每調度一個update,就更新跟蹤棧
  if (!enableSchedulerTracing) {
    return;
  }
  //調度的"交互"
  scheduleInteractions(root, expirationTime, __interactionsRef.current);
}
複製代碼

__interactionsRef:

// Set of currently traced interactions.
// Interactions "stack"
// Meaning that newly traced interactions are appended to the previously active set.
// When an interaction goes out of scope, the previous set (if anyis restored.

 //設置當前跟蹤的interactions,也是interactions的棧
//它是一個集合
let interactionsRef: InteractionsRef = (nullany);
複製代碼

scheduleInteractions():

//與schedule的交互
function scheduleInteractions(root, expirationTime, interactions{
  if (!enableSchedulerTracing) {
    return;
  }
  //當interactions存在時
  if (interactions.size > 0) {
    //獲取FiberRoot的pendingInteractionMap屬性
    const pendingInteractionMap = root.pendingInteractionMap;
    //獲取pendingInteractions的expirationTime
    const pendingInteractions = pendingInteractionMap.get(expirationTime);
    //若是pendingInteractions不爲空的話
    if (pendingInteractions != null) {
      //遍歷並更新還未調度的同步任務的數量
      interactions.forEach(interaction => {
        if (!pendingInteractions.has(interaction)) {
          // Update the pending async work count for previously unscheduled interaction.
          interaction.__count++;
        }

        pendingInteractions.add(interaction);
      });
    }
    //不然初始化pendingInteractionMap
    //並統計當前調度中同步任務的數量
    else {
      pendingInteractionMap.set(expirationTime, new Set(interactions));

      // Update the pending async work count for the current interactions.
      interactions.forEach(interaction => {
        interaction.__count++;
      });
    }
    //計算並得出線程的id
    const subscriber = __subscriberRef.current;
    if (subscriber !== null) {
      //這個暫時不看了
      const threadID = computeThreadID(root, expirationTime);
      //檢測這些任務是否會報錯
      subscriber.onWorkScheduled(interactions, threadID);
    }
  }
}
複製代碼

subscriber.onWorkScheduled():

//利用線程去檢測同步的update,判斷它們是否會報錯
function onWorkScheduled(
  interactions: Set<Interaction>,
  threadID: number,
): void 
{
  let didCatchError = false;
  let caughtError = null;
  //遍歷去檢測
  subscribers.forEach(subscriber => {
    try {
      subscriber.onWorkScheduled(interactions, threadID);
    } catch (error) {
      if (!didCatchError) {
        didCatchError = true;
        caughtError = error;
      }
    }
  });

  if (didCatchError) {
    throw caughtError;
  }
}
複製代碼

解析:
利用FiberRootpendingInteractionMap屬性和不一樣的expirationTime,獲取每次schedule所需的update任務的集合,記錄它們的數量,並檢測這些任務是否會出錯。

7、renderRoot()
做用:
初始化root,並調用workLoop進行循環單元更新

這個咱們放在後面再講。


後言:
原本是寫到十一了,但發現還有一層套一層的function,所有放在一篇文章裏的話,太長了,容易消化不了,因此咱們先在這裏打住:

export function scheduleUpdateOnFiber(){
  xxx
  xxx
  xxx
  if (expirationTime === Sync) {
    if( 第一次render ){
      //跟蹤這些update,並計數、檢測它們是否會報錯
      schedulePendingInteractions(root, expirationTime);
      //初始化root,調用workLoop進行循環單元更新
      let callback = renderRoot(root, Sync, true);

    }else{
      下篇要講的內容。。。。
    }
  }

}
複製代碼

scheduleWork 結束後,我會像往常同樣,製做一張流程圖幫助你們梳理思路!

源碼請參考GitHub:
github.com/AttackXiaoJ…


(未完待續!)

相關文章
相關標籤/搜索