在本文开始之前,先讲一些异步的相关概念。
众所周知, JavaScript 是单线程语言,为了解决单线程会卡顿程序的问题,出现了异步编程。异步编程主要包括:
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
一、什么是异步?
所谓的异步,就是将任务分阶段进行执行,先执行第一阶段,然后转而执行其他任务,再执行第二阶段、第三阶段。举例说明,有一个任务是读取文件进行处理,异步的执行过程是:请求文件 -> 做其他任务 -> 处理文件
。这个任务的第一阶段是向操作系统发起请求,要求读取文件。然后程序去执行了其他任务,等到操作系统返回文件,再接着执行第二阶段的任务,即处理文件。
这种不连续的执行,就叫做异步。相对的,连续的执行就叫做同步。如果是同步操作的话,在系统读取文件的过程中,程序只能等待,什么都不能做。
二、回调函数
JavaScript 对于异步编程的实现就是通过回调函数。回调函数,就是将任务的第二阶段单独写在一个函数里面,等到重新执行这个任务的时候就调用这个函数,就是代码中常见的 callback 。读取文件的代码如下所示:1
2
3
4fs.readFile('/ect/xxx',(err,data)=>{
if(err) throw err;
console.log(data);
})
以上,readFile 的第二个参数就是回调。等操作系统返回了 etc/xxx
文件后,回调函数就会执行。
三、Promise
在 JavaScript 中,回调函数很好的解决了异步操作问题,但是随之而来的问题即使回调的多层嵌套。以上述代码为例,在文件读取成功后再继续读取新的文件,如果有多层嵌套,代码横向发展而不是纵向发展,很快就会乱成一团、无法管理。这种情况就是我们常说的地狱回调。
Promise 就是为了应对这种情况出现的。他不是新的语法功能,而是一种新的写法,允许将回调函数的横向加载改成纵向加载。示例代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var readFile = require('fs-readfile-promise');
readFile(fileA)
.then((data) => {
console.log(data.toString())
})
.then(()=>{
return readFile(fileB)
})
.then((data)=>{
console.llg(data.toString())
})
.catch((err)=>{
console.log(err)
})
上面代码使用了 fs-readfile-promise
模块,他的作用是返回一个 Promise 版本的 readFile 函数。Promise 提供 then 方法加载回调函数,catch 用户捕捉执行过程中抛出的错误。可以看出,Promise 只是回调函数的改进,使用 then 方法后,异步任务的两段执行看的更清楚了。
Promise 代码最大的问题是代码冗余,原来的任务被 Promise 包裹了一下,不管什么操作,一眼看上去都是一堆 then ,使得语义不清。
四、协程
在传统的编程语言中,有一种叫做协程(coroutine)的异步解决方案,意思是多个线程相互协作,完成异步任务。他的大致流程如下:
- 第一步:协程 A 开始执行
- 第二步:协程 A 执行到一半,进入暂停,执行权转移到协程 B
- 第三步:一段时间后,协程 B 交回执行权
- 第四步:协程 A 恢复执行
以上协程 A 就是异步任务,因为他是分段执行的。看代码:1
2
3
4
5function asyncJob(){
...
var f = yield readFile(fileA)
...
}
以上函数 asyncJob 就是一个协程,关键点在于 yield 命令。他表示执行到此处,执行权交给其他协程。也就是说,yield 命令是异步两个阶段的分界线。协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续执行。除去 yield 命令,其他的地方的写法跟同步方法写法基本一致。
五、Generator 函数
1、概念
Generator 函数是协程在 ES6 中的实现。最大的特点是可以交出函数的执行权,又可称为暂停执行。看代码:1
2
3
4
5
6
7
8function * gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next(); // {done:false,value:3}
g.next(); // {done:true,value:undefined}
以上,调用 Generator 函数,会返回一个内部指针(即遍历器)g。这是 Generator 不同于普通函数的另一个地方,即执行后他不会返回结果,而是返回指针对象。调用指针 g 的 next 方法,会移动内部指针,指向第一个遇到的 yield 语句。上例是执行到 x + 2 为止。
next 方法的作用时分阶段执行 Generator 函数。每次调用 next 方法,都会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值,done 属性是一个 bool 值,表示 Generator 函数是否执行完毕,是否还有下一个阶段。
2、Generator 函数数据交换和错误处理
Generator 函数可以暂停执行和恢复执行,这是它能够实现异步操作的根本原因。除此之外他还有两个特性,可作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
next 方法返回的 value 值是 Generator 函数向外输出的数据。并且 next 方法还可以接收数据:1
2
3
4
5
6
7
8function * gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next(); // {done:false,value:3}
g.next(2); // {done:true,value:2}
以上第一个 next 调用不再赘述。第二个 next 方法带有参数 2 ,这个参数可以传入 Generator 函数中,作为上个阶段异步任务的返回结果,被函数体内的变量 y 接收。
Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。1
2
3
4
5
6
7
8
9
10
11
12
13function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try ... catch
代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
六、Thunk 函数
1、什么是 Thunk 函数
编译器的”传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function f(m){
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
上面代码中,函数 f 的参数 x + 5 被一个函数替换了。凡是用到原参数的地方,对 Thunk 函数求值即可。这就是 Thunk 函数的定义,它是”传名调用”的一种实现策略,用来替换某个表达式。
七、co 函数库
八、async 函数
针对以上的内容,主要来自阮一峰大大的博客。对于大部分内容的理解,我目前只停留在文章的理论,但是在项目中并没有相关的实践。