从别人博客学到的基本的Promise实现思路

本篇记录一个较早较简单的Promise基本实现,思想来自知名前端开发人员的博客:十年踪迹的博客

Promise是用来封装异步任务的,所以这篇笔记主要解释如何把异步任务的封装这个事情,一步步地进化为Promise的形式。

先来看Promise之前的异步任务封装形式。

封装异步任务

回调函数是最简单的封装异步任务的形式。 比如下面的代码封装一个异步任务,使用了两个回调函数,可分别接收最终异步任务成功或失败时的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createAsyncTask(onSuccess, onFail) {
setTimeout(function () {
// if async task finished
if(Math.random() * 10 > 5) {
let data = {name: 'lyzg'};
onSuccess && onSuccess(data);
} else {
onFail && onFail(new Error('sth happened'));
}

}, 1000);
}

createAsyncTask(function (data) {
console.log(data);
}, function(err){
console.error(err);
});

但是有时候会遇到不适合使用回调函数的场景,比如调用createAsyncTask的位置不需要做异步的回调,可能后面其它代码调用的内部才需要回调这个。 如果把异步任务,用对象的形式封装一下就能改善这一点:

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
function AsyncTask() {
return {
_state : 0,// 记录task的状态 0 执行中 1 执行成功 2 执行失败
_whenSuccess : [],// 存放task执行成功时的回调函数
_whenFail : [],// 存放task执行失败时的回调函数
_data : undefined,// 存放task执行成功时的状态
_error : undefined, // 存放task执行失败时的错误
finish(data) {
if(this._state !== 0) return;
this._state = 1;
this._data = data;//保存成功时状态
for(let callback of this._whenSuccess) {
callback(this._data);
}
this._whenSuccess.length = 0;
},
fail(err) {
if(this._state !== 0) return;
this._state = 2;
this._error = err;//保存失败时状态
for(let callback of this._whenFail) {
callback(this._error);
}
this._whenFail.length = 0;
},
whenSuccess(onSuccess) {
if(typeof onSuccess !== 'function') return;
if(this._state === 1) {
//如果添加回调时,task已经执行完毕,则直接把执行完毕时保存的状态回调onSuccess
onSuccess(this._data);
return;
}
if(this._whenSuccess.findIndex(onSuccess) === -1) {
this._whenSuccess.push(onSuccess);
}
},
whenFail(onFail) {
if(typeof onFail !== 'function') return;
if(this._state === 2) {
//如果添加回调时,task已经执行失败,则直接把执行失败时保存的状态回调onSuccess
onFail(this._error);
return;
}
if(this._whenFail.findIndex(onFail) === -1) {
this._whenFail.push(onFail);
}
}
};
}

function createAsyncTask() {
let task = new AsyncTask();
setTimeout(function () {
// if async task finished
if(Math.random() * 10 > 5) {
let data = {name: 'lyzg'};
task.finish(data);
} else {
task.fail(new Error('sth happened'));
}
}, 1000);
return task;
}

function anotherModule(asyncTask){
asyncTask.whenSuccess(function(data){
console.log(data);
});

asyncTask.whenFail(function(err){
console.error(err);
});
}

let asyncTask = createAsyncTask();
anotherModule(asyncTask);

上面模拟的就是在在调用createAsyncTask的时候可能不需要回调,真正的回调在另外一个代码anotherModule的内部,单纯的回调函数做不到这个形式。 它的核心点就在于把异步回调封装了可用于传递的对象,只要在异步任务完成之前,把这个异步对象传递到任何需要的位置,每个位置都能单独添加自己的回调函数,互不影响。 相比单纯的回调形式,这个方式有更多优点:

  1. 不受使用位置限制
  2. 可接收多个回调函数
  3. 更加面向对象
  4. 还可保存异步任务的状态,随时都能获取已经结束的异步任务的结果

第4点可以用下面的例子来测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function anotherModule(asyncTask){
setTimeout(function(){
asyncTask.whenSuccess(function(data){
console.log(data);
});

asyncTask.whenFail(function(err){
console.error(err);
});
}, 2000); // 2s后再取asyncTask的结果
}

let asyncTask = createAsyncTask();
anotherModule(asyncTask);

AsyncTask类的封装看起来增加了很多代码,但是完全没有增加使用时的复杂度,而且适用性更广,所以把代码重构成这样是值得的。再观察这个类的代码,还会发现一个新的问题,就是AsyncTask类的实例具有finish和fail这两个实例方法,而这两个实例方法的行为应该是只有异步任务才能使用的,所以直接部署在AsyncTask实例上不合适,外边拿到AsyncTask实例后,如果意外地调用了这两个方法,就会导致错误发生。 所以需要想办法把这两个方法的行为控制在异步任务内使用。

解决的办法就是再对这个AsyncTask封装一层,把finish和fail的行为,放到这新的一层处理。

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
function AsyncTask() {
return {
_state : 0,// 记录task的状态 0 执行中 1 执行成功 2 执行失败
_whenSuccess : [],// 存放task执行成功时的回调函数
_whenFail : [],// 存放task执行失败时的回调函数
_data : undefined,// 存放task执行成功时的状态
_error : undefined, // 存放task执行失败时的错误
whenSuccess(onSuccess) {
if(typeof onSuccess !== 'function') return;
if(this._state === 1) {
//如果添加回调时,task已经执行完毕,则直接把执行完毕时保存的状态回调onSuccess
onSuccess(this._data);
return;
}
if(this._whenSuccess.findIndex(onSuccess) === -1) {
this._whenSuccess.push(onSuccess);
}
},
whenFail(onFail) {
if(typeof onFail !== 'function') return;
if(this._state === 2) {
//如果添加回调时,task已经执行失败,则直接把执行失败时保存的状态回调onSuccess
onFail(this._error);
return;
}
if(this._whenFail.findIndex(onFail) === -1) {
this._whenFail.push(onFail);
}
}
};
}

function AsyncTaskWrapper() {
return {
asyncTask: new AsyncTask(),
finish(data) {
if(this.asyncTask._state !== 0) return;
this.asyncTask._state = 1;
this.asyncTask._data = data;//保存成功时状态
for(let callback of this.asyncTask._whenSuccess) {
callback(this.asyncTask._data);
}
this.asyncTask._whenSuccess.length = 0;
},
fail(err) {
if(this.asyncTask._state !== 0) return;
this.asyncTask._state = 2;
this.asyncTask._error = err;//保存失败时状态
for(let callback of this.asyncTask._whenFail) {
callback(this.asyncTask._error);
}
this.asyncTask._whenFail.length = 0;
},
}
}

在异步任务中,直接使用AsyncTaskWrapper,而不是直接使用AsyncTask:

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
function createAsyncTask() {
let taskWrapper = new AsyncTaskWrapper();
setTimeout(function () {
// if async task finished
if(Math.random() * 10 > 5) {
let data = {name: 'lyzg'};
taskWrapper.finish(data);
} else {
taskWrapper.fail(new Error('sth happened'));
}
}, 1000);
return taskWrapper.asyncTask;
}

function anotherModule(asyncTask){
asyncTask.whenSuccess(function(data){
console.log(data);
});

asyncTask.whenFail(function(err){
console.error(err);
});
}

let asyncTask = createAsyncTask();
anotherModule(asyncTask);

加了AsyncTaskWrapper之后,结果是一样的, 但是代码的封装性更好。 现在异步任务创建的AsyncTask实例就没有finish和fail的行为可以随便用了。

Promise模式初现

上面的AsyncTaskWrapper和AsyncTask类的使用模式,就是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
function Promise() {
return {
_state : 0,// 记录promise的状态 0 执行中 1 执行成功 2 执行失败
_resolved : [],// 存放promise被resolved时的回调函数
_rejected : [],// 存放promise被rejected时的回调函数
_data : undefined,// 存放promise被resolved时的状态
_error : undefined, // 存放promise被rejected时的错误
then(onResolved) {
if(typeof onResolved !== 'function') return;
if(this._state === 1) {
//如果添加回调时,promise已经resolved,则直接把执行完毕时保存的状态回调onResolved
onResolved(this._data);
return;
}
if(this._resolved.findIndex(onResolved) === -1) {
this._resolved.push(onResolved);
}
},
catch(onReject) {
if(typeof onReject !== 'function') return;
if(this._state === 2) {
//如果添加回调时,promise已经rejected,则直接把执行失败时保存的状态回调onRejected
onReject(this._error);
return;
}
if(this._rejected.findIndex(onReject) === -1) {
this._rejected.push(onReject);
}
}
};
}

function Deferred() {
return {
promise: new Promise(),
resolve(data) {
if(this.promise._state !== 0) return;
this.promise._state = 1;
this.promise._data = data;//保存成功时状态
for(let callback of this.promise._resolved) {
callback(this.promise._data);
}
this.promise._resolved.length = 0;
},
reject(err) {
if(this.promise._state !== 0) return;
this.promise._state = 2;
this.promise._error = err;//保存失败时状态
for(let callback of this.promise._rejected) {
callback(this.promise._error);
}
this.promise._rejected.length = 0;
},
}
}


function createAsyncTask() {
let deferred = new Deferred();
setTimeout(function () {
// if async task finished
if(Math.random() * 10 > 5) {
let data = {name: 'lyzg'};
deferred.resolve(data);
} else {
deferred.reject(new Error('sth happened'));
}
}, 1000);
return deferred.promise;
}

function anotherModule(promise){
promise.then(function(data){
console.log(data);
});

promise.catch(function(err){
console.error(err);
});
}

let promise = createAsyncTask();
anotherModule(promise);

看,现在是不是很像Promise的风格了!

尽管如此,它还是不完美的,首先是then跟catch方法并不统一,在Promise标准里面,then方法是同时加两个回调的,catch方法也只是重载then方法而已;然后最重要的就是Promise实例必须要支持then方法的链式调用,这才是Promise标准最大的特点。所以要想办法把上面的Promise类继续改进。

改进后的Promise类

先来看如何改进then方法,这个还是比较简单的:

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
function Promise() {
return {
_state : 0,// 记录promise的状态 0 执行中 1 执行成功 2 执行失败
_resolved : [],// 存放promise被resolved时的回调函数
_rejected : [],// 存放promise被rejected时的回调函数
_data : undefined,// 存放promise被resolved时的状态
_error : undefined, // 存放promise被rejected时的错误
then(onResolved, onReject) {
if(this._state === 1) {
//如果添加回调时,promise已经resolved,则直接把执行完毕时保存的状态回调onResolved
onResolved(this._data);
return;
} else if(this._state === 2) {
//如果添加回调时,promise已经rejected,则直接把执行失败时保存的状态回调onRejected
onReject(this._error);
return;
}


if(typeof onResolved === 'function' && this._resolved.findIndex(onResolved) === -1) {
this._resolved.push(onResolved);
}

if(typeof onReject === 'function' && this._rejected.findIndex(onReject) === -1) {
this._rejected.push(onReject);
}
},
catch(onReject) {
return this.then(null, onReject);
}
};
}

由于只是给then方法增加了几个if逻辑,所以then方法和catch方法是很好统一起来的。

接下来是重点,怎么样才能让then方法具备异步任务的链式调用特性呢?我们已经知道then方法要实现链式调用,那么它的返回值必须是一个Promise实例,所以then方法重构的第一步就是确定下面这个结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Promise() {
return {
_state : 0,// 记录promise的状态 0 执行中 1 执行成功 2 执行失败
_resolved : [],// 存放promise被resolved时的回调函数
_rejected : [],// 存放promise被rejected时的回调函数
_data : undefined,// 存放promise被resolved时的状态
_error : undefined, // 存放promise被rejected时的错误
then(onResolved, onReject) {
let deferred = new Deferred();
//todo more
return deferred.promise;
},
catch(onReject) {
return this.then(null, onReject);
}
};
}

由于deferred正常情况下都是在异步代码块里面去使用的,异步代码块可以调用deferred对象的resolve或reject方法来改变promise状态,以便promise的回调能够执行。 所以下一步就是要想办法调用then方法内部deferred对象的resolve或reject方法。 按照Promise标准,then的调用者被resolve或reject的时候,then添加的onResolved或onReject回调就会执行,而onResolved或onReject回调执行后,会导致then返回的Promise实例被resolve或reject。所以下一步就是要想办法在onResolved或onReject回调的时候,把then返回的promise实例给resolve或reject掉。 诀窍就是:原本应该把onResolved或onReject加到then调用者的回调数组中去的,调整一下,不是直接把它们加进去,而是加一个新的回调函数进去,在这个新的回调函数内再去调用原始的回调,同时还要调用deferred对象的resolve或reject方法。 简单不完整实现如下:

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
function Promise() {
return {
_state : 0,// 记录promise的状态 0 执行中 1 执行成功 2 执行失败
_resolved : [],// 存放promise被resolved时的回调函数
_rejected : [],// 存放promise被rejected时的回调函数
_data : undefined,// 存放promise被resolved时的状态
_error : undefined, // 存放promise被rejected时的错误
then(onResolved, onReject) {
let deferred = new Deferred();

let _onResolved = (data)=>{
let ret = typeof onResolved === 'function' ? onResolved(data) : undefined;
deferred.resolve(ret);
};

let _onReject = (err)=>{
let ret = typeof onReject === 'function' ? onReject(err) : undefined;
deferred.reject(ret);
};


this._resolved.push(_onResolved);
this._rejected.push(_onReject);

return deferred.promise;
},
catch(onReject) {
return this.then(null, onReject);
}
};
}

虽然上面这个then的实现很简陋,但已经展现了本篇要介绍的Promise简单实现思想的核心。 这个_onResolved和_onReject作为一个中间函数,成功地联结了then的调用者、then的返回者以及then的回调函数,这三个关键的角色。 回顾之前在ES6 Promise的学习笔记中的内容,理解then方法的特性,就是要理解这三者之间的关系。

到了这里,要让then方法实现异步任务的链式调用就比较容易了,只要对前面then方法继续改进即可,,示范及举例如下:

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
function Promise() {
let p = {
_state : 0,// 记录promise的状态 0 执行中 1 执行成功 2 执行失败
_resolved : [],// 存放promise被resolved时的回调函数
_rejected : [],// 存放promise被rejected时的回调函数
_data : undefined,// 存放promise被resolved时的状态
_error : undefined, // 存放promise被rejected时的错误
then(onResolved, onReject) {
let deferred = new Deferred();

let _onResolved = (data)=>{
try {
//加上try-catch,任何错误都将导致deferred调用reject

let ret = typeof onResolved === 'function' ? onResolved(data) : undefined;

if(ret instanceof Promise) {
//关键代码 onResolved如果返回一个新的Promise实例,那么该实例将决定deferred调用哪个方法
ret.then(function(data){
deferred.resolve(data);
}, function(err){
deferred.reject(err);
});
} else {
deferred.resolve(ret);
}
} catch(e) {
deferred.reject(e);
}
};

let _onReject = (err)=>{
try {
//加上try-catch,任何错误都将导致deferred调用reject

let ret = typeof onReject === 'function' ? onReject(err) : undefined;

if(ret instanceof Promise) {
//关键代码 onReject如果返回一个新的Promise实例,那么该实例将决定deferred调用哪个方法
ret.then(function(data){
deferred.resolve(data);
}, function(err){
deferred.reject(err);
});
} else {
// 只要onReject没有抛出新的错误,就还是resolve掉deferred
deferred.resolve(ret);
}
} catch(e) {
deferred.reject(e);
}
};


if(this._state === 1) {
_onResolved(this._data);
} else if(this._state === 2) {
_onReject(this._error);
} else {
this._resolved.push(_onResolved);
this._rejected.push(_onReject);
}

return deferred.promise;
},
catch(onReject) {
return this.then(null, onReject);
}
};

//必须加这行,否则上面instanceof运算不对
Object.setPrototypeOf(p, Promise.prototype);

return p;
}


function Deferred() {
return {
promise: new Promise(),
resolve(data) {
if(this.promise._state !== 0) return;
this.promise._state = 1;
this.promise._data = data;//保存成功时状态
for(let callback of this.promise._resolved) {
setTimeout(()=>{callback(this.promise._data);}, 0);
}
this.promise._resolved.length = 0;
},
reject(err) {
if(this.promise._state !== 0) return;
this.promise._state = 2;
this.promise._error = err;//保存失败时状态
for(let callback of this.promise._rejected) {
setTimeout(()=>{callback(this.promise._error);}, 0);
}
this.promise._rejected.length = 0;
},
}
}


function createAsyncTask() {
let deferred = new Deferred();
setTimeout(function () {
// if async task finished
deferred.resolve({name: 'lyzg'});
}, 1000);
return deferred.promise;
}

let promise = createAsyncTask();

promise.then(data=>{
console.log(data);
let deferred = new Deferred();
setTimeout(()=>{
deferred.resolve({name: 'lyzg2'});
}, 1000);

//模拟返回一个新的Promise实例
return deferred.promise;
}).then(data=> {
console.log(data);
throw new Error('error happened');
}).catch(err=> {
console.error(err);
}).then(data=>{
console.log('ended!');
});
// {name: "lyzg"}
// {name: "lyzg2"}
// Error: error happened
// ended!

到了这一步,本篇封装的Promise类就已经达到前面计划要实现的那两个目标了。 跟Promise标准实现比起来,这个类还差了很多很多特性,但是最起码已经把Promise最为突出的特点已经展现出来了,了解这个基本实现的思想,对于深入理解Promise的特性一定是有帮助的。

小结

最后我想说的是,本篇的思想是从别人那学来的,而且是13年的文章,现在看起来好像没有什么实用价值,但是对于我自己理解Promise的原理是很有帮助的一个点,所以我把它按自己的思路重新分享出来。