Fork me on GitHub

promise 的那些事

1. promise 的由来

1.1 异步

我们都知道 js 是单线程的,也就是说一次只能完成一件任务。如果有多个任务,就必须排队,等待前面一个任务完成,再执行后面一个任务。

这种方式虽然实现起来比较简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 js 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,js 语言将任务的执行模式分成两种:同步和异步(异步任务不具有”堵塞“效应)。

“异步模式”非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是 Ajax 操作。在服务器端,”异步模式”甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有 http 请求,服务器性能会急剧下降,很快就会失去响应。

1.2 处理异步的几种方式

  • 使用回调函数
1
2
3
4
5
6
7
8
9
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})

这是几年前的方法,我们可以看出这样写简单、好实现,但是很容易写出回调地狱,如果嵌套很深,维护人员会很痛苦,此外它不能使用 try catch 捕获错误,不能直接 return。

  • 事件监听
1
2
3
4
5
6
7
8
9

func.on('done', func2);

function func() {
setTimeout(function () {
// ...
func.trigger('done');
}, 1000);
}

这种方法可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合”,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。

  • 发布订阅
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
let fs = require('fs'); // fileSystem
// 希望两次都完成 后 分别打印最终结果 在打印一次已经处理完毕
// 发布 emit 订阅 on 一种一对多的关系 [fn,fn,fn]
class Events {
constructor() { this.stack = []; }
on(callback) { this.stack.push(callback); }
emit() { this.stack.forEach(callback => callback()) }
}

let events = new Events();
let school = {};
events.on(function () {
if (Object.keys(school).length === 2) {
console.log(school)
}
})
events.on(function () {
console.log('当前获取完毕')
})

// 前端 服务端 好多原理都是基于发布订阅模式的
fs.readFile('./javascript/promise/name.txt','utf8',function(err,data){ // 5s
school.name = data;
events.emit();
});
fs.readFile('./javascript/promise/name.txt','utf8',function(err,data){ // 3s
school.age = data;
events.emit();
});

这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

2. promise 介绍

中文网:https://es6.ruanyifeng.com/#docs/promise

promise 本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。符合 promise A+ 规范, 按照这个规范可以自己实现一个 promise.

promise 解决的问题:

  • 回调嵌套,回调地狱
  • 错误捕获不好处理错误
  • 多个异步同步的问题 Promise.all

promise 的几种状态

  • Pending—-Promise对象实例创建时候的初始状态
  • Fulfilled—-可以理解为成功的状态
  • Rejected—-可以理解为失败的状态

promise 一些特性:

  • 只有等待态 才能变成 成功 / 失败
  • 如果状态变化后不能在修改状态
  • promise中会存放两个变量, value 和 reason
    promise 的实例上,会有 then 方法
1
2
3
4
5
6
7
8
9
10
11
12
// 默认是等待态,可以转化成成功或者失败,状态更改后不能再更改状态
let promise = new Promise((resolve, reject) => {
resolve(123);
reject(222);
});
console.log(promise)

promise.then((value) => { // fulfilled
console.log('成功',value);
},(reason) => { // rejected
console.log('失败',reason);
});

3. promise 链式调用

  • 每次调用返回的都是一个新的 promise 实例(这就是 then 可用链式调用的原因)
  • 如果 then 中返回的是一个结果的话会把这个结果传递下一次 then 中的成功回调
  • 如果 then 中出现异常,会走下一个 then 的失败回调
  • 在 then 中使用了return,那么 return 的值会被Promise.resolve() 包装
  • then 中可以不传递参数,如果不传递会透到下一个 then 中
  • catch 会捕获到没有捕获的异常
  • finally 是 es9 的,不管成功或者失败都会走,不会中断运行,只是传递一个一定会执行的函数而已
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
let Promise = require('./promise');
let fs = require('fs');

function read(url) {
return new Promise((resolve, reject)=>{
fs.readFile(url, 'utf8', (err, data)=>{
if(err) reject(err);
resolve(data);
})
})
}

read('./javascript/promise/name.txt')
.then(data => {
throw new Error('出错了')
})
.then(data => {
console.log(data);
},err=>{
console.log('err', err);
})
.then(data => {
console.log(data);
})
// .catch(err => {
// console.log('catch');
// })
.then(data => {
console.log(data);
})
// .finally(() => {
// console.log('finally');
// })

4. promise 方法

  • Promise.resolve() 将现有对象转为 Promise 对象
1
2
3
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
  • Promise.reject(reason) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected
1
2
3
4
5
6
7
8
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
console.log(s)
});
// 出错了
  • Promise.all() 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例, 常用于无关联多接口请求处理 promise.all 执行时,若有一个reject, 边终止。
1
const p = Promise.all([p1, p2, p3]);
  • Promise.race() 只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数
1
const p = Promise.race([p1, p2, p3]);
  • Promise.allsettled() es2020 新增方法,用来解决 promise.all 执行时,若有一个 reject, 边终止的情况,还是会返回执行结果

  • Promise.any() 只要参数实例有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态

5. 生成器 Generators/ yield

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。

koa 1.0 在使用, koa2 废弃调了,使用 async + await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 生成器 -> 迭代器
// 可以暂停执行 * 表示是一个生成器函数 yield 产出
function* read() {
yield 1;
yield 2;
yield 3;
}

// generator 返回的是生成器,生成器有一个 next 方法,调用这个方法,会返回一个对象,对象done,是否迭代完成,value 产出的结果
let iterator = read();
console.log(iterator);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

// 打印结果
Object [Generator] {}
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }

这个地方要提下什么是类数组

类数组 => 可以被迭代

也可以用 Array.prototype.slice.call(arrayLike) 来将类数组转化成数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let arr = [...{
0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator]: function () {
let index = 0;
return {
next() {
return { done: this.length != index, value: this[index++] }
}
}
}
}];

let arr = [...{
0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator]: function* () {
let index = 0;
while (index != this.length) {
yield this[index++];
}
}
}];

使用例子如下:

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
function* read() {
// try{
let content = yield fs.readFile('./name.txt', 'utf8');
let age = yield fs.readFile(content, 'utf8');
let a = yield age + 100;
return a;
// }catch(err){
// console.log(err);
// }
}

let it = read();
let { value, done } = it.next();
value.then(function (data) {
let { value, done } = it.next(data);
value.then(function (data) {
let { value, done } = it.next(data);
value.then(function (data) {
let { value, done } = it.next(data);
console.log(value);
})
})
}, function (err) {
it.throw(err);
})

可以看出嵌套很深,老问题,我们常用 co 库来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function co(it) {
return new Promise((resolve, reject) => {
// 异步迭代 next
function next(data) { // 如果碰到异步迭代,需要借助一个自执行函数来实现,保证第一次执行后调用下一次执行
let { value, done } = it.next(data);
if (!done) {
Promise.resolve(value).then(data => {
next(data)
}, reject);
} else {
resolve(value);
}
}
next();
});
}
co(read()).then(data => {
console.log(data);
});

6. 终极方法 async + await

async + await 可以理解为 generator + co 语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
function readAll() {
read1()
read2() // 这个函数同步执行
}
async function read1() {
let r = await read('1.txt','utf8')
console.log(r)
}
async function read2() {
let r = await read('2.txt','utf8')
console.log(r)
}
readAll() // 2.txt 3.txt

所以归纳一下 js 异步编程进化史:callback -> promise -> generator -> async + await

7. 如何自己实现一个 promise

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
151
152
153
154
155
156
157
158
159
160
161
162
function resolvePromise(promise2, x, resolve, reject) {
// 判断x的类型 来处理 promise2 是成功还是失败
if (promise2 === x) {
return reject(new TypeError('TypeError: Chaining cycle detected for promise #<Promise>'));
}
let called;
if (typeof x === 'function' || (typeof x === 'object' && x != null)) {
try {
let then = x.then; // then 可能是getter object.defineProperty
if (typeof then === 'function') { // {then:null} 就认为他是一个promise
then.call(x, y => { // 让当前的promise 执行,不用多次取 then 方法了
if (called) return; // 1)
called = true;
resolvePromise(promise2, y, resolve, reject);
}, r => {
if (called) return; // 2)
called = true;
reject(r);
})
} else {
resolve(x);
}
} catch (e) {
if (called) return; // 3) 为了辨别这个promise 不能调用多次
called = true;
reject(e);
}
} else {
resolve(x);
}
}

class Promise {
constructor(executor) {
this.status = 'pending'; // 默认当前状态是等待态
this.value;
this.reason;
this.onResolvedCallbacks = []; // 存储成功的所有的回调 只有pending的时候才存储
this.onRejectedCallbacks = []; // 存储所有失败的
let resolve = (value) => {
if (this.status === 'pending') { // 只有等待态的时候才能更改数据
this.status = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
}
// 默认会调用执行函数
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}

then(onFulfilled, onRejected) {
// onFulfilled onRejected 是 可选参数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
let promise2;
// 可以不停的调用then方法,必须返还一个新的promise
// 异步的特点 等待当前主栈代码都执行后才执行
promise2 = new Promise((resolve, reject) => {
if (this.status === 'fulfilled') {
setTimeout(() => { // 为了保证Promise2 存在
try {
// 需要对 then 的成功的回调 和失败的回调 取到他的返回结果, 如果是普通值就让promise2成功
let x = onFulfilled(this.value);
// 对 x 的类型做 判断,常量可以直接抛出来,但是如果是promise 需要采用当前promise的状态
// x 如果是普通值,直接调用Promise2 的resolve
// x 如果是promsie, 让x这个promise执行 x.then
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === 'rejected') {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === 'pending') {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
})
}
})
return promise2;
}
catch(rejectFunc) { // 用来捕获错误 , 语法糖
return this.then(null, rejectFunc);
}
}

// 暴露一个方法, 这个方法需要返回一个对象,对象上需要有 promise resolve reject 三个属性
// 希望测试一下这个库是否符合我们的promise A+规范 https://promisesaplus.com/
// promises-aplus-tests 文件名

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

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

Promise.prototype.finally = function (callback) {
return this.then((data) => {
return Promise.resolve(callback()).then(() => data);
// return new Promise((resolve,reject)=>{
// resolve(callback()); // 如果callback是一个函数返回promise 就等待这个promise执行完毕
// }).then(()=>data);
// callback();
// return data;
}, (err) => {
return Promise.resolve(callback()).then(() => { throw err }); // koa 原理
// throw err;
});
};

module.exports = Promise;

上面是我简单实现的 promise

如何测试该 promise 是否合格,可参考promise test

8. 其他工具

  • bbluebird 第三方库 实现 promise 化
1
2
3
4
5
6
// node 中已经借鉴了,所有的异步方法参数第一个都是err
let read = bluebird.promisify(fs.readFile);

read('./javascript/promise/name.txt', 'utf8', function (err, data) {
console.log(data);
});

下面自己实现一个 promisefy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promisify = (fn) => (...args) => {
return new Promise((resolve, reject) => {
//fs.readFile(path.resolve(__dirname,'1.txt'),'utf8',)
fn(...args, (err, data) => {
if (err) { return reject(err) }
resolve(data);
})
})
}

const promisifyAll = (obj) => {
for (let key in obj) {
obj[key] = promisify(obj[key]);
}
}
  • mz 第三方包 他里面把所有的 node 模块, 都进行了包装promise
1
2
3
4
let fs = require('mz/fs');
fs.readFile('name.txt', 'utf8').then(data => {

});
-------------本文结束感谢您的阅读-------------