青山隐隐水迢迢,秋尽江南草未凋。 --寄扬州韩绰判官
前言
JavaScript 异步编程的前提是深刻了解事件循环,
都已经知道了JS引擎是单线程,如果不是单线程的,那么就在同一时间可以做多件事情,听起来很美好是不是,但是,这对面向前端的语言是危险的,比如,同时好几个线程同时操作一个 DOM 对象,该以谁的为准呢?等等会造成很多的问题。所以 JS 设计成单线程的作用就是简单化编程。
任务队列
当我们调用一个方法的时候,JavaScript 会生成一个与这个方法对应的执行环境,又叫执行上下文(context)。这个执行环境中保存着该方法的私有作用域、上层作用域(作用域链)、方法的参数等等;同步任务都在主线程上执行,形成一个执行栈,对于异步代码。主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
下图是事件循环示意图:
对于上图,我们可以认为同步代码块执行都位于左侧的同步执行栈,而所有的异步代码都位于右侧的任务队列,这里存放着所有的异步程序。JS 引擎会先执行同步代码块,当同步代码块执行完毕后,再去右侧异步代码存放区域取出异步代码执行。然后循环往复。
上图大致描述就是:
●主线程运行时会产生执行栈,栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)
●而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
微任务和宏任务
先记住一个前提,宏任务和微任务是对应异步代码来说的,同步代码是没这么多事情的,都是乖乖的按照执行栈执行的。
首先看一下常见的微任务和宏任务
宏任务:script(整体代码)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)
微任务:Promise.then、Promise.catch、Promise.final、 MutaionObserver、process.nextTick (Node.js)
同时存在微任务和宏任务的时候,微任务是先于宏任务执行的,所以,执行顺序是:同步代码 > 微任务 >
宏任务
EventLoop 和 Async/Await
async/await是 ES7 新增的 API,是生成器的语法糖,await会暂停代码的执行;
await 等待的是一个表达式(主要是等待一个 async
函数的返回值),这个表达式的计算结果是 Promise 对象(也就是异步方法)或者其它值,如字符串、数字等(换句话说,就是没有特殊限定)。所以,await 后面实际是可以接普通函数调用或者直接量的。
所以这可以当成 Promise 来看:
比如这段代码:
kotlin
async function foo() {
// await 前面的代码
await fun();
// await 后面的代码
}
async function fun() {
// 代码执行
}
foo();
这段代码,await前边是同步代码, 这句可以被转换成 Promise.resolve(bar())
;await后边是异步代码,会转换成 Promise ,await后面代码就是会在 Promise 的then 方法中执行的代码块,所以,await后面的代码属于微任务。
我们将上边的代码转换成 Promise 语法的代码。
kotlin
function foo() {
// await 前面的代码
Promise.resolve(fun()).then(() => {
// await 后面的代码
});
}
function fun() {
// 代码执行
}
foo();
定时器
事件循环机制的核心是:JS引擎线程和事件触发线程
当我们调用setTimeout后,并不是JS 引擎在负责定时器,而是有单独的定时器线程在负责,这就与其他的异步程序有所区分,
为什么要单独的定时器线程?因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响定时器的准确性,因此需要单独开辟一个线程来维护定时器,以保证其计时的准确性。当使用定时器时的时候,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。
譬如:
javascript
setTimeout(function(){ console.log('hello!'); }, 1000);
这段代码的作用是当1000毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行
javascript
setTimeout(function(){ console.log('hello!'); }, 0); console.log('begin');
这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行
注意:
- 执行结果是:先begin后hello!
- 虽然代码的本意是0毫秒后就推入事件队列,但是W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
(不过也有一说是不同浏览器有不同的最小时间设定)
- 就算不等待4ms,就算假设0毫秒就推入事件队列,也会先执行begin(因为只有可执行栈内空了后才会主动读取事件队列)
浅谈 Web Workers
需要强调的是,Worker 是浏览器 (即宿主环境) 的功能,实际上和 JavaScript 语言本身几乎没有什么关系。也就是说,JavaScript 当前并没有任何支持多线程执行的功能。
所以,JavaScript 是一门单线程的语言!JavaScript 是一门单线程的语言!JavaScript 是一门单线程的语言!
浏览器可以提供多个 JavaScript 引擎实例,各自运行在自己的线程上,这样你可以在每个线程上运行不同的程序。程序中每一个这样的的独立的多线程部分被称为一个 Worker。这种类型的并行化被称为 任务并行,因为其重点在于把程序划分为多个块来并发运行。下面是 Worker 的运作流图。
Web Worker 实例
下面用一个乘法的例子浅谈 Worker 的用法。
首先新建一个 index.html ,直接上代码:
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width" />
<title>Web Workers basic example</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Web<br />Workers<br />basic<br />example</h1>
<div class="controls" tabindex="0">
<form>
<div>
<label for="number1">Multiply number 1: </label>
<input type="text" id="number1" value="0" />
</div>
<div>
<label for="number2">Multiply number 2: </label>
<input type="text" id="number2" value="0" />
</div>
</form>
<p class="result">Result: 0</p>
</div>
<script src="main.js"></script>
</body>
</html>
新建一个 main.js,内容如下:
ini
const first = document.querySelector('#number1');
const second = document.querySelector('#number2');
const result = document.querySelector('.result');
if (window.Worker) {
const myWorker = new Worker("worker.js");
[first, second].forEach(input => {
input.onchange = function() {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}
})
myWorker.onmessage = function(e) {
result.textContent = e.data;
console.log('Message received from worker');
}
} else {
console.log('Your browser doesn't support web workers.');
}
新建一个worker.js
ini
onmessage = function(e) {
console.log('Worker: Message received from main script');
const result = e.data[0] * e.data[1];
if (isNaN(result)) {
postMessage('Please write two numbers');
} else {
const workerResult = 'Result: ' + result;
console.log('Worker: Posting message back to main script');
postMessage(workerResult);
}
}
首先,我们通过const myWorker = new Worker("worker.js");
引入worker
,为两个输入框注册了onchange
点击事件,点击事件的执行是为worker
线程传入两个输入框的值,
在worker
线程中,onmessage被调用开始执行,计算结果,得到结果后,执行postMessage(workerResult);
,向主线程发送数据,主线程通过myWorker.onmessage
接收数据,然后更新 DOM。