万字技术分享——React Schedule 是怎么工作的

前言

这篇文章整整拖延了一年时间,终于把它更新上来了,看了无数个关于React源码阅读的视频,看了很多个技术的帖子,目前总结出更加容易理解的熟悉掌握住整个 React 流程的讲解文章,目的是让你在脑海中至少掌握清楚 React的流程的。如果存在不合理的地方欢迎在评论区留言。

本文主要介绍的是,React 创建更新以后 进入调度流程,整体完整的介绍,React 的调度与工作流程是其核心逻辑的重要组成部分。自从 React 16(Fiber 架构)后,这部分逻辑变得尤为关键,因为 React 引入了时间切片和优先级概念,允许渲染工作被打断和重新调度。

判断 Expiration Time 是否同步,同步的话执行 performSyncWork,否则执行scheduleCallbackWithExpirationTime。 这两步逻辑,对于后续的更新差异是很大的。 同步更新的话,需要马上更新到 DOM Tree 上面,异步的话说明它的优先级不是特别高,任务是可以被打断的,那么它就会进入调度的流程。具体的 Expiration Time 计算流程可以 查看往期的文章

以下是 React Scheduler 和 Work 的大致流程,以及部分相关代码片段(这只是一个简化和概览,实际源代码结构和内容更为复杂)

整体流程图

整体的流程

setState & useState

  1. 触发更新 :通常情况下,调用 setStateuseState 的 setter 会触发更新。这里就不进行详细的描述了,这个点经常在我们的开发中出现
js 复制代码
function useState(initialValue) {
  // ... 省略部分代码
  const dispatch = (action) => {
    // 更新 state 和 queue
    // ...
    scheduleWork(fiber, expirationTime);  // 调度工作
  };
  // ...
}

scheduleWork

  1. 开始调度scheduleWork 是调度更新的入口。
js 复制代码
function scheduleWork(fiber, expirationTime) {
    // 更新 root 上的 expirationTime 
    const root = scheduleWorkToRoot(fiber, expirationTime);  // 详细可以看下面的代码内容
    if (!isWorking && !isCommitting) { 
        // 如果当前没有正在执行的工作,请求一个新的工作调度 
        requestWork(root, root.expirationTime); 
    }
}

scheduleWorkToRoot

调度工作时如何找到与给定 Fiber 对应的 Root Fiber,在调度更新时,React 会从当前 Fiber 向上遍历其父级,直到找到 root。

js 复制代码
function scheduleWorkToRoot(fiber, expirationTime) {
    // 首先,我们将更新标记到当前 fiber 及其父级上
    let node = fiber;
    while (node !== null) {
        // 更新该 fiber 的 expiration time
        if (node.expirationTime < expirationTime) {
            node.expirationTime = expirationTime;
        }

        // 如果 node 是一个 Root Fiber,那么我们找到了它并返回
        if (node.tag === HostRoot) {
            return node;
        }

        node = node.return;
    }
    return null;
}

上面是简化版,具体目的就是找 root。

requestWork

  1. 在 React 的更新机制中,requestWork 函数负责根据给定的优先级请求新的工作。这一过程涉及到 React 的任务调度,尤其在 React 16(Fiber 架构)之后,这一调度机制使 React 能够更有效地利用浏览器的资源,确保高优先级的工作得到及时处理,同时在浏览器空闲时处理低优先级的工作。

以下是一个简化和概括的 requestWork 函数及相关代码:

js 复制代码
// 根据 root 和 expirationTime 来请求工作
function requestWork(root, expirationTime) {
    // 添加到待处理的 roots 列表中
    addRootToSchedule(root, expirationTime);

    if (isRendering) {
        // 如果当前正在渲染,我们只是添加 root 到列表并返回
        return;
    }

    if (isBatchingUpdates) {
        // 如果正在批处理更新,等待直到批处理结束
        if (isUnbatchingUpdates) {
            // 如果在一个 unbatched 更新块中,继续执行工作
            performWorkOnRoot(root, expirationTime);
        }
        return;
    }

    // 根据 expirationTime 请求实际的浏览器调度
    requestHostCallback(() => {
        performWorkUntilDeadline(); // 当浏览器回调发生时,开始执行工作
    });
}

// 当浏览器回调被调用时,执行此函数
function performWorkUntilDeadline() {
    // ... 一些检查和设置

    // 进行实际的工作
    while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork) {
        performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
        // ... 更新 nextFlushedRoot 和 nextFlushedExpirationTime
    }

    // ... 判断是否还有更多工作要做,并可能再次请求 host callback
}

// 请求浏览器调度的实际方法。在 React 中,这通常使用 requestIdleCallback 或 setTimeout 实现,
// 取决于浏览器支持和任务优先级。
function requestHostCallback(callback) {
    // ... 使用 requestIdleCallback 或 setTimeout 来调度 callback
}

注意:上述代码是简化和概括的版本,真正的 React 源代码中 requestWork 以及相关函数包含了大量的细节和优化策略。此外,React 还涉及到与浏览器的集成,例如通过 requestIdleCallback 来获取浏览器的空闲时间,这使得 React 可以在这些空闲时段中处理低优先级的工作。

performWorkOnRoot

  1. 执行工作performWorkOnRoot 是真正开始工作的地方,这里会处理更新并执行渲染。

performWorkOnRoot 是 React 内部调度机制的核心部分之一,它是开始对某个 root 进行实际工作的地方。根据给定的 expirationTime,它会决定何时开始工作、何时提交工作,以及如何利用浏览器的空闲时间。

js 复制代码
function performWorkOnRoot(root, expirationTime) {
    // 记录当前正在处理的 root 和 expirationTime
    currentlyProcessingRoot = root;
    renderExpirationTime = expirationTime;

    let finishedWork = root.finishedWork;

    if (finishedWork !== null) {
        // 如果已经有完成的工作,直接进行提交
        completeRoot(root, finishedWork, expirationTime);
    } else {
        // 从 root 的 current fiber 开始工作
        workInProgress = createWorkInProgress(root.current, null);
        
        // 开始进入渲染的工作循环
        renderRoot();

        if (workInProgressRootExitStatus === Finished) {
            // 如果工作已完成,将 finishedWork 指向完成的工作,然后提交
            root.finishedWork = workInProgress;
            completeRoot(root, workInProgress, expirationTime);
        }
    }
}

function renderRoot() {
    let work = workInProgress;

    do {
        // 执行单个 fiber 的更新工作
        const result = performUnitOfWork(work);
        if (result !== null) {
            // 如果返回了下一个要处理的 fiber,继续执行
            work = result;
        } else {
            // 如果当前 fiber 的子任务已经完成,进入完成阶段
            const returnFiber = work.return;
            const siblingFiber = work.sibling;

            // 为父 fiber 完成工作
            completeUnitOfWork(work);

            if (siblingFiber !== null) {
                // 如果有同级的 fiber,处理它
                work = siblingFiber;
            } else if (returnFiber !== null) {
                // 如果有父 fiber,返回并处理它
                work = returnFiber;
            } else {
                // 如果没有要处理的 fiber,退出循环
                workInProgressRootExitStatus = Finished;
                return;
            }
        }
    } while (work !== null && shouldYieldToRenderer()); // shouldYieldToRenderer 判断是否应当暂停工作,让浏览器执行其他任务
}

function completeRoot(root, finishedWork, expirationTime) {
    // 在这里,React 会执行真正的 DOM 更新操作,应用所有的 side effects
    commitRoot(root, finishedWork);
}

这只是 performWorkOnRoot 的一个概览。在实际的 React 源代码中,该函数会涉及大量的优化和其他功能,如错误边界处理、Suspense、并发模式、事件处理等。

workLoop

  1. 工作循环:在执行工作时,React 会使用一个工作循环,逐个处理 Fiber 节点。

在 React 的 Fiber 架构中,工作循环 (work loop) 是核心的一部分。它决定何时执行某个 Fiber 对象的渲染工作,何时暂停来让浏览器执行其他高优先级任务,以及何时继续或完成工作。

js 复制代码
let nextUnitOfWork = null;

function workLoop(deadline) {
    let shouldYield = false;

    // 如果有下一个工作单元并且当前不需要让出控制权给浏览器,继续工作
    while (nextUnitOfWork !== null && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        shouldYield = deadline.timeRemaining() < 1;
    }

    if (nextUnitOfWork === null) {
        // 如果没有更多工作要做,可能提交工作
        commitAllWork();
    } else {
        // 如果还有更多工作要做,再次请求浏览器的空闲回调
        requestIdleCallback(workLoop);
    }
}

function performUnitOfWork(fiber) {
    // 开始该 fiber 的工作。这包括渲染组件或更新 DOM。
    beginWork(fiber);

    if (fiber.child) {
        // 如果 fiber 有子项,返回它,因为我们首先处理子项
        return fiber.child;
    }

    let nextFiber = fiber;
    while (nextFiber) {
        // 当子项完成时,我们"完成"每个父项
        completeUnitOfWork(nextFiber);
        if (nextFiber.sibling) {
            // 如果有同级的 fiber,返回并处理它
            return nextFiber.sibling;
        }
        // 否则,我们继续完成父 fiber,并往上查找
        nextFiber = nextFiber.return;
    }

    return null;
}

requestIdleCallback(workLoop);
  • 工作循环 (workLoop) : 这是整个过程的驱动力。它在浏览器的空闲时间进行,确保用户交互等高优先级任务不会被阻塞。
  • 单元工作 (performUnitOfWork) : 这是每个 Fiber 对象的实际工作。对于组件,这可能意味着运行组件的 render 方法,而对于 DOM 节点,这可能意味着实际更新 DOM。
  • 开始工作 (beginWork)完成工作 (completeUnitOfWork) : 这些表示 fiber 的两个阶段。在开始阶段,组件会被渲染。在完成阶段,side-effects 会被收集并预备提交。

上面只是简单的简化版,详细需要看 Github 代码,深入了解过程

commitRoot

  1. 提交阶段:完成所有工作后,进入提交阶段,此时会应用所有的 side effects 到 DOM。

commitRoot 是 React Fiber 架构中一个非常重要的部分,它负责将已完成的工作应用到 DOM 中。在此阶段,React 将执行 side-effects(例如真正的 DOM 更新、生命周期方法调用等)。

javascript 复制代码
function commitRoot(root, finishedWork) {
    // 1. 准备提交前的操作,如调用 getSnapshotBeforeUpdate
    prepareForCommit(root.containerInfo);

    // 2. 递归整个 effect list(包含所有带有 side-effect 的 fiber)
    let nextEffect = finishedWork.firstEffect;
    while (nextEffect !== null) {
        // 根据 fiber 的 effect tag,执行相应的 side-effect
        commitWork(nextEffect);
        nextEffect = nextEffect.nextEffect;
    }

    // 3. 提交后的操作,如调用 componentDidUpdate 和 componentDidMount
    completeAfterCommit(root);

    // 4. 重置 nextEffect 和 effect list
    finishedWork.firstEffect = finishedWork.lastEffect = null;
}

function commitWork(fiber) {
    switch (fiber.effectTag) {
        case PLACEMENT:
            // 将新的节点插入 DOM
            break;
        case UPDATE:
            // 更新 DOM 节点
            break;
        case DELETION:
            // 从 DOM 中删除节点
            break;
        // ... 其他 effect tags 如 CALLBACK, REF, SNAPSHOT 等
    }
    // 清除 effect tag,表示 side-effects 已被处理
    fiber.effectTag = NoEffect;
}

function prepareForCommit(containerInfo) {
    // 在这里,React 会执行一些提交前的操作。
    // 例如,它可能会调用类组件的 getSnapshotBeforeUpdate 生命周期方法。
}

function completeAfterCommit(root) {
    // 在这里,React 会执行提交后的操作。
    // 例如,它可能会调用 componentDidMount 和 componentDidUpdate 生命周期方法。
}

cleanUp

清理工作:完成所有的更新后,React 会清理所有不再需要的内容,如解除事件监听器、取消异步更新等。

当 React 完成更新后,它会进入提交阶段,其中一部分工作是清理旧的、不再需要的内容。这主要涉及到移除不再需要的 DOM 节点、取消未完成的工作、释放引用以便于垃圾收集等。

最明显的清理工作是处理带有 DELETION effect tag 的 Fiber 对象。当某个组件被判定为不再需要时(例如,由于父组件的重新渲染导致它不再在输出中),该组件的 Fiber 将被标记为 DELETION

js 复制代码
function commitDeletion(fiber) {
    // 移除该 fiber 及其子树中的节点
    unmountFiberRecursively(fiber);
    detachFiber(fiber);
}

function unmountFiberRecursively(fiber) {
    // 1. 如果 fiber 有子组件,递归删除
    let child = fiber.child;
    while (child) {
        unmountFiberRecursively(child);
        child = child.sibling;
    }

    // 2. 对当前 fiber 执行卸载操作
    unmountFiber(fiber);
}

function unmountFiber(fiber) {
    // 清理具体的工作,如移除 DOM 节点、调用 componentWillUnmount 生命周期方法等
    switch (fiber.tag) {
        case HostComponent: // DOM 节点
            commitNestedUnmounts(fiber.stateNode);
            return;
        case ClassComponent:
            // 如果是类组件,可能需要调用 componentWillUnmount
            if (typeof fiber.type.prototype.componentWillUnmount === 'function') {
                fiber.type.prototype.componentWillUnmount.call(fiber.stateNode);
            }
            return;
        // ...处理其他类型的 fiber
    }
}

function detachFiber(fiber) {
    // 这里会释放 fiber 对象的引用,使得它能够被垃圾收集
    fiber.alternate = null;
    fiber.child = null;
    fiber.dependencies = null;
    fiber.memoizedProps = null;
    fiber.memoizedState = null;
    fiber.pendingProps = null;
    fiber.return = null;
    fiber.sibling = null;
    fiber.stateNode = null;
    fiber.updateQueue = null;
}

总结

React 的调度机制(Scheduler)是其并发模式的核心,它决定何时开始或中断工作,以确保在保持应用响应的同时高效地进行更新。这个概述是基于 React 16+ 中的 Fiber 架构,特别是它在并发模式中的工作方式。

requestWork,performWorkOnRoot,workLoop 步骤 是 React Scheduler 的核心部分。通过在浏览器的空闲时间进行工作,React 确保了应用保持响应,并在需要的时候可以中断和重新开始工作。

如果我在上述的内容存在问题欢迎在评论区留下你宝贵的建议,希望这个对你有帮助,互相学习进步。谢谢大家

文章参考:

github.com/facebook/re...

相关推荐
Redstone Monstrosity15 分钟前
字节二面
前端·面试
东方翱翔22 分钟前
CSS的三种基本选择器
前端·css
Fan_web1 小时前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝1 小时前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
Hellc0071 小时前
MacOS升级ruby版本
前端·macos·ruby
前端西瓜哥1 小时前
贝塞尔曲线算法:求贝塞尔曲线和直线的交点
前端·算法
又写了一天BUG1 小时前
npm install安装缓慢及npm更换源
前端·npm·node.js
cc蒲公英2 小时前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel