🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 时间分片 ## 使用定时器 在 JS 的`Event Loop`中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染。 页面的卡顿是由于同时渲染大量DOM所引起的,所以我们考虑将渲染过程分批进行。 <br> ## requestAnimationFrame ### setTimeout 和闪屏现象 * `setTimeout`的执行时间并不是确定的。在JS中,`setTimeout`任务被放进事件队列中,只有主线程执行完才会去检查事件队列中的任务是否需要执行,因此`setTimeout`的实际执行时间可能会比其设定的时间晚一些。 * 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的刷新频率可能会不同,而`setTimeout`只能设置一个固定时间间隔,这个时间不一定和屏幕的刷新时间相同。 以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致。 在`setTimeout`中对dom进行操作,必须要等到屏幕下次绘制时才能更新到屏幕上,如果两者步调不一致,就可能导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素,从而导致丢帧现象。 <br> ### 使用 requestAnimationFrame 与`setTimeout`相比,`requestAnimationFrame`最大的优势是由系统来决定回调函数的执行时机。 如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,`requestAnimationFrame`的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象。 <br> ## DocumentFragment `DocumentFragments`是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。 <br> ## 最终代码 ~~~ <ul id="container"></ul> ~~~ ~~~ //需要插入的容器 let ul = document.getElementById('container'); // 插入十万条数据 let total = 100000; // 一次插入 20 条 let once = 20; //总页数 let page = total/once //每条记录的索引 let index = 0; //循环加载数据 function loop(curTotal,curIndex){ if(curTotal <= 0){ return false; } //每页多少条 let pageCount = Math.min(curTotal , once); window.requestAnimationFrame(function(){ let fragment = document.createDocumentFragment(); for(let i = 0; i < pageCount; i++){ let li = document.createElement('li'); li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total) fragment.appendChild(li) } ul.appendChild(fragment) loop(curTotal - pageCount,curIndex + pageCount) }) } loop(total,index); ~~~ <br> <br> # worker ## 什么是worker ~~~ 运行者 Worker 接口是Web Workers API的一部分,代表一个后台任务, 它容易被创建并向创建者发回消息。创建一个运行者只要简单的调用Worker()构造函数,指定一个脚本,在工作线程中执行。(引自MDN) ~~~ 看概念可能有点枯燥,通俗点讲就是:因为js是单线程运行的,在遇到一些需要处理大量数据的js时,可能会阻塞页面的加载,造成页面的假死。这时我们可以使用worker来开辟一个独立于主线程的子线程来进行哪些大量运算。这样就不会造成页面卡死。也说明 worker可以用来解决大量运算是造成页面卡死的问题。 <br> <br> ## 语法 ### 创建 Web Workers ~~~ const worker=new Worker(aURL, options) ~~~ 它有两个参数: * aURL(必须)是一个DOMString 表示worker 将执行的脚本的URL。它必须遵守同源策略。 * options (可选)它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程 <br> ### 收发消息 Web Workers 用来执行异步脚本,只要掌握了它与主线程通信的方式,就可以在指定时机运行异步脚本,并在运行完时将结果传递给主线程。 <br> ### 主线程接收发 Web Workers 消息 ~~~text const worker = new Worker("../src/worker.js"); worker.onmessage = e => {}; worker.postMessage("Marco!"); ~~~ 每个`worker`实例通过`onmessage`接收消息,通过`postMessage`发送消息。 <br> ### Web Workers 收发主线程消息 ~~~text self.onmessage = e => {}; self.postMessage("Marco!"); ~~~ 和主线程代码类似,在 Web Workers 代码中,也是`onmessage`接收消息,这个消息来自主线程或者其它 Workers。也可以通过`postMessage`发送消息。 <br> ### 销毁 Web Workers ~~~text worker.terminate(); ~~~ 文章内容就这么多,是不是有写太简单了呢!笔者结合自己的使用经验,再补充一些知识。 <br> <br> ## 使用worker的注意点 <br> ### 1.同源限制 分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。 <br> ### 2.DOM 限制 Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。 <br> ### 3.通信联系 Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。 <br> ### 4.脚本限制 Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。 <br> ### 5.文件限制 Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。 <br> ## 优化 ### 对象转移(Transferable Objects) 对象转移就是将对象引用零成本转交给 Web Workers 的上下文,而不需要进行结构拷贝。 这里要解释的是,**主线程与 Web Workers 之间的通信,并不是对象引用的传递,而是序列化/反序列化的过程**,当对象非常庞大时,序列化和反序列化都会消耗大量计算资源,降低运行速度。 ![](https://pic2.zhimg.com/80/v2-be0271a7f89a35c6cd4b74e5f2a7cc49_1440w.jpg) 上面的图充分证明了,大对象传递,使用对象转移各项指标都优于结构拷贝。 对象转移使用方式很简单,给`postMessage`增加一个参数,把对象引用传过去即可: ~~~text var ab = new ArrayBuffer(1); worker.postMessage(ab, [ab]); ~~~ 浏览器兼容性也不错:Currently Chrome 17+, Firefox, Opera, Safari, IE10+。更具体内容,可以看[Transferable Objects: Lightning Fast!](https://link.zhihu.com/?target=https%3A//developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast)。 > 需要注意的是,对象引用转移后,原先上下文就无法访问此对象了,需要在 Web Workers 再次将对象还原到主线程上下文后,主线程才能正常访问被转交的对象。 ### 如何不用 JS 文件创建 Web Workers Web Workers 优势这么大,但用起来需要在同域下创建一个 JS 文件实在不方便,尤其在前后端分离做的比较彻底的团队,前端团队能控制的仅仅是一个 JS 文件。那么下面给出几个不用 JS 文件,就创建 Web Workers 的方法: ### webpack 插件 - worker-loader [worker-loader](https://link.zhihu.com/?target=https%3A//github.com/webpack-contrib/worker-loader)是一个 webpack 插件,可以将一个普通 JS 文件的全部依赖提取后打包并替换调用处,以 Blob 形式内联在源码中。 ~~~text import Worker from "worker-loader!./file.worker.js"; const worker = new Worker(); ~~~ 上述代码的魔术在于,转化成下面的方式执行: ~~~text const blob = new Blob([codeFromFileWorker], { type: "application/javascript" }); const worker = new Worker(URL.createObjectURL(blob)); ~~~ ### Blob URL 第二种方式由第一种方式自然带出:如果不想用 webpack 插件,那自己通过 Blob 的方式创建也可以: ~~~text const code = ` importScripts('https://xxx.com/xxx.js'); self.onmessage = e => {}; `; const blob = new Blob([code], { type: "application/javascript" }); const worker = new Worker(URL.createObjectURL(blob)); ~~~ 看上去代码更轻量一些,不过问题是当遇到复杂依赖时,如果不能把依赖都转化为脚本通过`importScripts`方式引用,就无法访问到主线程环境中的包。如果真的遇到了这个问题,可以用第一种 webpack 插件的方式解决,这个插件会自动把文件所有依赖都打包进源码。 ### 管理 postMessage 队列 为什么 postMessage 会形成队列,为什么要管理它? 首先在 Web Workers 架构设计上就必须做成队列,因为调用`postMessage`时,对应的 Web Workers 不一定完成了初始化,所以浏览器底层必须管理一个队列,在 Web Workers 初始化完毕时,依次消费,这样才能确保任何时候发出的`postMessage`都能被 Web Workers 接收到。 其次,为什么要手动维护这个队列,原因可能取决于如下几点: * 业务原因,前面的`postMessage`还没来得及消费,就不要发送新的消息,或者丢弃新的消息,这时候需要通过双向通信拿到 Web Workers 的执行结果回执,手动控制队列。 * 性能原因,一般 Web Workers 都会被用来执行耗时的同步运算,如果运算时间比较长,那短期塞入多个消息队列是没有意义的。 ![](https://pic1.zhimg.com/80/v2-6bb0d68f3e9457ab3ea0f8d2b52a29e0_1440w.jpg) 如上图所示,对于每次用户输入都要进行的 SQL Parser 很耗时,及时放在 Web Workers 也可能导致将 Workers 撑爆到无响应,这是不仅要使用多 Workers 缓冲池,还要对待执行队列进行过滤,因为用户永远只关心最后一次输入的 Parser 结果。 由于 Web Workers 运算被卡住时,除了销毁 Worker 没有别的办法,而销毁 Worker 的成本比较高,不能对每一个用户输入都销毁并新建 Web Workers,所以利用 Workers 缓冲池,当缓冲池满了,新的消费队列又进来的时候,可以销毁全部 Workers 缓冲池,换一批新缓冲池重新消费用户输入。 <br> <br> # 虚拟列表 https://juejin.cn/post/6844903982742110216#heading-5 <br> <br> # 参考资料 * [「中高级前端」高性能渲染十万条数据(时间分片)](https://juejin.im/post/5d76f469f265da039a28aff7) * [ 聊聊前端开发中的长列表](https://zhuanlan.zhihu.com/p/26022258) * [再谈前端虚拟列表的实现](https://zhuanlan.zhihu.com/p/34585166)