Promise简单实现原理

最近看了几篇讲解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实现贴在一起,方便讲解。

  1. getUserId方法被调用后,返回一个Promise对象,而在getUserId方法中返回的Promise接收了一个function参数,这个传入的方法内部会执行getUserId方法需要执行的主体功能,如例一中的IO操作。
  2. getUserId的主体功能完成后,需要触发下一步的函数,可以看到success状态后,执行resolve方法,传入相应参数即用户ID,完成触发。
  3. 这里可能就会有人对resolve方法有些疑惑,觉得这个函数出现的有点突然。其实上一条已经从功能上简单叙述了resolve方法的作用,要理解resolve具体的作用原理我们需要看看Promise对象的实现代码。Promise对象接受一个参数fn,这个fn便是例一中用于实现getUserId的主体功能的代码。跳到Promise实现的最后一行,我们可以看到,创建Promise对象的时候,会以回调函数的形式调用传入方法fn,而fn的参数就是Promise对象内部的方法resolve。至此resolve方法的来源已经秦楚了:resolve是Promise对象的内部方法,功能是触发下一步执行
  4. Promise对象中deferreds可以简单看做需要异步操作顺序执行的方法队列。继续看Promise对象中resolve方法的具体实现。假设情景是getUserId中IO操作完成,调用resolve方法,此时resolve方法接受参数value并将回调函数队列一一执行。读者很容易就可以发现,如果只执行getUserId(),那么执行resolve触发时,回调队列是空的,没有函数被调用。
  5. 接着看Promise对象的then方法,接受一个方法参数onFulfilled,将该函数加入回调队列尾。到这里我们就把最基础的Promise实现全都连上了:调用getUserId –> 以回调函数形式传入匿名函数 –> 调用回调函数IO操作(异步代码加入事件队列尾) –> 返回Promise对象 –> 执行then方法 –> 将IO完成后需要执行的函数加入队列尾 –> IO结束,执行resolve触发 –> 执行deferreds队列中的方法

这就是Promise最基础的实现方式,虽然简单,但是已经将最基本的代码思路给出来了,后续的升级版本也是在这个基础上完善。
例如想要传入在一个异步操作执行完成后,执行多个同步方法,只需要稍稍修改then方法就可以达到目的

1
2
3
4
this.then = function (onFulfilled) {
deferreds.push(onFulfilled);
return this;
};

增加延时

从之前的叙述可以知道,如果Promise中传入的方法是同步代码,那么执行resolvethen方法还没被调用,这时回调队列其实是空的,而且接下来用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
27
function 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方法之后执行。通过使用setTimeoutresolve执行事件添加到事件队列尾

1
2
3
4
5
6
7
function resolve(value) {
setTimeout(function () {
deferreds.forEach(function (deferred) {
deferred(value);
});
}, 0);
}

引入状态

这一段无话可说

串行Promise

原文中这一段有点难度,看着有些绕,看了其他几篇文章,最后也没看到明悟,所以这里按博主自己的理解讲。
如果将来出了偏颇的话,我是不负责的

稍加理解就能知道,上面的Promise基本实现的then方法只能接受同步代码,而不能在then方法中传入另一个异步方法,要解决这个问题,需要进一步修改Promise对象。
这里贴一下引文中为了衔接当前 promise 和后邻 promisePromise对象做的修改

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
this.then = function (onFulfilled) {
return new Promise(function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
});
});
};

function handle(deferred) {
if (state === 'pending') {
deferreds.push(deferred);
return;
}

var ret = deferred.onFulfilled(value);
deferred.resolve(ret);
}

function resolve(newValue) {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve);
return;
}
}
state = 'fulfilled';
value = newValue;
setTimeout(function () {
deferreds.forEach(function (deferred) {
handle(deferred);
});
}, 0);
}

原文的描述是:

  • then 方法中,创建了一个新的 Promise 实例,并作为返回值,这类 promise,权且称作 bridge promise。这是串行 Promise 的基础。另外,因为返回类型一致,之前的链式执行仍然被支持;
  • handle 方法是当前 promise 的内部方法。这一点很重要,看不懂的童鞋可以去补充下闭包的知识。then 方法传入的形参 onFullfilled,以及创建新 Promise 实例时传入的 resolve 均被压入当前 promise 的 deferreds 队列中。所谓“巧妇难为无米之炊”,而这,正是衔接当前 promise 与后邻 promise 的“米”之所在。

博主当时就是这一段看的有点迷hhh,这里说一下自己的理解。

  1. then方法传入一个异步方法用作回调时,此时返回一个新的Promise对象,称作bridge Promise
  2. then方法返回的bridge Promiseresolve方法和传入形参onFullfilled包装为对象被压入当前Promise对象的回调队列。意思就是,当当前Promise异步操作结束后,调用自身的resolve方法,就会通过当前Promise的handle方法执行刚刚生成的bridge Promise传入的onFulilled方法。
  3. 而执行的bridge Promise传入的onFulilled方法即是执行另一个异步操作函数,产生一个新的Promise并等待执行。至此,当前和后邻Promise就衔接上了。
    bridge Promise1
    bridge Promise2

失败处理和异常处理

这两块如果上面看懂了的话,看看原文的代码应该很快就理解了,这里就不多说了。觉得Promise的前后衔接还是应该是全文最复杂、最难理解的一块了。。

结语

这篇个人整理就这样啦,有兴趣(没看懂)的读者可以再看看下面的补充资料~

用钱砸我