欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

当异步不再能满足需求:对浏览器中的多线程的介绍

程序员文章站 2022-07-06 11:46:22
转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 原文转载自 https://neoteric.eu/blog/when-async-is-not-enough-introduction-to-multithreading-in-the-browser/ 先 ......

转载请注明出处:,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。

原文转载自 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会非常有帮助。尝试一下,亲自看看吧。我鼓励你去试验。