图片做视频在线观看网站,贵阳网站建设在线,网站设计导航栏怎么做,网站建设都需要什么文案文章目录 背景时间切片原理requestIderCallback 方法setImmediateMessageChannelsetTimeout React 18 时间切片源码手撸时间切片问题拆解构建任务队列宏任务包装首次开启任务递归任务执行workLoop 开启工作循环demo 模拟 总结 背景 前文学习了 fiber 架构和双缓存技术#xff… 文章目录 背景时间切片原理requestIderCallback 方法setImmediateMessageChannelsetTimeout React 18 时间切片源码手撸时间切片问题拆解构建任务队列宏任务包装首次开启任务递归任务执行workLoop 开启工作循环demo 模拟 总结 背景 前文学习了 fiber 架构和双缓存技术接下来我们深入源码一起学习下时间切片的原理。 React 探秘(一)fiber 架构
React 探秘(二)双缓存技术
React 时间切片是 React 通过将任务分割成小的时间片然后分批次去处理任务在 js 线程繁忙的时候把控制权交还给浏览器本身如渲染进程等以提高应用程序性能的一种技术。本文将介绍 React v18.3.1 时间切片并提供一个简单的 demo以便开发者学习相关知识。
时间切片的主要优点 提高应用程序的响应性和流畅度分批次运行任务可以避免长时间占用 CPU。更好地控制渲染过程让用户可以快速看到应用程序的变化避免白屏等问题。
时间切片技术位置 fiber 架构的 Scheduler 调度器层。 Scheduler 分为两大部分
时间切片 异步渲染是优先级调度实现的前提优先级调度在异步渲染的基础上引入优先级机制控制任务的打断、替换。
本文只介绍时间切片相关内容
时间切片原理
时间切片的原理就是把我们一次性执行完的任务切分到不同时间间隔去完成如果超出这个时间间隔就会暂时挂起交给浏览器等到空闲了继续执行。那么问题就转化为如何实现给任务添加时间间隔
这里涉及到 js 事件循环机制同步代码宏任务-微任务-宏任务。
执行全局代码当 JavaScript 代码第一次运行时首先会执行同步代码相当于一次宏任务如果遇到微任务会把微任务方微任务队列遇到宏任务放入宏任务队列检查微任务队列一旦同步代码宏任务完成事件循环会检查并执行微任务队列中的所有任务直到队列为空。执行下一个宏任务如果微任务队列为空事件循环会从宏任务队列中取出下一个任务并执行。重复上述步骤这个过程会不断循环直到所有任务执行完毕。
宏任务会在下次事件循环中执行不会阻塞本次页面渲染更新。
微任务「微任务是在本次页面更新前会全部执行」这一点与同步执行无异不会让出主线程。
常见的宏任务方法有
setTimeoutmessageChannelsetImmediate
此外还有 requestIdleCallback 是在浏览器渲染后有空闲时间时执行。
requestIderCallback 方法
window.requestIdleCallback() 方法插入一个函数这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作而不会影响延迟关键事件如动画和输入响应。函数一般会按先进先调用的顺序执行然而如果回调函数指定了执行超时时间 timeout则有可能为了在超时前执行函数而打乱执行顺序。
通过这个函数我们其实就可以时间一个简单的时间切片
function workLoop(deadline) {let shouldYield false;// 存在fiber并且时间空闲while (nextUnitOfWork !shouldYield) {nextUnitOfWork performUnitOfWork(nextUnitOfWork);shouldYield deadline.timeRemaining() 1; // 剩余时间是否小于1ms 代表任务繁忙}// 没有fiber并且wip存在if (!nextUnitOfWork workInProgress) {commitRoot();}// 繁忙时继续执行主任务requestIdleCallback(workLoop);
}我们执行某个 fiberNode 的时候浏览器主线程被占用这个时候就可以暂停 fiberNode 的继续执行等浏览器空闲时继续 nextUnitOfWork。这就实现了可暂停可继续。但是呢这 api 有限制
requestIdleCallback 的执行时机不是完全可控的这可能导致在不同环境中表现不一致。requestIdleCallback 是利用帧之间空闲时间来执行 js它是一个低优先级的处理策略但实际上 fiber 的处理上并不算是一个低优先级任务。
setImmediate
setImmediate 这个是最早执行的宏任务但是也可能会有兼容性问题。
MessageChannel
MessageChannel 的执行时机比 setTimeout 靠前而且执行实际准确但是会有兼容性问题。
setTimeout
setTimeout 执行时机在 messageChannel 之后如下 demo
function workLoop() {setTimeout(() {nextUnitOfWork performUnitOfWork(nextUnitOfWork);workLoop()}, 0)
}但是 setTimeout 的递归层级过深的话延迟就不是1ms而是4ms这样会造成延迟时间过长时间浪费。
看了上面这些方法多多少少都有些问题那么下面我们讲一下 react 怎么实现时间切片的。
React 18 时间切片源码
源码位置 https://github.com/facebook/react/blob/v18.3.1/packages/scheduler/src/forks/Scheduler.js 可以看到 react 18 中其实就是做了个兼容性判断
优先 setImmediate其次 messageChannel最后 setTimeout
直接看源码很容易懵逼因为源码中包含大量的兼容判断和优先级相关代码容易混淆我们的视线因此我们把复杂问题拆解一下从源码入手手撸一个 mini 版时间切片。
手撸时间切片
问题拆解
入口构建任务队列 创建时间切片通过当前时间 延迟得到过期时间塞入任务队列 创建宏任务 通过 setImmediate 等方法创建宏任务。 执行宏任务-循环执行时间切片 递归调用时间切片方法用于挂起、重启。 开启工作循环 循环执行队列任务超出时间不执行。
构建任务队列
使用 performance.now() 获取更精确的时间来创建每个任务过期时间并塞入任务队列中。
// 入口创建 task 并添加过期时间执行任务
function scheduleCallback(callbcak) {let unitOfwork {callbcak,expirationTime: performance.now() 5,}taskQueue.push(unitOfwork)// 开启宏任务requestHostCallback(workLoop)
}宏任务包装
通过如下三个方法 localSetImmediate MessageChannel localSetTimeout 包装我们的 callback 为宏任务
// 把 performWorkUntilDeadline 方法放入宏任务当中
if (typeof localSetImmediate function) {schedulePerformWorkUntilDeadline () {localSetImmediate(performWorkUntilDeadline);};
} else if (typeof MessageChannel ! undefined) {const channel new MessageChannel();const port channel.port2;channel.port1.onmessage performWorkUntilDeadline;schedulePerformWorkUntilDeadline () {port.postMessage(null);};
} else {schedulePerformWorkUntilDeadline () {localSetTimeout(performWorkUntilDeadline, 0);};
}
首次开启任务
拿到当前正在处理的任务开启执行包装好的宏任务
function requestHostCallback(callback) {scheduledHostCallback callback;if (!isMessageLoopRunning) {isMessageLoopRunning true;schedulePerformWorkUntilDeadline();}
}递归任务执行
执行宏任务获取当前时间,判断如果还有未完成的任务则开启递归。
// 宏任务执行的方法核心方法
const performWorkUntilDeadline () {if (scheduledHostCallback ! null) {const currentTime getCurrentTime();startTime currentTime;const hasTimeRemaining true;let hasMoreWork true;try {// 执行任务 scheduledHostCallback 就是 workLoophasMoreWork scheduledHostCallback(hasTimeRemaining, currentTime);} finally {if (hasMoreWork) {// 如果任务队列中还存在任务则继续递归执行schedulePerformWorkUntilDeadline();} else {isMessageLoopRunning false;scheduledHostCallback null;}}} else {isMessageLoopRunning false;}
};
workLoop 开启工作循环
循环执行队列中的任务currentTask 为空结束循环判断时间是否过期过期则不执行任务把控制权还给浏览器。 function workLoop(hasTimeRemaining, initialTime) {let currentTime initialTime;currentTask peek(taskQueue);while (currentTask) {// 判断是时间是否过期if ((currentTask.expirationTime currentTime) (shouldYieldToHost() || !hasTimeRemaining)) {break} else {// 执行具体回调currentTask.callbcak()currentTask taskQueue.shift()// currentTask peek(taskQueue); // react 18 写法 包含小顶堆的排序算法}}// 还有剩余任务未执行完成返回 trueif (currentTask ! null) {return true;} else {return false;}
}demo 模拟
下面使具体案例来模拟一下时间切片带来的改善:
完整版时间切片方法 let taskQueue [] // 任务队列
let isMessageLoopRunning false; // 标记 宏任务 正在运行
let scheduledHostCallback null; // 要执行的函数 workLoop
let currentTask null; // 当前执行的任务
let startTime null; // 任务开始的时间const localSetTimeout typeof setTimeout function ? setTimeout : null;
const localClearTimeout typeof clearTimeout function ? clearTimeout : null;
const localSetImmediate typeof setImmediate ! undefined ? setImmediate : null;// 获取当前时间
const getCurrentTime () performance.now();
// 根据时间判断是否把控制权交给浏览器
function shouldYieldToHost() {const timeElapsed getCurrentTime() - startTime;if (timeElapsed 5) {return false;}return true;
}
// 获取数组第一项
function peek(heap) {return heap.length 0 ? null : heap[0];
}
// 入口创建 task 并添加过期时间执行任务
function scheduleCallback(callbcak) {let unitOfwork {callbcak,expirationTime: performance.now() 5,}taskQueue.push(unitOfwork)// 开启宏任务requestHostCallback(workLoop)
}// 宏任务执行的方法核心方法
const performWorkUntilDeadline () {if (scheduledHostCallback ! null) {const currentTime getCurrentTime();startTime currentTime;const hasTimeRemaining true;let hasMoreWork true;try {// 执行任务 scheduledHostCallback 就是 workLoophasMoreWork scheduledHostCallback(hasTimeRemaining, currentTime);} finally {if (hasMoreWork) {// 如果任务队列中还存在任务则继续递归执行schedulePerformWorkUntilDeadline();} else {isMessageLoopRunning false;scheduledHostCallback null;}}} else {isMessageLoopRunning false;}
};// 把 performWorkUntilDeadline 方法放入宏任务当中
if (typeof localSetImmediate function) {schedulePerformWorkUntilDeadline () {localSetImmediate(performWorkUntilDeadline);};
} else if (typeof MessageChannel ! undefined) {const channel new MessageChannel();const port channel.port2;channel.port1.onmessage performWorkUntilDeadline;schedulePerformWorkUntilDeadline () {port.postMessage(null);};
} else {schedulePerformWorkUntilDeadline () {localSetTimeout(performWorkUntilDeadline, 0);};
}function requestHostCallback(callback) {scheduledHostCallback callback;if (!isMessageLoopRunning) {isMessageLoopRunning true;schedulePerformWorkUntilDeadline();}
}function workLoop(hasTimeRemaining, initialTime) {let currentTime initialTime;currentTask peek(taskQueue);while (currentTask) {// 判断是时间是否过期if ((currentTask.expirationTime currentTime) (shouldYieldToHost() || !hasTimeRemaining)) {break} else {// 执行具体回调currentTask.callbcak()currentTask taskQueue.shift()// currentTask peek(taskQueue); // react 18 写法 包含小顶堆的排序算法}}// 还有剩余任务未执行完成返回 trueif (currentTask ! null) {return true;} else {return false;}
}demo 模拟实现
let taskIndex 0;
let taskTotal 5000; // 任务数量
const start Date.now();function handleTask() {for (let j 0; j 5000; j) {// 执行一些耗时操作const btn1Attr document.getElementById(btn1).attributes;const btn2Attr document.getElementById(btn2).attributes;const btn3Attr document.getElementById(btn3).attributes;}if(taskIndex taskTotal) {console.log(任务调度完成用时, Date.now() - start, ms!);}
}while (taskIndex taskTotal) {scheduleCallback(handleTask) // 时间切片执行// handleTask() // 普通执行taskIndex
}document.getElementById(btn1).onclick function () {console.log(11111, click)
}// html
bodydiv idrootbutton idbtn1按钮1/buttonbutton idbtn2按钮2/buttonbutton idbtn3按钮3/buttonbutton idbtn4按钮4/button/divscript src./sh.js/script
/body
上面这一串代码在使用我们封装的 scheduleCallback 执行任务时dom 渲染几乎秒开但是如果使用普通的调用页面则会卡顿 3s 左右才会出现。
总结
react 使用时间切片提升渲染性能在熟知原理后同样我们在业务中也有很多优化场景可以使用到。例如高频埋点批量切片上传大量 dom 节点操作等等。