最近看了几篇讲解Js实现Promise的原理的文章,弄懂了以前没懂的一些东西,今天也稍稍整理一下
前言
不是很懂怎么写前言,Fork(抄袭)一段好了
随着浏览器端异步操作复杂程度的日益增加,以及以 Evented I/O 为核心思想的 NodeJS 的持续火爆,Promise、Async 等异步操作封装由于解决了异步编程上面临的诸多挑战,得到了越来越广泛的应用。本文旨在剖析 Promise 的内部机制,从实现原理层面
深入(点到为止地)探讨
— 美团点评技术团队 spring
本文的参考资料是
尤其是剖析 Promise 之基础篇,希望大家阅读本文之前先看看这篇文章。本文的叙述模式主要是讲解上文中的小细节和个人整理,适合对Promise不大理解的小白食用。
正文
基础实现
先贴一段引文中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/********* 例1 ***********/
function getUserId() {
return new Promise(function (resolve) {
// 异步请求
Y.io('/userid', {
on: {
success: function (id, res) {
resolve(JSON.parse(res).id);
}
}
});
});
}
getUserId().then(function (id) {
// do sth with id
});
/********* promise基础实现 ***********/
function Promise(fn) {
var value = null,
deferreds = [];
this.then = function (onFulfilled) {
deferreds.push(onFulfilled);
};
function resolve(value) {
deferreds.forEach(function (deferred) {
deferred(value);
});
}
fn(resolve);
}
引文中例一的代码对于对promise了解不多的同学来说,可能看的云里雾里,所以我把对应的promise实现贴在一起,方便讲解。
getUserId
方法被调用后,返回一个Promise
对象,而在getUserId
方法中返回的Promise
接收了一个function参数
,这个传入的方法内部会执行getUserId
方法需要执行的主体功能,如例一中的IO操作。- 当
getUserId
的主体功能完成后,需要触发下一步的函数,可以看到success
状态后,执行resolve
方法,传入相应参数即用户ID,完成触发。 - 这里可能就会有人对
resolve
方法有些疑惑,觉得这个函数出现的有点突然。其实上一条已经从功能上简单叙述了resolve
方法的作用,要理解resolve
具体的作用原理我们需要看看Promise
对象的实现代码。Promise
对象接受一个参数fn
,这个fn
便是例一中用于实现getUserId
的主体功能的代码。跳到Promise
实现的最后一行,我们可以看到,创建Promise
对象的时候,会以回调函数的形式调用传入方法fn
,而fn
的参数就是Promise
对象内部的方法resolve
。至此resolve
方法的来源已经秦楚了:resolve是Promise对象的内部方法,功能是触发下一步执行。 Promise
对象中deferreds
可以简单看做需要异步操作顺序执行的方法队列。继续看Promise
对象中resolve
方法的具体实现。假设情景是getUserId
中IO操作完成,调用resolve
方法,此时resolve方法接受参数value
并将回调函数队列一一执行。读者很容易就可以发现,如果只执行getUserId()
,那么执行resolve
触发时,回调队列是空的,没有函数被调用。- 接着看
Promise
对象的then
方法,接受一个方法参数onFulfilled
,将该函数加入回调队列尾。到这里我们就把最基础的Promise实现全都连上了:调用getUserId
–> 以回调函数形式传入匿名函数 –> 调用回调函数IO操作(异步代码加入事件队列尾) –> 返回Promise
对象 –> 执行then
方法 –> 将IO完成后需要执行的函数加入队列尾 –> IO结束,执行resolve
触发 –> 执行deferreds
队列中的方法
这就是Promise最基础的实现方式,虽然简单,但是已经将最基本的代码思路给出来了,后续的升级版本也是在这个基础上完善。
例如想要传入在一个异步操作执行完成后,执行多个同步方法,只需要稍稍修改then
方法就可以达到目的1
2
3
4this.then = function (onFulfilled) {
deferreds.push(onFulfilled);
return this;
};
增加延时
从之前的叙述可以知道,如果Promise
中传入的方法是同步代码,那么执行resolve
时then
方法还没被调用,这时回调队列其实是空的,而且接下来用then
方法注册的回调函数也不会再被调用。,读者可以将下面的代码写入Js解释环境进行验证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
27function fakePromise(fn) {
var value = null,
deferreds = [];
this.then = function (onFulfilled) {
deferreds.push(onFulfilled);
};
function resolve(value) {
deferreds.forEach(function (deferred) {
deferred(value);
});
}
fn(resolve);
}
function getUserId() {
return new fakePromise(function (resolve) {
console.log("getUserId执行完毕");
resolve("执行回调函数");
});
}
getUserId().then(function (str) {
console.log(str);
});
为此,我们需要认为地确保resolve
触发动作在then
方法之后执行。通过使用setTimeout
将resolve
执行事件添加到事件队列尾1
2
3
4
5
6
7function resolve(value) {
setTimeout(function () {
deferreds.forEach(function (deferred) {
deferred(value);
});
}, 0);
}
引入状态
这一段无话可说
串行Promise
原文中这一段有点难度,看着有些绕,看了其他几篇文章,最后也没看到明悟,所以这里按博主自己的理解讲。
如果将来出了偏颇的话,我是不负责的
稍加理解就能知道,上面的Promise
基本实现的then
方法只能接受同步代码,而不能在then
方法中传入另一个异步方法,要解决这个问题,需要进一步修改Promise
对象。
这里贴一下引文中为了衔接当前 promise 和后邻 promise对Promise
对象做的修改
1 | this.then = function (onFulfilled) { |
原文的描述是:
- then 方法中,创建了一个新的 Promise 实例,并作为返回值,这类 promise,权且称作 bridge promise。这是串行 Promise 的基础。另外,因为返回类型一致,之前的链式执行仍然被支持;
- handle 方法是当前 promise 的内部方法。这一点很重要,看不懂的童鞋可以去补充下闭包的知识。then 方法传入的形参 onFullfilled,以及创建新 Promise 实例时传入的 resolve 均被压入当前 promise 的 deferreds 队列中。所谓“巧妇难为无米之炊”,而这,正是衔接当前 promise 与后邻 promise 的“米”之所在。
博主当时就是这一段看的有点迷hhh,这里说一下自己的理解。
- 对
then
方法传入一个异步方法用作回调时,此时返回一个新的Promise
对象,称作bridge Promise
。 then
方法返回的bridge Promise
的resolve
方法和传入形参onFullfilled
包装为对象被压入当前Promise对象的回调队列。意思就是,当当前Promise
异步操作结束后,调用自身的resolve
方法,就会通过当前Promise的handle方法执行刚刚生成的bridge Promise
传入的onFulilled
方法。- 而执行的
bridge Promise
传入的onFulilled
方法即是执行另一个异步操作函数,产生一个新的Promise
并等待执行。至此,当前和后邻Promise就衔接上了。
失败处理和异常处理
这两块如果上面看懂了的话,看看原文的代码应该很快就理解了,这里就不多说了。觉得Promise的前后衔接还是应该是全文最复杂、最难理解的一块了。。
结语
这篇个人整理就这样啦,有兴趣(没看懂)的读者可以再看看下面的补充资料~