react时间分片实现流程详解

目录
  • 什么是时间分片
  • 为什么需要时间分片
  • 实现分片开启 - 固定
    • 为什么用performance.now()而不用Date.now()
  • 实现分片中断、重启 - 连续
    • 分片中断
    • 分片重启
  • 实现延迟执行 - 有间隔
    • 为什么选择宏任务实现异步执行
    • 时间分片异步执行方案的演进
  • 时间分片简单实现
  • 总结

我们常说的调度,可以分为两大模块,时间分片和优先级调度

  • 时间分片的异步渲染是优先级调度实现的前提
  • 优先级调度在异步渲染的基础上引入优先级机制控制任务的打断、替换。

本节将从时间分片的实现剖析react的异步渲染原理,阅读本文你讲可以了解

  • 时间分片是什么
  • 为什么需要时间分片
  • 时间分片在react中是如何运行的
  • 时间分片的极简实现

什么是时间分片

上文提到过,时间分片其实就是一个固定而连续且有间隔的时间区间

固定:时间分片是工作时长是固定的
连续:分片之间是连续的,当前分片内有工作没做完,会留到下个分片继续
有间隔:在进入下一个分片前,会有一定时间的间隔

这些解释比较抽象,可以更加通俗去理解

固定:每天固定工作8小时
连续:每天都要上班
有间隔:明天上班前会休息一段时间

为什么需要时间分片

我们知道,react最重要,也是最耗时的任务是节点遍历。

设想一个页面上有一万个DOM节点,如果我们用同步的方式一个个遍历完需要花费多少时间。而且如果是同步遍历的话,遍历的过程中,JS线程一直会霸占主线程,导致阻塞了浏览器的其他线程,导致卡顿的情况出现。

换个思路解决这个遍历问题,能不能遍历一会,休息一会,休息的过程中就可以把主线程交还给渲染线程和事件线程,这样就能及时渲染节点和响应用户事件,避免造成卡顿。

为了实现遍历一会,休息一会,我们可以将整个过程分解为以下三个步骤

  • 分片开启
  • 分片中断、分片重启
  • 延迟执行

这三个步骤与时间分片的三个特性一一对应

实现分片开启 - 固定

时间分片是独立于React的节点遍历流程的,所以只需要把节点遍历的入口函数以回调函数的形式传入即可,这样就可以让时间分片来决定节点遍历执行时机。

// 节点遍历的入口函数
function Reconcile协调() {
    节点遍历()
}
function Schedule调度() {
    创建分片(Reconcile协调)
}

第一步,需要将时间分片要调度的函数抽象为一个任务对象

function 创建分片(需要被调度的函数) {
    const 新的任务 = {
        callback: 需要被调度的函数
    }
}

第二步,设定分片工作时长,为了方便后续,可以直接计算过期时间。分片工作时长一般为5ms,但Scheduler会根据任务优先级有所调整,这里为了更好理解,先默认5ms

const taskQueue = []
function 创建分片(需要被调度的函数) {
    const 新的任务 = {
        callback: 需要被调度的函数,
        expirationTime: performance.now() + 5000
    }
    taskQueue.push(新的任务)
    发起异步调度()
}

每次分片的创建其实都是新一轮调度的开始,所以在末尾会发起异步调度

为什么用performance.now()而不用Date.now()

performance.now()返回当前页面的停留时间,Date.now()返回当前系统时间。但不同的是performance.now()精度更高,且比Date.now()更可靠

  • performance.now()返回的是微秒级的,Date.now()只是毫秒级
  • performance.now()一个恒定的速率慢慢增加的,它不会受到系统时间的影响。Date.now()受到系统时间影响,系统时间修改Date.now()也会改变

实现分片中断、重启 - 连续

分片中断

我们在第一章已经将React的虚拟DOM结构从树形结构优化成链表结构,所以能轻松使用while循环实现可中断的遍历

那么如果要将遍历任务时间分片相结合,且实现分片中断功能的话,只需要在while循环出加入分片时间过期的校验即可

function 分片过期校验() {
    return (perfromance.now() - 分片开启时间) >= 5000
}
let 需要被遍历的幸运儿节点 = null
function 构建节点() {
    /** * ...在这里进行节点构建工作 */
    需要被遍历的幸运儿节点 = 需要被遍历的幸运儿节点.next
}
function 节点遍历() {
    while (需要被遍历的幸运儿节点 != null && !分片过期校验()) {
        构建节点()
    }
}
function Schedule调度() {
    创建分片(Reconcile协调)
}

分片重启

分片重启意思就是上一轮时间分片因为过期中断了,需要重新发起一轮时间分片。

实现的思路是,在上一轮分片结束之后判断是否还需要开启下一轮分片,需要的话则重新发起一轮异步调度即可,相关参考视频讲解:进入学习

function 分片过期校验() {
    return (perfromance.now() - 分片开启时间) >= 5000
}
function 分片事件循环() {
    let 栈顶任务 = taskQueue.peek()

    while (栈顶任务) {
        if (分片过期校验()) break
        const 栈顶任务回调 = 栈顶任务.callback()
        if (typeof 栈顶任务回调 == 'function') {
            // 当前任务还没有执行完,继续搞
            栈顶任务.callback = 栈顶任务回调
        } else {
            // 当前任务已执行完,弹出队列
            taskQueue.pop()
        }
        栈顶任务 = taskQueue.peek()
    }
    // 还有任务哦
    if (栈顶任务) return true
    return false
}
function 分片执行() {
    分片开启时间 = performance.now()
    var 是否还有任务未执行完毕
    try {
        是否还有任务未执行完毕 = 分片事件循环()
    } finally {
        // 分片重启
        if (是否还有任务未执行) 发起异步调度()
    }
}
function 发起异步调度() {
    // 这里实际上是异步执行,看下面有间隔
    分片执行()
}

重启的条件就是判断分片任务队列中是否还有任务,有的话就发起下一轮的时间分片

实现延迟执行 - 有间隔

有间隔的本质是延迟JS的执行,让浏览器有喘息的时间,去处理其他线程的任务,哪如何把主线程控制权交还给浏览器呢??

可以使用异步特性发起下一轮时间分片,实现延迟执行

function 发起异步调度() {
    // 将主线程短暂的交还给浏览器
    setTimeout(() => {
        分片执行()
    }, 0)
}

为什么选择宏任务实现异步执行

微任务无法真正达到交还主线程控制权的要求。

因为一轮事件循环,是先执行一个宏任务,然后再清空微任务队列里面的任务,如果在清空微任务队列的过程中,依然有新任务插入到微任务队列中的话,还是把这些任务执行完毕才会释放主线程。所以微任务不合适。

时间分片异步执行方案的演进

为什么不是setTimeout

因为setTimeout的递归层级过深的话,延迟就不是1ms,而是4ms,这样会造成延迟时间过长

为什么不是requestAnimationFrame

requestAnimationFramed是在微任务执行完之后,浏览器重排重绘之前执行,执行的时机是不准确的。如果raf之前JS的执行时间过长,依然会造成延迟

为什么不是requestIdleCallback

requestIdleCallback的执行时机是在浏览器重排重绘之后,也就是浏览器的空闲时间执行。其实执行的时机依然是不准确的,raf执行的JS代码耗时可能会过长

为什么是 MessageChannel

MessageChannel的执行时机比setTimeout靠前

在React中,异步执行优先使用setImmediate,其次是MessageChannel,最后是setTimeout,都是根据浏览器对这些的特性支持程度决定的。

时间分片简单实现

下面会整合上面的所有代码,模拟出最简单的时间分片实现(不包含优先级机制)

Scheduler.js

const taskQueue = []
let 分片开启时间 = -1
// **时间分片核心**
const 分片过期校验 = () => {
    return (perfromance.now() - 分片开启时间) >= 5000
}
function 分片事件循环() {
    let 栈顶任务 = taskQueue.peek()
    while (栈顶任务) {
        // 每执行完一个任务,都要校验一下分片是否过期
        if (分片过期校验()) break
        const 栈顶任务回调 = 栈顶任务.callback()
        if (typeof 栈顶任务回调 == 'function') {
            // 当前任务还没有执行完,继续搞
            栈顶任务.callback = 栈顶任务回调
        } else {
            // 当前任务已执行完,弹出队列
            taskQueue.pop()
        }
        栈顶任务 = taskQueue.peek()
    }
    // 还有任务哦
    if (栈顶任务) return true
    return false
}
function 分片执行() {
    分片开启时间 = performance.now()
    var 是否还有任务未执行完毕
    try {
        是否还有任务未执行完毕 = 分片事件循环()
    } finally {
        // **时间分片核心:分片重启**
        if (是否还有任务未执行) 发起异步调度()
    }
}
// 实例化 MessageChannel
const channel = new MessageChannel()
const port2 = channel.port2
channel.port1.onmessage = 分片执行
function 发起异步调度() {
    // 向通道1发消息,通道1收到消息就会执行分片任务
    // **时间分片核心:延迟执行**
    port2.postMessage(null)
}
function 创建分片(需要被调度的函数) {
    // **时间分片核心:分片开启**
    const 新的任务 = {
        callback: 需要被调度的函数,
        expirationTime: performance.now() + 5000
    }
    taskQueue.push(新的任务)
    发起异步调度()
}
export default {
    创建分片,
    分片过期校验
}

ReactDOM.js

import * as Scheduler from './Scheduler'
const {
    创建分片,
    分片过期校验
} = Scheduler
let 需要被遍历的幸运儿节点 = null
function 构建节点() {
    /** * ...在这里进行节点构建工作 */
    需要被遍历的幸运儿节点 = 需要被遍历的幸运儿节点.next
}
function 节点遍历() {
    // **时间分片核心:分片中断**
    while (需要被遍历的幸运儿节点 != null && !分片过期校验()) {
        构建节点()
    }
}
function Schedule调度() {
    创建分片(Reconcile协调)
}
function 调度入口() {
    需要被遍历的幸运儿节点 = react应用根节点
    Schedule调度()
}
调度入口()

这段时间分片的伪代码相对于react中源码的实现,少了很多逻辑判断,并且集中了起来,应该会相对好理解很多。

如果还是觉得有点晦涩,可以重点关注伪代码中标有时间分片核心注释的代码,结合上文提到的概念理解

总结

读完这篇文章估计你可能对时间分片的概念已经有所有了解了,是不是觉得react16的新特性之一时间分片,也并没有想象中的神秘。

总的下来,时间分片就是由简单的三个模块组成:

  • 分片开启
  • 分片中断、重启
  • 延迟执行

时间分片是Scheduler调度器两大特性中的一个,另一个是任务的优先级调度,接下来可能会花两到三篇的篇幅去讲解。在源码阅读的过程中,我觉得时间分片的实现已经非常惊艳了,没想到后面优先级调度的设计对我更是无可匹敌的冲击。

到此这篇关于react时间分片实现流程详解的文章就介绍到这了,更多相关react时间分片内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • React+Node实现大文件分片上传、断点续传秒传思路

    目录 1.整体思路 2.实现步骤 2.1 文件切片加密 2.2 查询上传文件状态 2.3 秒传 2.4 上传分片.断点续传 2.5 合成分片还原完整文件 3.总结 4.后续扩展与思考 5.源码 1.整体思路 将文件切成多个小的文件: 将切片并行上传: 所有切片上传完成后,服务器端进行切片合成: 当分片上传失败,可以在重新上传时进行判断,只上传上次失败的部分实现断点续传: 当切片合成为完整的文件,通知客户端上传成功: 已经传到服务器的完整文件,则不需要重新上传到服务器,实现秒传功能: 2.实现步骤

  • react时间分片实现流程详解

    目录 什么是时间分片 为什么需要时间分片 实现分片开启 - 固定 为什么用performance.now()而不用Date.now() 实现分片中断.重启 - 连续 分片中断 分片重启 实现延迟执行 - 有间隔 为什么选择宏任务实现异步执行 时间分片异步执行方案的演进 时间分片简单实现 总结 我们常说的调度,可以分为两大模块,时间分片和优先级调度 时间分片的异步渲染是优先级调度实现的前提 优先级调度在异步渲染的基础上引入优先级机制控制任务的打断.替换. 本节将从时间分片的实现剖析react的异步

  • react电商商品列表的实现流程详解

    目录 整体页面效果 项目技术点 拦截器的配置 主页面 添加商品 分页与搜索 修改商品 删除商品 完整代码 整体页面效果 项目技术点 antd组件库,@ant-design/icons antd的图标库 axios 接口请求,拦截器配置 node-sass sass-loader css样式的一个嵌套 react-router-dom react路由使用 react-redux redux hooks:大多数我们用的是函数组件,函数组件没有state属性,所以我们使用hooks来初始化数据,并且函

  • react组件的创建与更新实现流程详解

    目录 React源码执行流程图 legacyRenderSubtreeIntoContainer legacyCreateRootFromDOMContainer createLegacyRoot ReactDOMBlockingRoot createRootImpl createContainer createFiberRoot createHostRootFiber createFiber updateContainer 总结 这一章节就来讲讲ReactDOM.render()方法的内部实现

  • React实现卡片拖拽效果流程详解

    前提摘要: 学习宋一玮 React 新版本 + 函数组件 &Hooks 优先 开篇就是函数组件+Hooks 实现的效果如下: 学到第11篇了 照葫芦画瓢,不过老师在讲解的过程中没有考虑拖拽目标项边界问题,我稍微处理了下这样就实现拖拽流畅了 下面就是主要的代码了,实现拖拽(src/App.js): 核心在于标记当前项,来源项,目标项,并且在拖拽完成时对数据处理,更新每一组数据(useState): /** @jsxImportSource @emotion/react */ // 上面代码是使用e

  • React Redux使用配置详解

    目录 前言 redux三大原则 redux执行流程 redux具体使用 执行流程 redux使用流程 前言 在使用redux之前,首先了解一下redux到底是什么? 用过vue的肯定知道vuex,vuex是vue中全局状态管理工具,主要是用于解决各个组件和页面之间数据共享问题,对数据采用集中式管理,而且可以通过插件实现数据持久化 redux跟vuex类似,最主要的就是用作状态的管理,redux用一个单独的常量状态state来保存整个应用的状态,可以把它想象成数据库,用来保存项目应用中的公共数据

  • React Redux应用示例详解

    目录 一 React-Redux的应用 1.学习文档 2.Redux的需求 3.什么是Redux 4.什么情况下需要使用redux 二.最新React-Redux 的流程 安装Redux Toolkit 创建一个 React Redux 应用 基础示例 Redux Toolkit 示例 三.使用教程 安装Redux Toolkit和React-Redux​ 创建 Redux Store​ 为React提供Redux Store​ 创建Redux State Slice​ 将Slice Reduc

  • Redis Sentinel服务配置流程(详解)

    1.Redis Sentinel服务配置 1.1简介 Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务: 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常. 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过API 向管理员或者其他应用程序发送通知. 自动故障迁移(Automatic failover): 当一个主服务器不

  • 浅谈Python生成器generator之next和send的运行流程(详解)

    对于普通的生成器,第一个next调用,相当于启动生成器,会从生成器函数的第一行代码开始执行,直到第一次执行完yield语句(第4行)后,跳出生成器函数. 然后第二个next调用,进入生成器函数后,从yield语句的下一句语句(第5行)开始执行,然后重新运行到yield语句,执行后,跳出生成器函数,后面再次调用next,依次类推. 下面是一个列子: def consumer(): r = 'here' for i in xrange(3): yield r r = '200 OK'+ str(i)

  • java存储以及java对象创建的流程(详解)

    java存储: 1)寄存器:这是最快的存储区,位于处理器的内部.但是寄存器的数量有限,所以寄存器根据需求进行分配.我们不能直接进行操作. 2)堆栈:位于通用RAM中,可以通过堆栈指针从处理器那里获取直接支持.堆栈指针往下移动,则分配新的内存.网上移动,则释放内存.但是 在创建程序的时候必须知道存储在堆栈中的所有项的具体生命周期,以便上下的移动指针.一般存储基本类型和java对象引用. 3)堆:位于通用RAM中,存放所有的java对象,不需要知道具体的生命周期. 4)常量存储:常量值通常直接存放在

  • Android Bluetooth蓝牙技术使用流程详解

    在上篇文章给大家介绍了Android Bluetooth蓝牙技术初体验相关内容,感兴趣的朋友可以点击了解详情. 一:蓝牙设备之间的通信主要包括了四个步骤 设置蓝牙设备 寻找局域网内可能或者匹配的设备 连接设备 设备之间的数据传输 二:具体编程实现 1. 启动蓝牙功能 首先通过调用静态方法getDefaultAdapter()获取蓝牙适配器BluetoothAdapter,如果返回为空,则无法继续执行了.例如: BluetoothAdapter mBluetoothAdapter = Blueto

随机推荐