专栏首页前端干货和生活感悟React源码解析之scheduleWork(上)

React源码解析之scheduleWork(上)

前言: 你需要知道:浅谈React 16中的Fiber机制(https://tech.youzan.com/react-fiber/)、React源码解析之RootFiberReact源码解析之FiberRoot

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

createUpdate后就进入scheduleWork流程,接下来我们就正式进入调度流程

一、scheduleUpdateOnFiber() 作用: 调度update任务

提示: scheduleWorkscheduleUpdateOnFiber

export const scheduleWork = scheduleUpdateOnFiber;

源码:

//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);
      }
    }
  }
}

解析: 有点长,我们慢慢看

二、checkForNestedUpdates() 作用: 判断是否是无限循环的update

源码:

// 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死循环

三、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

四、checkForInterruption() 作用: 判断是否有高优先级任务打断当前正在执行的任务

源码:

//判断是否有高优先级任务打断当前正在执行的任务
function checkForInterruption(
  fiberThatReceivedUpdate: Fiber,
  updateExpirationTime: ExpirationTime,
) {
  //如果任务正在执行,并且异步任务已经执行到一半了,
  //但是现在需要把执行权交给浏览器,去执行优先级更高的任务
  if (
    enableUserTimingAPI &&
    workInProgressRoot !== null &&
    updateExpirationTime > renderExpirationTime
  ) {
    //打断当前任务,优先执行新的update
    interruptedBy = fiberThatReceivedUpdate;
  }
}

解析: 如果当前fiber的优先级更高,需要打断当前执行的任务,立即执行该fiber上的update,则更新interruptedBy

五、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()


六、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 any) is restored.

 //设置当前跟踪的interactions,也是interactions的栈
//它是一个集合
let interactionsRef: InteractionsRef = (null: any);

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任务的集合,记录它们的数量,并检测这些任务是否会出错。

七、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: https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberWorkLoop.js


本文分享自微信公众号 - webchen(webchen1995)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 如何自己实现一个简单的webpack构建工具 【精读】

    webpack可以说是目前最火的打包工具,如果用不好他,真的不敢说自己是个合格的前端工程师

    Peter谭金杰
  • 一文带你了解2018年最流行的前端技术

    2018年即将过半,前端开发这个行业又进一个台阶了。找来一个现代前端技术图谱看看,真是吓尿了——宝宝心里苦啊!

    用户6167509
  • ES6的前世今生

    1996 年 11 月,Netscape 创造了javascript并将其提交给了标准化组织 ECMA,次年,ECMA 发布 262 号标准文件(ECMA-26...

    逆月翎
  • 掌握react,这一篇就够了

    react众所周知的前端3大主流框架之一,由于出色的性能,完善的周边设施风头一时无两。本文就带大家一起掌握react。

    frontoldman
  • 手把手教会使用react开发日历组件

    提前需要准备好react脚手架开发环境,由于react已经不支持在页面内部通过jsx.transform来转义,我们就自己大了个简易的开发环境

    frontoldman
  • redux原来如此简单

    redux是专门为react开发的,但并不是只能用于react,可以用于任何界面库。

    frontoldman
  • 新手学习 react 迷惑的点(一)

    网上各种言论说 React 上手比 Vue 难,可能难就难不能深刻理解 JSX,或者对 ES6 的一些特性理解得不够深刻,导致觉得有些点难以理解,然后说 Rea...

    桃翁
  • Axios是什么?用在什么场景?如何使用?

    Axios 是一个基于 promise 的 HTTP 库,简单的讲就是可以发送get、post请求。说到get、post,大家应该第一时间想到的就是Jquery...

    Javanx
  • 【Netty】浅谈Netty的线程模型

    说起netty的线程模型,首先我们应该能想到经典的Reactor线程模型,不同的NIO框架的实现方式尽管不同,但是本质上还是遵循了Reactor线程模型。

    周三不加班
  • 终于,北京的前端从业者开始坐不住了...

    然后我就问他,你目前在帝都月薪多少K?他还不好意思,说出来嫌丢人。我说没关系的,我目前在太原10K,这没什么可丢人的,勇敢地说出来吧~

    闰土大叔

扫码关注云+社区

领取腾讯云代金券