转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 原文转载自 https://neoteric.eu/blog/when-async-is-not-enough-introduction-to-multithreading-in-the-browser/
先说最重要的:JavaScript代码可以异步执行,但并不意味着它是跑在多个线程里。那么异步到底是什么意思?让我们想象发一个Ajax请求,向服务端请求数据。你并不是立即得到响应——你需要等待一小段时间,让服务端返回数据。在等待响应的过程中,程序运行着你其他部分的代码。如果不是这样,Ajax请求会冻结住,不让后面的代码执行,直到收到服务端的响应——这不是我们想要的,对吧?
事件循环(Event Loop)
在JavaScript运行环境中,有个非常重要的概念,叫事件循环。它周而复始地工作着,每一次循环被称为一个"tick"。如果在某一个tick中,有等待着的事件队列需要处理,那么它们会一个个地被执行。大家所熟知的setTimeout函数就是一个很好的例子。它的第一个参数是一个回调函数——一个在某段时间之后被执行的函数。当setTimeout被解析时,它被压入函数调用栈的栈顶,它设置一个定时器,然后就从栈顶弹出,把你的回调函数塞到事件循环的后面——那意味着这个回调函数不会精确地在定义的时间间隔后执行——在事件队列中等待的其他事件需要被优先处理。当时机到来,你的回调函数被压入函数调用栈的栈顶,然后执行。你发向服务器的请求,也是同样的原理——你定义一个回调函数,当收到响应后,它被塞进事件循环队列的后面。
函数调用栈(Call Stack)
函数调用栈是一个底层的数据结构——它记录我们运行到程序哪儿了。当程序进入一个函数,就把它放在栈顶,当从函数中返回,就意味着把它从栈中弹出。让我们使用一点递归式的逻辑来简单展示一下:
function factorial(n) {
if(n === 1 || n === 0) {
return 1;
}
return factorial(n - 1) * n;
}
console.log(factorial(3)); // 3! = 6
WebWorkers
你已经看到,异步代码,解决的是一件事情"现在发生"还是"以后发生",而不是解决如何让"多个事情同时发生"。但如果有一些处理器密集型任务,我们担心它会让界面卡住,怎么办?
答案是WebWorkers。它允许JavaScript代码在后台以一个独立的线程被执行。它允许主线程流畅运行,不被阻塞。WebWorkers在另一个与window不同的全局上下文环境中。这也带来了一些局限:比如,你不能直接在Worker里操作DOM。最基础的(也是浏览器支持得最好的)WebWorker类型是Dedicated Worker。
想创建一个Worker,你需要向Worker构造函数传入一个文件名,在该文件中包含了需要执行的JavaScript脚本。
// 在主线程
var factorialWorker = new Worker('factorial.worker.js');
比如说,我们想得到一整组数字的阶乘。
想向Worker传数据,你需要调用postMessage方法:
// 在主线程
var arr = [50, 100, 125, 150];
for(var i = 0; i < arr.length; ++i) {
factorialWorker.postMessage(arr[i]);
}
你可以通过事件在主线程和Worker线程之间通信。如果你想监听Worker的返回值,就在主线程注册一个事件监听器。
// 在主线程
factorialWorker.addEventListener('message', function(event) {
console.log('!' + event.data.number + ' = ' + event.data.factorial);
});
这会输出传入给Worker的数字的阶乘。
剩下唯一要做的事情就是创建factorial.workder.js文件。
它需要返回当前计算的数字的阶乘,还要定义计算阶乘的函数本身。
在Worker中,有一个self属性。它返回指向WorkerGlobalScope的引用。利用它,我们可以和向Worker发送数据的脚本通信。
// factorial.workder.js
function factorial(n) {
if(n === 1 || n === 0) {
return 1;
}
return factorial(n - 1) * n;
}
self.addEventListener('message', function(event) {
self.postMessage({ number: event.data, factorial: factorial(event.data) });
});
这里发生的情况是,我们创建了一个新的Worker,并监听它给我们返回的数据。然后,我们向它发送数据——Worker会得到数据,在完成它内部的计算之后,向我们发送一个响应。所有的计算都在一个单独线程中完成。很酷吧?
不过你可能会遇到一些问题。第一个问题是Chrome不能以本地文件的方式使用WebWorkers。不过你可以开启一个http服务器来尝试使用它。
Webpack
另一个问题可能在你使用Webpack时出现。它可能会给你一个404 Not Found错误,因为它不知道你想以WebWorker的形式加载文件。你需要额外的加载器(loader)来加载类似的文件。让我带你看看这个过程。首先,用npm安装加载器:
npm install --save-dev worker-loader
然后你需要在webpack.config.js中添加一条规则:
module: {
rules: [
{
test: /\.worker\.js$/,
use: { loader: 'worker-loader' }
},
(...)
]
}
现在,如果你引入以.workder.js结尾的文件,Webpack会使用worker-loader来加载。让我们用ES6的一些特性来修改一下代码:
import FactorialWorker from './factorial.worker.js';
const factorialWorker = new FactorialWorker();
factorialWorker.addEventListener('message', event => {
console.log(`!${event.data.number} = ${event.data.factorial}`);
});
const arrayOfNumbers = [50, 100, 125, 150];
for(let number of arrayOfNumbers) {
factorialWorker.postMessage(number);
}
总结一下,当开发一个背后有很多操作(尤其是密集型计算)的富应用时,WebWorkers会非常有帮助。尝试一下,亲自看看吧。我鼓励你去试验。