当异步不再能满足需求:对浏览器中的多线程的介绍
转载请注明出处:,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。
原文转载自 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。不过你可以开启一个来尝试使用它。
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会非常有帮助。尝试一下,亲自看看吧。我鼓励你去试验。
下一篇: python之反射