Skip to main content

后台线程处理 JS

在浏览器中运行的 JavaScript 代码通常会占用主线程,会持续执行直至任务完成或挂起。如果 JavaScript 代码过于复杂或任务过多,可能会导致界面不流畅或卡顿现象,降低用户体验。

对代码进行一些优化可以适当的减少基于计算阻塞 JS 进程和主线程的问题,例如避免重复运算、减少对象和数组的使用、使用适当的数据类型等,但是不能从根本解决大量计算带来的问题。

我们可以将一些耗时比较长的操作交给后台线程来处理,从而释放主线程。

setTimeout

可以通过 setTimeout() 函数来实现类似“一点一点地” 执行代码的效果。比如处理大量 DOM 元素时,可使用 setTimeout() 在每一步处理后都暂停执行一小段时间(如 50ms),从而避免浏览器假死。

以下是一个将多个同步任务分批处理成异步任务调用的函数:

function multiStep(steps, args, callback) {
const tasks = steps.concat() // 克隆一份要执行的任务列表

setTimeout(function () {
const task = tasks.shift() // 获取第一个任务
task.apply(null, args || []) // 执行第一个任务,args 参数需要是数组

if (tasks.length > 0) {
setTimeout(multiStep(tasks, args, callback), 25) // 重新执行 multiStep
} else {
callback() // 完成所有任务后的回调
}
}, 25)
}

下面是一个"分时"处理数组的函数,适用于对大量数据中的每一项做处理,并检查是否消耗了太长时间。如果处理完成这个元素需要的时间超过了 50 毫秒(可以根据实际情况修改此参数),就搁置当前循环,并在 25 毫秒后继续进行处理。这样,一旦有时间片可用,处理函数就能得到及时的处理。

function timeProcessArray(items, process, callback) {
const todo = items.concat()

setTimeout(function () {
var start = +new Date()

do {
process(todo.shift())
} while (todo.length > 0 && +new Date() - start < 50)

if (todo.length > 0) {
setTimeout(timeProcessArray(todo, process, callback), 25)
} else {
callback(items)
}
}, 25)
}

但是,由于 setTimeout() 是放在事件队列中的异步函数,所以可能会有多个间隔相等的 setInterval/setTimeout 的回调函数累积在事件队列中依次执行的情况,而主线程并没有任何控制权去限制他们的数量,安排得不当可能会造成意外的性能问题或卡死页面。

requestAnimationFrame

requestAnimationFrame() 方法从性能上来说比 setTimeout 好,因为它执行时机与浏览器的刷新频率同步。它可以使回调函数安排最接近上一次绘图的重绘机会,对于动态效果特别明显。

Web Worker

Web Workers 是一种专门用于在后台线程中执行 JavaScript 代码的技术。它允许 JavaScript 代码并发地在多个线程中运行,从而将一些操作转移到后台线程中去处理,从而最大化主线程的吞吐量。Web Workers 能与页面脚本分离,但仍然能进行要求数据传输和共享内存。

Workers 和主线程间的数据传递通过这样的消息机制进行——双方都使用 postMessage() 方法发送各自的消息,使用 onmessage 事件处理函数来响应消息(消息被包含在 message 事件的 data 属性中)。这个过程中数据并不是被共享而是被复制。

以下是个简单 Worker 示例:

if (window.Worker) {
// 为了更好的错误处理控制以及向下兼容
// 创建一个新的 Worker,指定一个脚本的 URI 来执行 Worker 线程
const myWorker = new Worker('worker.js')
const result = {} // 接收 Worker 中的数据

// 1 秒后向 Worker 发送消息,模拟交互
setTimeout(() => {
myWorker.postMessage(['Hello', 'YINAOR'])
console.log('Message posted to worker')
}, 1000)

myWorker.onmessage = function (e) {
result.textContent = e.data
console.log('Message received from worker')
}

// 当 Worker 出现运行中错误时
myWorker.onerror = function (e) {
console.log(e.message) // 可读性良好的错误消息
console.log(e.filename) // 发生错误的脚本文件名
console.log(e.lineno) // 发生错误时所在脚本文件的行号
}

// 1 分钟后主线程中立刻终止运行中的 Worker,即使 Worker 中的代码没执行完
setTimeout(() => {
myWorker.terminate()
}, 60000)
}
// 在 Worker 中接收到消息
onmessage = function (e) {
console.log('Message received from main script')
const workerResult = 'Result: ' + e.data[0] * e.data[1]
console.log('Posting message back to main script')
postMessage(workerResult)
}

// Worker 中的全局上下文与主进程的 Window 不同,所以这里的 setTimeout 在单独的线程中执行,并且不能访问 DOM 元素
// 但是仍然可以使用大量 window 对象之下的东西,包括 WebSockets,IndexedDB 以及 FireFox OS 专用的 Data Store API 等数据存储机制。
setTimeout(() => {
// 在 Worker 线程中关闭自己
close()
}, 120000)

上面只是一个简单的 Worker 示例,还有很多未提及的概念:

  • 共享 Worker:可以被多个脚本使用,与专用 Worker 有区别
var myWorker = new SharedWorker('worker.js')
  • subworker:Worker 生成更多的 Worker
  • importScripts:Worker 中引用脚本与库
  • 嵌入式 Worker:像 <script> 元素一样将 Worker 的代码嵌入的网页中
<script type="text/js-worker">
// 该脚本不会被 JS 引擎解析,因为它的 mime-type 是 text/js-worker。
var myVar = "Hello World!";
// 剩下的 worker 代码写到这里。
</script>
  • 服务 worker:ServiceWorkers
  • Chrome Workers
  • 音频 worker

详细使用查看 MDN 的文章使用 Web Workers