美文网首页前端开发那些事
React 源码解析 - React 创建更新回顾和 Reac

React 源码解析 - React 创建更新回顾和 Reac

作者: coolheadedY | 来源:发表于2019-02-23 23:23 被阅读43次

    回顾 React 更新

    创建更新

    • ReactDOM.render 初始渲染
      每次调用都通过传入的 <App />, getElementById('app') 构建 root 节点,每个 rootFiber 都有独立的 updateQueue 和 fiberTree,最后调用 ReactRoot.prototypye.render 来创建更新。

    • setState & forceUpdate 更新渲染
      都是 Component 构造函数的原型方法,目的都是给节点的 fiber 对象上创建更新,区别在于更新的类型不同。
      创建更新 update,记录当前时间,计算 expirationTime,设置当前更新的 payload,再把 update 推入 fiber 对象的 updateQueue 属性上, 之后进入调度流程。

    expirationTime

    由 ReactFiberReconciler.js 包,updateContainer 中的 const expirationTime = computeExpirationForFiber(currentTime, current); 计算出

    function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
      let expirationTime;
    // ...
        if (fiber.mode & ConcurrentMode) {
          if (isBatchingInteractiveUpdates) {
            // This is an interactive update 高优先级
            expirationTime = computeInteractiveExpiration(currentTime);
          } else {
            // This is an async update 低优先级
            expirationTime = computeAsyncExpiration(currentTime);
          }
        } else {
          // This is a sync update
          expirationTime = Sync;
        }
    }
    
    • 简化 computeExpirationForFiber 函数
    • 发现 expirationTime 正常情况是 Sync = 1 同步的
    • 只有在 fiber.mode 存在并且使用 ConcurrentMode 新版本的异步更新模式时才会真正的计算 expirationTime
    • ConcurrentMode 模式下还会根据 isBatchingInteractiveUpdates 全局变量判断当前更新的上下文环境来决定 expirationTime 是高优先级还是低优先级的运算结果。(isBatchingInteractiveUpdates 在 batchedUpdates 中讲解)

    scheduleWork 开始调度

    核心功能

    • 找到更新对应的 FiberRoot 节点
      setState 时传入的都是组件的 Fiber 节点而不是 FiberRoot 节点
    • 符合条件时 - 重置 stack
      具有公共变量,用于调度和更新
    • 符合条件时 - 请求工作调度

    回顾 FiberTree

    • FiberTree 属性
      1 child 为第一个子节点
      2 sibling 为兄弟节点
      3 return 为父节点,只有 RootFiber 对象 renturn 为 null
      4 FiberRoot.current 和 RootFiber.stateNode 互相引用

    • 执行操作时的 Fiber 对象
      1 点击组件上的元素
      2 执行组件的原型方法调用 setState
      3 把 RootFiber 加入到调度中

    scheduleWork 进入调度队列

    每一次进入调度队列的只有 FiberRoot 对象, 更新也是从 FiberRoot 对象上开始的。

    function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
      // 找到 root 更新 FiberTree 上的所有 expirationTime
      const root = scheduleWorkToRoot(fiber, expirationTime);
      if (root === null) { // 没有 FiberRoot 暂停
        return;
      }
    
      if (
        !isWorking && // 没有执行渲染
        nextRenderExpirationTime !== NoWork && // 任务是个异步的,执行到一半了,交还给浏览器执行
        expirationTime < nextRenderExpirationTime // 新的任务优先级高于现在的任务
      ) {
        // This is an interruption. (Used for performance tracking.)
        interruptedBy = fiber; // 记录
        resetStack(); // 优先执行高优先级任务
      }
      markPendingPriorityLevel(root, expirationTime);
      if (
        !isWorking || // 没有正在工作
        isCommitting || // 或者正在提交,也就是更新dom 树的渲染阶段
        nextRoot !== root // 不同的 root 一般不存在不同
      ) {
        const rootExpirationTime = root.expirationTime;
        requestWork(root, rootExpirationTime); // 请求工作
      }
      if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
        // Reset this back to zero so subsequent updates don't throw.
        nestedUpdateCount = 0;
        invariant( false, 超出最大更新深度。 当组件在componentWillUpdate或componentDidUpdate中重复调用setState时,可能会发生这种情况。 React限制嵌套更新的数量以防止无限循环。  );
      }
    }
    

    scheduleWorkToRoot 通过 Fiber 对象找到 RootFiber 对象进行调度

    • 根据传入的 Fiber 对象向上寻找到 RootFiber 对象
    • 同时更新所有子树上面的 expirationTime
    function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
      // ...
      if (  // 更新 fiber 对象上 expirationTime
        fiber.expirationTime === NoWork || // 没有任何更新操作的
        fiber.expirationTime > expirationTime // 有更新产生,但是优先级低于新计算的 expirationTime
      ) { // 设置成最新的 expirationTime
        fiber.expirationTime = expirationTime;
      }
      let alternate = fiber.alternate;
      if (
        alternate !== null &&
        (alternate.expirationTime === NoWork ||
          alternate.expirationTime > expirationTime)
      ) {
        // 逻辑和上面一样,更新 alternate 的expirationTime
        alternate.expirationTime = expirationTime;
      }
     // 通过 FiberTree 的属性向上寻找 FiberRoot 并更新每个子 fiber 对象的 expirationTime
      let node = fiber.return; // renturn 父节点,
      let root = null;
      // node === null 就是 FiberRoot 对象
      if (node === null && fiber.tag === HostRoot) {
        root = fiber.stateNode;
      } else {
        // 循环查找 FiberRoot
        while (node !== null) {
          alternate = node.alternate;
          if ( // 更新 expirationTime
            node.childExpirationTime === NoWork ||
            node.childExpirationTime > expirationTime
          ) {
            node.childExpirationTime = expirationTime;
            if (
              alternate !== null &&
              (alternate.childExpirationTime === NoWork ||
                alternate.childExpirationTime > expirationTime)
            ) {
              alternate.childExpirationTime = expirationTime;
            }
          } else if (
            alternate !== null &&
            (alternate.childExpirationTime === NoWork ||
              alternate.childExpirationTime > expirationTime)
          ) {
            alternate.childExpirationTime = expirationTime;
          }
          // 找到 FiberRoot 结束循环
          if (node.return === null && node.tag === HostRoot) {
            root = node.stateNode;
            break;
          }
          // 继续向父节点查找
          node = node.return;
        }
      }
    

    resetStack

    • 当发现当前任务的优先级大于下一个任务的优先级时,把下个任务的优先级重置执行当前任务
    • resetStack 在重置下个任务时,会先记录这个任务,等待以后执行,并且使用 unwindInterruptedWork 来重置这个任务 fiber 上级的状态
      if (
        !isWorking && // 没有执行渲染
        nextRenderExpirationTime !== NoWork && // 任务是个异步的,执行到一半了,交还给浏览器执行
        expirationTime < nextRenderExpirationTime // 新的任务优先级高于现在的任务
      ) {
        // This is an interruption. (Used for performance tracking.)
        interruptedBy = fiber; // 记录
        resetStack(); // 优先执行高优先级任务
      }
    
    function resetStack() {
      // nextUnitOfWork 被打断的任务
      if (nextUnitOfWork !== null) {
        // 记录,等待以后执行
        let interruptedWork = nextUnitOfWork.return;
        while (interruptedWork !== null) {
          // 退回任务
          unwindInterruptedWork(interruptedWork);
          interruptedWork = interruptedWork.return;
        }
      }
    
      // 变回初始值,进行新任务更新
      nextRoot = null;
      nextRenderExpirationTime = NoWork;
      nextLatestAbsoluteTimeoutMs = -1;
      nextRenderDidError = false;
      nextUnitOfWork = null;
    }
    

    何时执行 requestWork

    isWorking, isCommitting 是 react 渲染的两个不同阶段,

    • isWorking
      working 包含 committing(不可打断)
    • isCommitting
      fiberTree 的更新已经结束,正在提交也就是更新dom 树的渲染阶段, 不可打断
      if (
        !isWorking || // 没有正在工作
        isCommitting || // 或者正在提交,也就是更新dom 树的渲染阶段
        nextRoot !== root // 不同的 root 一般不存在不同
      ) {
        const rootExpirationTime = root.expirationTime; // 重新查找 root expirationTime,因为可能会改变
        requestWork(root, rootExpirationTime); // 请求工作
      }
    

    requestWork

    核心功能

    • 将 root 节点加入到 root调度队列中
    • 判断是否是批量更新
    • 最后根据 expirationTime 的类型判断调度的类型

    requestWork 流程

    function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
      addRootToSchedule(root, expirationTime); // 把当前 root设置为最高优先级
      // isRendering 调度已经在执行了, 循环已经开始了
      if (isRendering) {
        return;
      }
    
      // 批量处理相关
      // 调用 setState 时在 enqueueUpdates 前 batchedUpdates 会把 isBatchingUpdates 设置成 true
      if (isBatchingUpdates) {
        // Flush work at the end of the batch.
        if (isUnbatchingUpdates) {
          // ...unless we're inside unbatchedUpdates, in which case we should
          // flush it now.
          nextFlushedRoot = root;
          nextFlushedExpirationTime = Sync;
          performWorkOnRoot(root, Sync, true);
        }
        return; // isBatchingUpdates true // 普通的 setState 在进入 enqueueUpdates 时在这里直接不执行了,下面其实没进入调度
      }
    
      // TODO: Get rid of Sync and use current time?
      if (expirationTime === Sync) { // 同步的调用 js 代码
        performSyncWork();
      } else { // 异步调度 独立的 react 模块包,利用浏览器有空闲的时候进行执行,设置 deadline 在此之前执行
        scheduleCallbackWithExpirationTime(root, expirationTime); // 在 secheduler 文件夹下的单独模块
      }
    }
    

    addRootToSchedule

    • 判断当前 root 是否调度过, 单个或多个 root 构建成单向链表结构
    • 如果调度过,设置当前任务优先级最高
    function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
      // root.nextScheduledRoot 用来判断是否有异步任务正在调度, 为 null 时会增加 nextScheduledRoot
      // 这个 root 还没有进入过调度
      if (root.nextScheduledRoot === null) {
        root.expirationTime = expirationTime;
        // lastScheduledRoot firstScheduledRoot 是单向链表结构,表示多个 root 更新
        // 这里只有一个 root 只会在这里执行
        if (lastScheduledRoot === null) {
          firstScheduledRoot = lastScheduledRoot = root;
          root.nextScheduledRoot = root;
        } else { // 有个多个root 时进行单向链表的插入操作
          lastScheduledRoot.nextScheduledRoot = root;
          lastScheduledRoot = root;
          lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
        }
      } else { 
        // 传入的 root 已经进入过调度, 把 root 的优先级设置最高
        const remainingExpirationTime = root.expirationTime;
        // 如果 root 的 expirationTime 是同步或者优先级低,增加为计算出的最高优先级
        if (
          remainingExpirationTime === NoWork ||
          expirationTime < remainingExpirationTime
        ) {
          root.expirationTime = expirationTime; // 把当前 root 的优先级设置为当前优先级最高的
        }
      }
    }
    

    batchedUpdates 批量更新

    • 每次 react 创建更新都会执行 requestWork。如: setState
    • 在 requestWork 中决定 react 的更新是异步调度还是同步执行

    setState 的调用

    import React from 'react'
    import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'
    
    export default class BatchedDemo extends React.Component {
      state = {
        number: 0,
      }
    
      handleClick = () => {
        // 方法一
        // 事件处理函数自带`batchedUpdates`
        // this.countNumber() // 执行的结果是 0, 0, 0
    
        // 方法二
        // 主动`batchedUpdates`
       setTimeout(() => {
          this.countNumber() // 执行的结果是 1,2,3
        }, 0)
        // 方法三
        // setTimeout中没有`batchedUpdates`
        // setTimeout(() => {
        //   batchedUpdates(() => this.countNumber())  // 执行的结果是 0, 0, 0
        // }, 0)
    
      }
    
      countNumber() {
        const num = this.state.number
        this.setState({
          number: num + 1,
        })
        console.log(this.state.number)
        this.setState({
          number: num + 2,
        })
        console.log(this.state.number)
        this.setState({
          number: num + 3,
        })
        console.log(this.state.number)
      }
    
      render() {
        return <button onClick={this.handleClick}>Num: {this.state.number}</button>
      }
    }
    

    requestWork

    • 当 setState 创建更新后进入调度,执行到 requestWork 里时会判断一个 isBatchingUpdates 的全局变量。
    • 在 requestWork 中断点
    function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
      debugger
      // ...
      if (isRendering) {
        return;
      }
    
      // 批量处理相关
      // 调用 setState 时在 enqueueUpdates 前 batchedUpdates 会把 isBatchingUpdates 设置成 true
      if (isBatchingUpdates) {
        if (isUnbatchingUpdates) {
          nextFlushedRoot = root;
          nextFlushedExpirationTime = Sync;
          performWorkOnRoot(root, Sync, true);
        }
        return; // isBatchingUpdates true // 普通的 setState 在进入 enqueueUpdates 时在这里直接不执行了,下面其实没进入调度
      }
      // 只有异步模式任务时才会执行
      }
    
    • 在 requestWork 中断点,发现在判断 isBatchingUpdates 变量时就直接返回了,虽然 expirationTime 是 Sync 但是下面的 performSyncWork() 并不会执行。
    • setState 时先执行了一个 batchedUpdates 的函数。


    • 多次的 setState 在 enqueueUpdates 函数中,fiber 对象的 baseState 仍然是 0, 但是 fiber 对象上的 updateQueue 更新队列上已经记录好了多次 update 对象将要更新 state 的 payload。


    batchedUpdates 的源码

    • setState 在 batchedUpdates 中先把 isBatchingUpdates 暂存为 previousIsBatchingUpdates, 再设置为 true 防止在 requestWork 中执行。
    • 在 try 代码块中执行组件的方法 fn,fn 不论执行多少次 setState 执行完了都会通过 finally 进入把 isBatchingUpdates 再设置回 false。
    • 最后通过执行 performSyncWork() 方法,而不是在 requestWork 中调用。
    function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
      const previousIsBatchingUpdates = isBatchingUpdates; // 初始为 false
      isBatchingUpdates = true;
      try {
        return fn(a); // 执行组件绑定的方法, 走到 requestWork 里
      } finally {
        // setState 最终 enqueueUpdates 全部走到 requestWork 后变回 false 再一同 performSyncWork 才真正的执行并改变 state
        isBatchingUpdates = previousIsBatchingUpdates; // 变回 false
        // 如果是 setTimeout(() => { this.setState }) setTimeout 走到这里后才执行 this.setState 这时上下文环境是 window isBatchingUpdates 已经 false,setState 就是同步的
        if (!isBatchingUpdates && !isRendering) {
          performSyncWork(); // 当所有 setState 执行完全部enqueueUpdates 后代替 requestWork 来调度
        }
      }
    }
    

    方法二 setTimout 执行方式

     setTimeout(() => {
       batchedUpdates(() => this.countNumber())
     }, 0)
    
    • setTimeout 等浏览器 API 执行的方式结果都会把三次 setState 结算的结果打印出来,像是一种同步的执行方式
    • 再次 debugger ,这时在 batchedUpdates 函数中的 fn 的执行内容只是 setTimeout。
    • 当 setTimeout 执行完后直接进入了 finally 代码块中,isBatchingUpdates 变回了 false
    • 当 setTimeout 结束执行回调中的 setState 进入 requestWork 时 isBatchingUpdates 已经变为 false,requestWork 将会执行下去,最终执行自己 performSyncWork()
    • 三次 setState 都会通过 requestWork 执行 performSyncWork(),而不是之前通过 batchedUpdates 执行一次,所以每次 setState 的 update 都会立刻改变 state,结果也是同步的输出。

    方法三 使用 batchedUpdates API

    batchedUpdates 让 setState 的更新仍然为批量更新

     setTimeout(() => {
       batchedUpdates(() => this.countNumber())
     }, 0)
    
    • batchedUpdates API 其实就是 batchedUpdates 函数
    • setTimeout 执行回调时 batchedUpdates API 又把 isBatchingUpdates 设置为 true,让 多次的 setState 又能进行批量更新。
    function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
      const previousIsBatchingUpdates = isBatchingUpdates; // 初始为 false
      isBatchingUpdates = true;
      try {
        return fn(a); // 执行组件绑定的方法, 走到 requestWork 里
      } finally {
        isBatchingUpdates = previousIsBatchingUpdates; // 变回 false
        if (!isBatchingUpdates && !isRendering) {
          performSyncWork(); // 当所有 setState 执行完全部enqueueUpdates 后代替 requestWork 来调度
        }
      }
    }
    

    总结 setState 是同步还是异步

    • setState 本身的方法调用时同步的,但是调用 setState 不表示 state 立即更新的,state 的更新是根据我们执行环境的上下文来判断的。
    • 如果处于批量更新的情况下 state 就不是立即更新的,如果不处于批量更新情况下有可能立即更新.
    • 现在有 asyncMode 异步渲染的情况,state 也不是立即更新的,需要进入异步调度的过程。

    相关文章

      网友评论

        本文标题:React 源码解析 - React 创建更新回顾和 Reac

        本文链接:https://www.haomeiwen.com/subject/lbvzeqtx.html