Javascript进阶——异步编程原理

理解JS异步

同步和异步
  • 同步:调用之后得到结果,再依次执行其他的任务
  • 异步:调用之后可以不等待结果,继续做其他的事

众所周知,Javascript是单线程的,代码只能一行一行通过JS引擎的主线程执行。但是这种模式存在一个问题:如果有一个任务耗时很长,后面的任务都必须排队等着,会导致整个程序卡在一个地方,其他任务无法执行,造成页面长时间无响应,甚至卡死,用户体验很糟糕。

如此,“异步”模式就显得很重要了,耗时很长的操作都使用异步执行,避免浏览器失去响应,让用户能够流畅的访问网页。那么单线程的JS是怎么实现异步的呢?

JS异步原理

JS引擎本身是单线程的,异步其实是借助浏览器内核多线程来实现的。现代浏览器使用的都是多进程架构(如下图),而一个进程可以包含一个或多个线程。JS引擎处于渲染进程,异步也是通过此进程中的各个线程的协调来完成的。


浏览器进程
  • GUI线程:主要用于渲染布局
  • JS引擎线程:用于解析、执行JS代码;它与GUI线程是互斥的,因为JS里可以操作DOM,如果与GUI同时执行,可能会引起页面渲染混乱
  • 定时触发器线程:用于执行定时任务,setTimeout,setInterval
  • 事件触发线程:将满足条件的事件加入任务队列
  • 异步HTTP请求线程:XHR所在线程,用于处理AJAX请求

多线程之间的配合实现异步:

  • 定时器,异步请求线程可以独立于JS引擎主线程同时运行
  • 定时器任务定时完成后,会通知事件触发线程,将定时器的回调任务加入任务队列
  • 异步HTTP请求完成时,如果有回调函数,就会通知事件触发线程往任务队里添加事件
  • 通过Event Loop机制,浏览器依次执行完任务队列里的所有任务

理解 Event Loop机制

JS异步的实现,Event Loop是关键的一步。Event Loop其实是一个处理模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。这里我们主要来看看浏览器端的实现。

在Event Loop模型中,我们将任务分为宏任务(macrotask)和微任务(microtask)。
以下任务会加入宏任务队列:

  • script,执行主线程中的script,是全局任务,也可以看成是一个宏任务
  • setTimeout/setInterval
  • I/O
  • UI rendering

以下任务会加入微任务队列:

  • Promise
  • Object.observe
  • MutationObserver
  • postMessage, 主要用于window对象之间的通信

首先我们先来看看浏览器执行一个JavaScript代码的具体流程:


JS代码执行流程
  1. 首先执行全局script,script可以包含同步任务和异步任务,同步任务执行完就出栈,异步任务则通过异步处理机制加入任务队列
  2. 当所有同步任务执行完成,首先检查微任务队列,一次执行完所有微任务
  3. 当所有微任务执行完以后,从任务队列中取出最早的宏任务,宏任务执行前,再次检查微任务队列,如果有新的微任务,先清空微任务,再执行宏任务,执行完成后,依次从队列中取出后面的宏任务,重复此步骤,直到执行完所有宏任务,Stack清空

步骤3,可以解释为以下Event Loop模型:


Event Loop处理模型

我们看一段示例代码,进一步加深理解:

console.log("1");
setTimeout(function() {
  console.log("2");
}, 0);
Promise.resolve().then(function() {
  console.log("3");
});
console.log("4");

console.log("start");

setTimeout(() => {
  console.log("setTimeout");
  new Promise(resolve => {
    console.log("promise inner1");
    resolve();
  }).then(() => {
    console.log("promise then1");
  });
}, 0);

new Promise(resolve => {
  console.log("promise inner2");
  resolve();
}).then(() => {
  console.log("promise then2");
});

正确的打印结果如下:
1
4
start
promise inner2
3
promise then2
2
setTimeout
promise inner1
promise then1
我们来解析一下执行过程:

  1. 首先依次执行同步任务:console.log("1"),console.log("4"),console.log("start")
  2. 需要注意的是console.log("promise inner2")是在new Promise定义中中声明的,也是同步执行的,then方法里面的才是异步任务
  3. 同步任务执行完,Stack清空,依次执行定义在外层的Promise微任务
  4. 外层微任务清空,依次执行宏任务setTimeout

参考链接:

  1. 浏览器进程?线程?傻傻分不清楚!
  2. [译]官方图解:Chrome 快是有原因的,现代浏览器的多进程架构!
  3. Event Loops标准
  4. JS中的栈内存堆内存

你可能感兴趣的:(Javascript进阶——异步编程原理)