我们可以先看下异步解决方案的进阶历史:
回调函数 ---> 事件监听 ---> 发布/订阅 --->
Promise
--->Generator
--->async/await
我们按顺序看下这些解决方案的优缺点,进阶的必要性。着重说一下 Promise
Generator
async/await
回调函数
优点:
简单
缺点:
回调多的情况下容易产生回调地狱
示例
// normal
fs.readFile(xxx, 'utf-8', function(err, data) {
// code
});
// callback hell
fs.readFile(A, 'utf-8', function(err, data) {
fs.readFile(B, 'utf-8', function(err, data) {
fs.readFile(C, 'utf-8', function(err, data) {
fs.readFile(D, 'utf-8', function(err, data) {
//....
});
});
});
});
事件监听
优点
可以绑定多个事件,每个事件可以指定多个回调函数
缺点
整个流程都要变成事件驱动型,运行流程会变得不清晰
示例
f1.on('done', f2);
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}
发布/订阅
优点
我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行
缺点
写法依然不直观
示例
jQuery.subscribe("done", f2);
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
jQuery.unsubscribe("done", f2);
Promise
Promise
最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise
对象。
特点
- 对象的状态不受外界影响。
Promise
对象代表一个异步操作,有三种状态:pending
、fulfilled
和rejected
。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。
优点
- 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
Promise
对象提供统一的接口,使得控制异步操作更加容易
缺点
- 无法取消 Promise
- 当处于 pending 状态时,无法得知目前进展到哪一个阶段(待详解)
示例
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
read(A).then(data => {
return read(B);
}).then(data => {
return read(C);
}).then(data => {
return read(D);
}).catch(reason => {
console.log(reason);
});
无疑,Promise
的实现对于异步处理来说是一个巨大的进步,那么其原理是什么呢?
我们可以先思考一下如果自己实现一个 Promise
的对象,应该考虑哪些方面:
- 状态变更,
PENDING
/RESLOVED
/REJECTED
- 参数是一个立即执行函数,它接收
resolve
和reject
两个回调方法 - then 的链式调用及状态问题
- 存储成功和失败回调函数列表
- ...
原理
我们可以手动实现一下:
// 判断变量否为function
const isFunction = variable => typeof variable === 'function'
// 定义Promise的三种状态常量
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
export class MyPromise {
constructor(handle) {
// 非函数直接报错
if (!isFunction(handle)) {
throw new Error('MyPromise must accept a function as a parameter')
}
// 添加状态
this._status = PENDING
// 接收数据变量
this._value = undefined
// 添加成功回调函数队列
this._fulfilledQueues = []
// 添加失败回调函数队列
this._rejectedQueues = []
// 执行handle
try {
handle(this._resolve.bind(this), this._reject.bind(this))
} catch (err) {
this._reject(err)
}
}
// 添加resovle时执行的函数
_resolve(val) {
console.log('MyPromise -> _resolve -> _resolve', val)
const run = () => {
// 改变状态之后不可变,所以不再执行
if (this._status !== PENDING) return
// 依次执行成功队列中的函数,并清空队列
const runFulfilled = value => {
console.log('MyPromise -> runFulfilled -> runFulfilled', value)
let cb
while ((cb = this._fulfilledQueues.shift())) {
cb(value)
}
}
// 依次执行失败队列中的函数,并清空队列
const runRejected = error => {
console.log('MyPromise -> runRejected -> runRejected', error)
let cb
while ((cb = this._rejectedQueues.shift())) {
cb(error)
}
}
/* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
*/
if (val instanceof MyPromise) {
console.log('MyPromise -> run -> MyPromise', ' val is MyPromise')
val.then(
value => {
this._value = value
this._status = FULFILLED
runFulfilled(value)
},
err => {
this._value = err
this._status = REJECTED
runRejected(err)
}
)
} else {
console.log('MyPromise -> run -> MyPromise', ' val is not MyPromise')
this._value = val
this._status = FULFILLED
runFulfilled(val)
}
}
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0)
}
// 添加reject时执行的函数
_reject(err) {
console.log('MyPromise -> _reject -> _reject', err)
if (this._status !== PENDING) return
// 依次执行失败队列中的函数,并清空队列
const run = () => {
this._status = REJECTED
this._value = err
let cb
while ((cb = this._rejectedQueues.shift())) {
cb(err)
}
}
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0)
}
// 添加then方法
then(onFulfilled, onRejected) {
console.log('MyPromise -> then -> onFulfilled', onFulfilled)
console.log('MyPromise -> then -> onRejected', onRejected)
const { _value, _status } = this
// 返回一个新的Promise对象
return new MyPromise((onFulfilledNext, onRejectedNext) => {
console.log('MyPromise -> then -> onFulfilledNext', onFulfilledNext)
console.log('MyPromise -> then -> onRejectedNext', onRejectedNext)
// 封装一个成功时执行的函数
let fulfilled = value => {
try {
if (!isFunction(onFulfilled)) {
onFulfilledNext(value)
} else {
let res = onFulfilled(value)
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
// 封装一个失败时执行的函数
let rejected = error => {
try {
if (!isFunction(onRejected)) {
onRejectedNext(error)
} else {
let res = onRejected(error)
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
switch (_status) {
// 当状态为pending时,将then方法回调函数加入执行队列等待执行
case PENDING:
this._fulfilledQueues.push(fulfilled)
console.log('MyPromise -> then -> _fulfilledQueues', this._fulfilledQueues)
this._rejectedQueues.push(rejected)
console.log('MyPromise -> then -> _rejectedQueues', this._rejectedQueues)
break
// 当状态已经改变时,立即执行对应的回调函数
case FULFILLED:
fulfilled(_value)
break
case REJECTED:
rejected(_value)
break
}
})
}
// 添加catch方法
catch(onRejected) {
console.log('MyPromise -> catch')
return this.then(undefined, onRejected)
}
// 无论状态,最后执行方法
finally(cb) {
console.log('MyPromise -> finally -> finally')
return this.then(
value => MyPromise.resolve(cb()).then(() => value),
reason =>
MyPromise.resolve(cb()).then(() => {
throw reason
})
)
}
}
那么 Promise
有没有改变 callback
的本质?并没有,Promise
只是换了种对异步的写法,优化了对代码的可读性,从实现来看还是依赖 callback
获得的数据,还是在 then
的 callback
里获取到的,还没有真正像同步那样的写法。
真正的同步写法,要看下面的 generator
。
Generator
Generator
函数是 ES6
提供的一种异步编程解决方案。
特点:
- 以
function*
开始,注意这个*
- 内部有一个
yield
关键字,跟return
有点像,不同是yield
可以有多个 - 最突出的特点:可以交出执行权,暂停执行
优点
- 同步写法简单明了
- 特殊命名便于区分
缺点
- 多个任务要写多个
next()
,需要自己手动加上自动执行
示例
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
可以通过不断地执行 next()
方法,可以改变当前函数的状态,其原理是什么呢?
这里就需要引申出一个迭代器的概念。
Iterator迭代器
它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator
接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator
的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的 next
方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的 next
方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的 next
方法,直到它指向数据结构的结束位置。
而 Generator
对象本身就具有 Symbol.iterator
的属性,所以执行 Generator
函数就会返回一个遍历器对象,可以依次遍历 Generator
函数内部的每一个状态。
我们可以通过不断地执行 next()
获取内部状态的改变,但是当状态很多的时候,我们还要手动去执行?显然是不合理的,所以 Generator
函数一般要搭配自动执行一起使用。
generator
自动执行
我们先来看一个最简单的自动执行
function* gen() {
// ...
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
但是,这不适合异步操作。如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。
所以我们在这个时候就要用 yield
命令将程序的执行权移出 Generator
函数,然后再这一种方法,将执行权再交还给 Generator
函数。这种方法有两种:
Thunk函数
co模块
Thunk函数
我们先简单了解下什么 Thunk函数
。
编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做
Thunk 函数
。
JavaScript 语言的 Thunk 函数
有所不同。
JavaScript 语言是传值调用,它的 Thunk 函数
含义有所不同。在 JavaScript 语言中,Thunk 函数
替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
上面代码中,fs
模块的 readFile
方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk 函数
。
我们可以使用已经封装好的第三方插件 thunkify
模块,比较便捷。
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
Generator
和 Thunk 函数
搭配使用就可以达到自动流程管理。
var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
};
var g = gen();
var r1 = g.next();
r1.value(function (err, data) {
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
上面的代码我们可以发现 Generator
函数的执行过程,其实是将同一个回调函数,反复传入 next
方法的 value
属性。这使得我们可以用递归来自动完成这个过程。
// 执行器
function run(generator) {
const gen = generator();
// 这个next其实就是Thunk函数的回调函数
function next(err, data) {
const res = gen.next(data); // 类似{value: Thunk函数, done: false}
if (res.done) {
return;
}
res.value(next); // res.value是一个Thunk函数,而参数next就是一个callback
}
next();
}
const gen = function* () {
var f1 = yield readFileThunk('fileA');
var f2 = yield readFileThunk('fileB');
// ...
var fn = yield readFileThunk('fileN');
}
// run执行Generator函数
run(gen);
Thunk 函数
并不是 Generator 函数自动执行的唯一方案。下面的 co模块
会更加便捷。
co模块
var co = require('co');
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
co(gen);
// 或者
co(gen).then(function (){
console.log('Generator 函数执行完成');
});
co 模块
的原理
为什么 co
可以自动执行 Generator
函数?
前面说过,Generator
就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点。
-
回调函数。将异步操作包装成
Thunk 函数
,在回调函数里面交回执行权。 -
Promise
对象。将异步操作包装成Promise
对象,用then
方法交回执行权。
co 模块其实就是将两种自动执行器(Thunk 函数
和 Promise
对象),包装成一个模块。使用 co 的前提条件是,Generator
函数的yield命令后面,只能是 Thunk 函数
或 Promise
对象。
除了co模块,Promise也可以实现自动执行,可参考Generator 函数的异步应用
generator
比 Promise
写法更加直观,但是自动执行需要我们自己添加,ES7在这个问题的基础上,提出了 async/await
方法,很多人认为它是异步操作的终极解决方案。
async/await
ES2017
标准引入了async
函数,使得异步操作变得更加方便。
async
函数是什么?一句话,它就是 Generator 函数的语法糖。
优点
asyncFn
函数对 Generator
函数的改进,体现在以下四点。
- 内置执行器
- 更好的语义
- 更广的适用性
- 返回值是 Promise
缺点
await
将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了await
会导致性能上的降低
Tip:
await
命令后面是一个Promise
对象,返回该对象的结果。如果不是Promise
对象,就直接返回对应的值。
另一种情况是,await
命令后面是一个thenable对象(即定义then
方法的对象),那么await
会将其等同于Promise
对象。
示例
// 定义async函数,注意async关键字
const readAsync = async function() {
const data1 = await readPromise('./test1.txt'); // 注意await关键字
const data2 = await readPromise('./test2.txt');
return 'ok'; // 返回值可以在调用处通过then拿到
}
// 执行
readAsync();
// 或者
readAsync().then((data) => {
console.log(data); // 'ok'
});
底层其实就是可以理解为 Generator
和 Thunk函数
的搭配使用
// 执行器
function asyncFn(generator) {
const gen = generator();
// 这个next其实就是Thunk函数的回调函数
function next(err, data) {
const res = gen.next(data); // 类似{value: Thunk函数, done: false}
if (res.done) {
return;
}
res.value(next); // res.value是一个Thunk函数,而参数next就是一个callback
}
next();
}
const gen = function* () {
var f1 = yield readFileThunk('fileA');
var f2 = yield readFileThunk('fileB');
// ...
var fn = yield readFileThunk('fileN');
}
// run执行Generator函数
asyncFn(gen);
目前为止, async/await
被大多数人认为是最终的异步解决方案,但是未来不可知,也许会有更高级的解决方案被提出呢?