ES6之promise、generator+co、async+await手写promise

一、前言

传统的解决代码单线程执行的方案是回调函数和事件。这是个解决问题的方案,但是会造成回调地狱。

异步编程是优化代码逻辑提高代码易读性的关键。

目前通用的异步编程方法有三种:

  1. Promise
  2. generator+co
  3. async+await

这三种方法我都经常在用,但是对它们的原理却一知半解。于是想炒个冷饭从头到尾理一遍,梳理一下它们之间的关系。

二、Promise

2.1 原理

Promise对象是一个构造函数,用来生成Promise实例。
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
Promise函数的两个参数分别是resolvereject。它们是Promise中定义的两个函数,在运行自定义函数时返回。
resolve函数将Promise对象的状态从 pending 变为resolved,reject将Promise对象的状态从 pending 变为rejected
Promise的原型链上定义了then方法,提供两个回调函数分别捕获resolve、reject返回的值。

2.2 静态方法

方法描述Promise.resolve(promise);返回 promise(仅当 promise.constructor == Promise 时)Promise.resolve(thenable);
thenable 中生成一个新promisethenable 是具有 then() 方法的类似于 promise 的对象。Promise.resolve(obj);
在此情况下,生成一个promise 并在执行时返回 obj。Promise.reject(obj);生成一个promise 并在拒绝时返回 obj。
为保持一致和调试之目的(例如堆叠追踪), obj 应为 instanceof Error
Promise.all(array);生成一个promise
该 promise 在数组中各项执行时执行,在任意一项拒绝时拒绝。Promise.race(array);
生成一个 Promise,该 Promise 在任意项执行时执行,或在任意项拒绝时拒绝,以最先发生的为准。
sample 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let p1 = new Promise((resolve,reject)=>{
console.log('hello')
setTimeout(function () {
reject('1212')
},1000)
})

p1.then(data=> {
console.log('success'+data)
},err=>{
console.log('err'+err)
})

p1.then(data=> {
console.log('success'+data)
},err=>{
console.log('err'+err)
})

terminal:

1
2
3
hello
err1212
err1212

sample 1 中新建了一个Promise实例,定时1S后使用reject方法,将Promise实例的状态从pending变成rejected,触发then的err捕捉回调函数。

在sample 1 中调用then方法,并不会马上执行回调。是等待实例中状态改变后才会执行。这一点和发布订阅模式很类似。

sample 2

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
let fs = require('fs')

let event = {
arr:[],
result:[],
on(fn){
this.arr.push(fn)
},
emit(data){
this.result.push(data)
this.arr.forEach(fn=>fn(this.result))
}
}

event.on(function (data) {
if(data.length === 2){
console.log(data)
}
})

fs.readFile('1.txt','utf8',function (err,data) {
event.emit(data)
})
fs.readFile('2.txt','utf8',function (err,data) {
event.emit(data)
})

smaple2 中将结果data放入暂存数组中,在执行接听函数的时候返回。

2.3 简写Promise源码

通过之前的例子和对发布订阅模式的理解,我们可以大概写出Promise实例的基本功能:

code 1:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function Promise(executor) {
let self = this
self.value = undefined
self.reason = undefined
self.status = 'pending'
self.onResovedCallbacks = []
self.onRejectedCallbacks = []
function resolve(data) {
if(self.status === 'pending'){
self.value = data
self.status = 'resolved'
self.onResovedCallbacks.forEach(fn=>fn())
}
}
function reject(reason) {
if(self.status === 'pending') {
self.reason = reason
self.status = 'reject'
self.onRejectedCallbacks.forEach(fn=>fn())
}
}
//如果函数执行时发生异常
try{
executor(resolve,reject)
}catch (e){
reject(e)
}
}

Promise.prototype.then = function (onFulfilled,onRejected) {
let self = this
if(self.status === 'pending'){
self.onResovedCallbacks.push(()=>{
onFulfilled(self.value)
})
self.onRejectedCallbacks.push(()=>{
onRejected(self.reason)
})
}elseif(self.status === 'resolved'){
onFulfilled(self.value)
}elseif(self.status === 'reject'){
onRejected(self.reason)
}

}

module.exports = Promise

  • 函数内部变量

  • status:储存Promise的状态

  • onResovedCallbacks:储存Promise pending状态下成功回调函数
  • onRejectedCallbacks:储存Promise pending状态下失败回调函数
  • resolve函数
  • reject函数

  • Promise.prototype.then

  • 根据实例状态执行响应的回调

  • status == pending使用发布订阅模式储存回调函数。
2.4 Promise用法简述
  1. 如果一个promise执行完后,返回的还是一个Promise对象,会把这个promise的执行结果,传递给下一个then中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let fs = require('fs')

    functionread(filePath,encoding) {
    return new Promise((resolve,reject)=>{
    fs.readFile(filePath,encoding,(err,data)=> {
    if(err) reject(err)
    resolve(data)
    })
    })
    }

    read('1.txt','utf8').then(
    f1=>read(f1,'utf8') // 1
    ).then(
    data=> console.log('resolved:',comments)
    err=> console.log('rejected: ',err)
    )
  2. 如果then中返回的不是promise,是一个普通值,会将这个普通值作为下一个then的返回结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     ......

    read('1.txt','utf8').then(
    f1=>read(f1,'utf8')
    ).then(
    return 123 //2
    ).then(
    data=> console.log('resolved:',comments)
    err=> console.log('rejected: ',err)
    )
  3. 如果当前then中失败了会走下一个then的失败。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     ......

    read('1.txt','utf8').then(
    f1=>read(f1,'utf8')
    ).then(
    return 123
    ).then(
    throw new Error('出错') //3
    ).then(
    data=> console.log('resolved:',comments)
    err=> console.log('rejected: ',err)
    )
  4. 如果返回的是undefined不管当前是失败还是成功,都会走下一次成功。

  5. catch是错误没有处理的情况下会走。
  6. then中可以不写。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     ......

    read('1.txt','utf8').then(
    f1=>read(f1,'utf8')
    ).then(
    return 123
    ).then(
    throw new Error('出错')
    ).then() //6
    .then(
    data=> console.log('resolved:',comments)
    err=> console.log('rejected: ',err)
    )

这些用法中最重要的是promise的then链式调用。
可以大致猜到,旧Promise的then方法返回的是一个新的Promise对象。

参考Promises/A+规范,可以完善手写的Promise源码使其支持promise的静态方法和调用规则。

code 2:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
function Promise(executor) {
let self = this
self.value = undefined
self.reason = undefined
self.status = 'pending'
self.onResovedCallbacks = []
self.onRejectedCallbacks = []
function resolve(value) {
if (self.status === 'pending') {
self.value = value
self.status = 'resolved'
self.onResovedCallbacks.forEach(fn=>fn())
}
}

function reject(reason) {
if (self.status === 'pending') {
self.reason = reason
self.status = 'rejected'
self.onRejectedCallbacks.forEach(fn=>fn())
}
}

//如果函数执行时发生异常
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}


function resolvePromise(promise2, x, resolve, reject) {
//If promise and x refer to the same object, reject promise with a TypeError as the reason.
if (promise2 === x) {
return reject(new TypeError('chaining cycle'))
}
let called
//2.3.3.Otherwise, if x is an object or function,
if (x !== null && (typeof x == 'object' || typeof x === 'function')) {
try {
letthen = x.then
//2.3.3.3.If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where:
//2.3.3.3.3.If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
if (typeof then === 'function') {
then.call(x, y=> {
if (called) return;
called = true;
//递归直到解析成普通值为止
//2.3.3.1.If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
resolvePromise(promise2, y, resolve, reject)
}, err=> {
if (called) return;
called = true;
reject(err)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return;
called = true;
//2.3.3.3.If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
reject(e)
}
} else {
//If x is not an object or function, fulfill promise with x.
resolve(x)
}
}
//then调用的时候 都是异步调用 (原生的then的成功或者失败 是一个微任务)
Promise.prototype.then = function (onFulfilled, onRejected) {
//成功和失败的函数 是可选参数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val=>val;
onRejected = typeof onRejected === 'function' ? onRejected : (e)=> {throw e};
let self = this
let promise2;
promise2 = new Promise((resolve, reject)=> {
if (self.status === 'resolved') {
setTimeout(()=> {
try {
let x = onFulfilled(self.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
} elseif (self.status === 'rejected') {
setTimeout(()=> {
try {
let x = onRejected(self.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
} elseif (self.status === 'pending') {
self.onResovedCallbacks.push(()=> {
setTimeout(()=> {
try {
let x = onFulfilled(self.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
//当执行成功回调的时候,可能会出现异常,那就用这个异常作为promise2的错误结果
reject(e)
}
}, 0)
})
self.onRejectedCallbacks.push(()=> {
setTimeout(()=> {
try {
let x = onRejected(self.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise2
}
//setTimeout (规范要求)

Promise.reject = function (reason) {
return new Promise((resolve,reject)=>{
reject(reason)
})
}

Promise.resolve = function (value) {
return new Promise((resolve,reject)=>{
resolve(value)
})
}

Promise.prototype.catch = function (onReject) {
return this.then(null,onReject)
}

Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve, reject)=> {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd;
}

module.exports = Promise

  1. 为了支持then的链式调用,Promise.then.prototype中返回一个新的Promise对象
    1
    return p2 = new Promise()

2.增加resolvePromise方法,处理旧Promise的回调函数的结果x,根据x的类型,分别调用新promise对象的resolve/reject方法。

  • 是普通值用resolve方法返回
  • 是函数或者对象就继续用resolvePromise方法迭代(解决回调函数是Promise对象)
  • 出错就用reject方法返回

三、bluebird

1: NodeJS 中的 fs.readFile 方法的基本使用方式

1
2
3
4
5
6
7
8
9
const fs = require('fs'),path = require('path');

fs.readFile(path.join(__dirname, '1.txt'), 'utf-8', (err, data) => {
if (err) {
console.error(err);
} else {
console.log(data);
}
});

2:使用Promise封装

1
2
3
4
5
6
7
8
9
10
11
12
let fs = require('fs')

functionread(filePath, encoding) {
return new Promise((resolve, reject)=> {
fs.readFile(filePath, encoding, (err, data)=> {
if (err) reject(err)
resolve(data)
})
})
}

read('1.txt', 'utf8').then( data=> data)

把fs.readFile方法用Promise封装一下就能使用Promise api。但是每次手动封装比较麻烦,bluebird可以帮我们简化这个步骤。

3:在 NodeJS 环境中,通过 const bluebird = require(‘bluebird’) 就可以开始使用 Bluebird 提供的 Promise 对象。

Promise.promisify 将单个方法转换成Promise对象。

1
2
3
4
5
const bluebird = require('bluebird') 
letread = bluebird.promisify(fs.readFile)
read('1.txt', 'utf-8').then(data=> {
console.log('data promisify', data)
})

使用bluebird.promisify方法,就能将fs.readFile直接封装成一个promise对象,它的原理很简单,return new Promise
是它的核心:

1
2
3
4
5
6
7
8
9
10
function promisify(fn) {
return function () {
return new Promise((resolve, reject)=> {
fn(...arguments, function (err, data) {
if (err) reject(err)
resolve(data)
})
})
}
}

4.使用 Promise.promisifyAll 把一个对象的所有方法都自动转换成使用 Promise。

1
2
3
4
5
6
7
8
const bluebird = require('bluebird'),
fs = require('fs'),
path = require('path');
Promise.promisifyAll(fs);

fs.readFileAsync(path.join(__dirname, 'sample.txt'), 'utf-8')
.then(data => console.log(data))
.catch(err => console.error(err));

promisifyAll核心是遍历对象,生成些新创建方法的名称在已有方法的名称后加上”Async”后缀。

1
2
3
4
5
6
7
function promisifyAll(obj) {
Object.keys(obj).forEach(key=>{
if(typeof obj[key] === 'function'){
obj[key+'Async'] = promisify(obj[key])
}
})
}

四、generator+co

4.1 简介

generator函数最大的特点是可以用yield暂停执行,为了区别普通函数在函数名前加*号。

1
2
3
4
5
6
7
8
9
function *say() {
let a = yield "test1"let b = yield "test2"
}

let it = say();

console.log(1, it.next()) //1 { value: 'test1', done: false }
console.log(2, it.next()) //2 { value: 'test2', done: false }
console.log(3, it.next()) //3 { value: undefined, done: true }

执行say()方法返回的是指针对象,不会返回函数执行结果。it 就是iterator 迭代器

需要调用指针对象的next()方法,让函数指针不断移动并返回一个对象。({value:xxx,done:xxx})

value是yield后面的值,done表示函数是否执行完成。

我们可以用generator函数实现结果的产出,但是也需要它支持输入。

generator函数的运行顺序如下:
使用it.next()执行函数,结果并不会返回给定义的变量a。next方法可以接受参数,这是向 Generator 函数体内输入数据。
第二个next的时候传入参数,就能被变量a接收到。

terminal 返回:

1
2
3
4
5
1 { value: 'test1', done: false }
aaa
2 { value: 'test2', done: false }
bbb
3 { value: undefined, done: true }

4.2 使用

example:使用generator异步执行函数,使函数的返回作为下一个函数的入参执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
let bluebird = require('bluebird')
let fs = require('fs')
letread = bluebird.promisify(fs.readFile)

function *r() {
let r1 = yield read('1.txt', 'utf-8')
console.log('r1',r1); // r1 2.txt
let r2 = yield read(r1, 'utf-8')
console.log('r2',r2); // r2 3.txt
let r3 = yield read(r2, 'utf-8')
console.log('r3',r3); // r3 hello
return r3
}

拿读取文件的例子:使用bluebird将fs.readFile变成promise对象,将读取到的文件内容作为入参传入下一个要执行的函数。

突然发现,要拿到结果会是个复杂的过程,但还是硬着头皮下下去:

1
2
3
4
5
6
7
8
9
10
const it_r = r()
it_r.next().value.then(d1=>{
return it_r.next(d1).value
}).then(d2=>{
return it_r.next(d2).value
}).then(d3=>{
return it_r.next(d3).value
}).then(data=>{
console.log(data) // hello
})

it.next().value 返回的是一个promise,使用then方法,拿到它成功回调的值,并传入下一个next。

这样能成功拿到我们要的值,但是太麻烦了。于是就有了generator+co的组合!

安装:

1
$ npm install co

使用:

1
2
3
co(r()).then(data=> {
console.log(data)
})

co会迭代执行it.next()方法,直到done的布尔值为true就返回generator函数的运行结果。

大致执行代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function co(it) {
return new Promise((resolve, reject)=> {
function next(data) {
let {value, done} = it.next(data)
if(done){
resolve(value)
}else{
value.then(data=> {
next(data)
},reject)
}
}
next()
})
}

五、async+await

async 函数是Generator 函数的语法糖。

比Generator函数用起来简单

  1. 可以让代码像同步
  2. 可以try+catch
  3. 可以使用promise api
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    async functionr() {
    try{
    let r1 = await read('1.txt','utf8')
    let r2 = await read(r1,'utf8')
    let r3 = await read(r2,'utf8')
    return r3
    }catch(e){
    console.log('e',e)
    }
    }

    r().then(data=> {
    console.log(data)
    },err=>{
    console.log('err',err)
    })

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。遇到await就会先返回,等待函数执行。