关于前端异步与单线程

你真的了解前端异步编程、单线程吗?

写在前面的定义:

  • 同步:一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去。
  • 异步:进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
  • 进程:狭义上,就是正在运行的程序的实例。广义上,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 线程:线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位。指运行中的程序的调度单位。
  • 单线程:单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。单线程就是进程里只有一个线程。
  • 多线程:在单个程序中同时运行多个线程完成不同的工作,称为多线程。

了解下js运行和浏览器解析:

1. js是单线程的,但是浏览器解析是多线程:

  • JS运行在浏览器中,是单线程的,每个window[对应浏览器的tab页]一个JS线程,既然是单线程的,在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码,这是一个主线程的运行过程以执行栈来完成,包括函数的压栈和出栈过程。——————‘先进后出’
  • 而浏览器是事件驱动的,浏览器中很多行为是异步的,会创建事件并放入执行队列中,JavaScript引擎是单线程处理它的任务队列。——————‘先进先出’
  • 主要的异步行为:
  1. 事件[Event Loop(事件循环)}
  2. 定时器
  3. XMLHttpRequest完成回调触发等
    除此之外nodejs的一些IO操作
  • 浏览器是多线程协同解析的:

    JavaScript引擎线程
    界面渲染线程
    浏览器事件触发线程
    HTTP请求线程

当一个异步事件发生的时候,它就进入事件队列。浏览器有一个内部大消息循环,Event Loop(事件循环),会轮询事件队列并处理事件。比如,浏览器当前正在忙于处理onclick事件,这时window onSize事件发生了,这个异步事件就被放入事件队列等待处理,只有前面的处理完毕了,空闲了才会执行这个事件。

为什么JavaScript是单线程的却能让AJAX异步发送和回调请求,为什么setTimeout也看起来像是多线程的?

Ajax请求确实是异步的,这请求是由浏览器新开了一个线程请求,事件回调的时候是放入Event loop单线程事件队列等候处理。当浏览器空闲的时候出队列任务被处理,JavaScript引擎始终是单线程运行回调函数、单线程处理它的任务队列。
setTimeout(func, 0)神奇在哪儿?那就是告诉js引擎,在0ms以后把func放到主事件队列中,等待当前的代码执行完毕再执行,注意:重点是改变了代码流程,把func的执行放到了主事件队列中。这就是它的神奇之处了。它的用处有三个:

让浏览器渲染当前的变化(很多浏览器UI render和js执行是放在一个线程中,线程阻塞会导致界面无法更新渲染)
重新计算script运行时间,即重新判断”script is running too long”这个警告
改变了执行顺序

异步编程三种

一:回调函数这是异步编程最基本的方法。假定有两个函数f1和f2,后者等待前者的执行结果。

1
2
3
4
5
6
7
8
9
10
 f1();
 f2();如果f1是一个很耗时的任务,可以考虑把f2写成f1的回调函数。
 function f1(callback){
    setTimeout(function () {
      // f1的任务代码
      callback();
    }, 1000);
  }执行代码就变成下面这样:
 
 f1(f2);

采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行。回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱,而且每个任务只能指定一个回调函数。

二、事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)。

1
2
3
4
5
6
7
  f1.on('done', f2);上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:
  function f1(){
    setTimeout(function () {
      // f1的任务代码
      f1.trigger('done');
    }, 1000);
  }

f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2。这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合”,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

三、Promises对象

Promise 是异步编程的一种解决方案,比传统的解决方案“回调函数”和“事件”——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。基本用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});
// 下面列出异步操作失败、抓捕异常的另一种写法
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});

这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,可以实现许多强大的功能。比如,指定多个回调函数等等。

四、async/await

使用最新版本的Node已经可以原生支持async/await写法了,通过各种pollyfill也能在旧的浏览器使用。那么为什么说async/await方法是最优雅的呢?且看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function fn1 () {
console.log('Function 1')
}
function fn2 () {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 2')
resolve()
}, 500)
})
}
function fn3 () {
console.log('Function 3')
}
async function asyncFunArr () {
fn1()
await fn2()
fn3()
}
asyncFunArr()
// output =>
// Function 1
// Function 2
// Function 3

有没有发现,在定义异步函数fn2的时候,其内容和前文使用Promise的时候一模一样?再看执行函数asyncFunArr(),其执行的方式和使用generator的时候也非常类似。

异步的操作都返回Promise,需要顺序执行时只需要await相应的函数即可,这种方式在语义化方面非常友好,对于代码的维护也很简单——只需要返回Promise并await它就好,无需像generator那般需要自己去维护内部yield的执行。