JS 中与异步相关的概念整理

在使用 Angular 开发的过程中经常会遇到框架层面操作和我们代码逻辑上的执行顺序问题,往往一个 setTimeout 就能解决问题,但 setTimeout 到底做了什么呢?

经常听说并行、并发等概念,它们和异步又有什么关系呢?

我们先从进程、线程、协程是什么说起~

进程&线程&协程

进程

操作系统分配资源的最小单位。

线程

操作系统分配 CPU 资源的最小单位。

进程和线程,它们都属于系统级别的任务调度。

程序在运行的时候,需要操作系统分配内存和其他硬件资源,所以将运行的程序抽象为进程。一开始操作系统只能执行单一进程,后来使用分时间片来运行多个进程产生了多任务系统。

而线程的出现,是由于进程开销比较大,还有就是进程之间的资源隔离,导致数据沟通复杂。

进程和进程之间的资源都是相互隔离的,所以一个进程的崩溃不会影响到其他进程。但是由于线程是包含在进程之内的,线程的崩溃就会引发进程的崩溃,而在同一进程内的线程也会继而崩溃。

JavaScript 是单线程的

所谓单线程,是指在JS引擎中负责解释和执行 JavaScript 代码的线程只有一个。

但是实际上还存在其他的线程。例如:处理AJAX请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程(例如在Node.js中)等等。这些线程可能存在于JS引擎之内,也可能存在于JS引擎之外。详情看JS 引擎(engine)&JS 运行时(runtime)

协程

协程与操作系统没有关系,是程序内部的逻辑流调度。目标是在该程序分配到的时间片内能够充分利用 CPU 资源。

Generator

Generator 函数是 ECMAScript 6 对协程的实现,但属于不完全实现,只做到了暂停执行和转移执行权,有一些特性没有实现,比如不支持所调用的函数之中的 yield 语句(即递归执行yield语句)。

如果将 Generator 函数看作多任务运行的方式,存在多个进入点和退出点。那么,一方面,并发的多任务可以写成多个 Generator 函数;另一方面,继发的任务则可以按照发生顺序,写在一个 Generator 函数之中,然后用一个任务管理函数执行。

并发&并行

并发

看似同时在处理,实际可能每个任务在轮流利用极小的时间片。

img

例如:在单核处理器上的处理多线程就是并发——每个线程都处于执行过程中的某个状态。

并行

并行是真正的同时执行

并行一定是发生在多核处理器上,线程在不同的处理器上同时运行。

同步&异步

同步

同步就是程序一个语句一个语句顺序执行,很符合人得思维,是线性执行。

异步

异步是非阻塞的,异步逻辑与主逻辑相互独立,主逻辑不需要等待异步逻辑完成,而是可以立刻继续下去。

异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。

实现异步可以采用多线程技术或交给另外的进程来处理。

异步的实现

AJAX

AJAX 的全称是异步的 Javascript 和 XML ,通过在后台与服务器进行少量数据交互,实现网页的异步更新,在不重新加载整个界面的情况下,做到网页的部分刷新。

交互过程:

  1. 用户发出异步请求
  2. 创建 XMLHttpRequest 对象
  3. 告诉 XMLHttpRequest 对象哪个函数会处理 XMLHttpRequest 对象状态的改变,为此要把对象的 onReadyStateChange 属性设置为响应该事件的 JavaScript 函数的引用
  4. 创建请求,设置参数(用 open 方法指定是 get 还是 post 、是否异步、url 地址等)
  5. 发送请求, send 方法
  6. 接收结果并分析
  7. 实现刷新

注意:XMLHttpRequest 对象不是 JavaScript 语言本身的对象,是浏览器提供的。所有现代浏览器 (IE7+、Firefox、Chrome、Safari 以及 Opera) 都内建了 XMLHttpRequest 对象。

setTimeout

js 中的 setTimeout() 函数是异步的,setTimeout() 中的代码将被顺次插入到事件队列中,等待主逻辑运行完毕才会执行。

1
2
3
4
5
6
for(var i=0;i<10;i++){
setTimeout(function() {
console.log(i);
}, 0);
}
// 结果是打印10次10
1
2
3
4
5
6
7
8
9
10
var t = true;

window.setTimeout(function (){
t = false;
},1000);

while (t){}

alert('end');
// setTimeout 中的函数和 alert 都不会执行

消息队列&事件循环

异步过程中,工作线程在异步操作完成后需要通知主线程。那么这个通知机制是怎样实现的呢?答案是利用消息队列和事件循环。

消息队列

消息队列是一个先进先出的队列,它里面存放着各种消息。

事件循环

事件循环是指主线程重复从消息队列中取消息、执行的过程。

工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。

异步&事件

消息队列中的每条消息实际上都对应着一个事件。

DOM事件也会触发一个异步流程,addEventListener 函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。

另一方面,所有的异步过程也都可以用事件来描述。例如:setTimeout 可以看成对应一个 时间到了!的事件。前文的 setTimeout(fn, 1000) 可以看成:

1
timer.addEventListener('timeout', 1000, fn);

至于为什么一些事件在绑定之后可以一直存在并多次触发?

这一点跟 setInterval 有点类似,可以这样认为,异步过程不一定是一次性的,在发起后可以分多次得到多个结果,即说收到多次通知。对于这种类型的异步过程,工作线程必须是一个一直存在的线程。

JS 引擎(engine)&JS 运行时(runtime)

JS 引擎

js 引擎始终只有一个线程,它维护一个消息队列,当前函数栈执行完成之后就去不断地取消息队列中的消息(回调),取到了就执行。但是js引擎只负责取消息,不负责生产消息

JS 运行时

js运行时,就负责给 js 引擎线程发送消息。

比如浏览器 DOM 事件发送一条鼠标点击的消息(浏览器子线程和 JS 引擎线程的 IPC 通信),那么 js 引擎在执行完函数栈之后就会取到这条鼠标点击信息,执行消息(即回调);

比如node运行时读取文件,执行系统调用,完成后发送读取文件完成的消息,之后的过程同上。js 运行时只负责生产消息,不负责取消息