玩转 Promise 了解一下

  这篇文章解释了Promise的创建流程,实现基本原理,使用技巧和需要注意的地方,让你知道如何更好地运用 Promise 开发。

解释下Promise

一个 Promise 对象代表一个目前还不可用,但是在未来的某个时间点可以被解析的值。采用统一的接口编写,允许你以一种同步的方式将复杂异步处理轻松模块化的规范做法。

举个例子:例如,如果你想要使用 Promise API 异步调用一个远程的服务器,你需要创建一个代表数据将会在未来由 Web 服务返回的 Promise 对象。唯一的问题是目前数据还不可用。当请求完成并从服务器返回时数据将变为可用数据。在此期间, Promise 对象将扮演一个真实数据的代理角色。接下来,你可以在 Promise 对象上绑定一个回调函数,一旦真实数据变得可用这个回调函数将会被调用。

快速上手使用

创建 promise 对象的流程

new Promise(fn) 返回一个promise对象。 在 fn 中处理异步,处理结果正常调用 resolve(value),处理结果错误,调用reject(error)

针对上面创建promise对象,其实有更快捷的写法,这些静态方法是原本写法的一些语法糖。

1
Promise.resolve(value)

等同于

1
2
3
new Promise(function(resolve){
resolve(value);
});

处理结果

1
2
3
Promise.resolve(value).then(function(value){
console.log(value);
});
1
Promise.reject(error)

等同于

1
2
3
new Promise(function(resolve, reject){
reject(error);
});

处理错误

1
2
3
Promise.resolve(error).catch(function(error){
throw new Error(error);
});

具有 then() 方法的对象A(通常也称thenable的对象),可以通过Promise.resolve(A)转换为一个Promise对象,当然前提是这个对象A要遵循Promises/A+或ES6 Promises标准,否则可能会缺失部分信息。

then(fn1) 中 fn1 函数与 new Promise(fn2) 中的 fn2 函数执行顺序不同,fn1调用是异步执行,fn2调用是同步执行,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
var promise = new Promise(function (resolve){
console.log("inner promise"); // 1 ->最先执行
resolve("inner then");
});
promise.then(function(value){
console.log(value); // 3 -> 最后执行
});
console.log("outer promise"); // 2 -> 然后执行
// 执行结果:
inner promise // 1
outer promise // 2
inner then // 3

异步调用

需要注意的一点是,Promise 只能使用异步调用方式,否则将引起同步、异步调用的混乱问题,导致栈溢出,处理顺序与预期不符或者异常处理错乱等问题。

了解Chain即链式调用,这在Jquery中普遍使用,Promise.then().catch(),其中 then 和 catch 方法都返回了一个Promise对象,但两个Promise对象状态不同,一个是Fulfilled, 一个是Rejected,表明结果正确还是出现异常,所以在一个链式调用中,如

1
2
3
4
5
6
7
8
9
function fn1 () { 
throw new Error('fn1出错了');
}
var promise = Promise.resolve()
promise
.then(fn1)
.then(fn2)
.catch(fn3)
.then(fn4)

一旦,fn1中抛出异常,.then(fn1)返回的将是一个Rejected状态的Promise对象,而 then法不接受Rejected状态的Promise对象,所以fn2、fn4将不会被执行,直接调用fn3进行异常处理。
此外,fn1使用了throw方法来触发try…catch,进行异常捕捉。不过推荐使用reject方法Promise对象状态设置为Rejected,如fn1可以写为:

1
2
3
function fn1 () {
return Promise.reject('fn1出错了');
}

其实从上面例子不难发现几点:

  1. 是如何在then方法中使用reject。
  2. 推荐使用reject,因为在Promise中,它比throw更安全,优雅。
  3. 如果一个错误回调中,没有重新抛出错误,promise的状态会转变为resolved,继续执行后续处理,而不会进入catch。
  4. catch方法只能捕获上一级抛出的异常。

链式调用then方法和循环调用then,虽然每次都是返回一个Promise,但是前者状态可能因为then中的异常有无而不断改变,后者则每次返回一个focked Promise,状态不变,即即使上一次循环抛出异常,依然可以执行下一次循环。

使用扩展

  • done方法与then的区别:前者不会返回Promise对象,所以只能放在链式的最后,done中发生异常不会被Promise捕获进入catch,而是直接抛出到调用链外面。
  • all方法会同并发执行多个Promise对象,而不是按照参数顺序一个个执行。想要实现顺序执行promise,可以有几个方法,比如循环调用then。
  • race方法与all的区别在于,在执行promise数组执行过程中,当任何一个promise对象进入到确定(解决)状态后,就继续进行后续处理,即执行then方法。而all则会全部执行promise对象的数组,在继续后续处理。所以根据这一特性,可以使用race方法来实现超时机制,一旦有某个请求超时,则停止程序,处理超时。
  • 如何暂停一个Promise的执行:在想要暂停的PromiseA的then方法中,返回一个PromiseB,这样promiseA就被暂停了,等Promise的then方法执行完毕后,继续执行。举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function fn1 () {
    // to do something
    return Promise.resolve();
    }
    var promise = Promise.resolve()
    promise
    .then(fn1)
    .then(fn2)
    .then(fn3)
    .done(fn4)

详解 Promise 核心实现原理

基本实现

basePromise.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
28
29
30
31
32
'use strict';

var immediate = require('immediate');

function INTERNAL() {}
function isFunction(func) {
return typeof func === 'function';
}
function isObject(obj) {
return typeof obj === 'object';
}
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

module.exports = Promise;

function Promise(resolver) {
if (!isFunction(resolver)) {
throw new TypeError('resolver must be a function');
}
this.state = PENDING;
this.value = void 0;
this.queue = [];
if (resolver !== INTERNAL) {
safelyResolveThen(this, resolver);
}
}

immediate 是一个将同步转异步执行的库。INTERNAL 是一个空函数,类似于 noopisFunction、isObject 和 isArray 是 3 个辅助判断函数。PENDINGFULFILLED 和 REJECTED 是 promise 的 3 种状态。safelyResolveThen 后讲。
Promise 内部有三个变量:

  1. state: 当前 promise 的状态,初始值为 PENDING。状态改变只能是 PENDING-> FULFILLED 或 PENDING -> REJECTED。
  2. value: 当 state 是 FULFILLED 时存储返回值,当 state 是 REJECTED 时储错 误。
  3. queue: promise 内部的回调队列,下面会讲到。

状态转变不可逆

Promise 本质是一个状态机。每个 promise 只能是 3 种状态中的一种:pending、fulfilled 或 rejected。状态转变只能是 pending -> fulfilled 或者 pending -> rejected。状态转变不可逆。

实现原理简析一

1
2
3
4
5
6
7
8
9
10
11
var Promise = require('basePromise')
var promise = new Promise((resolve) => {
setTimeout(() => {
resolve('abc')
}, 1000)
})
var a = promise.then(function onSuccess() {})
var b = promise.catch(function onError() {})
console.dir(promise, { depth: 10 })
console.log(promise.queue[0].promise === a)
console.log(promise.queue[1].promise === b)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 打印输出
Promise {
state: 0,
value: undefined,
queue:
[ QueueItem {
promise: Promise { state: 0, value: undefined, queue: [] },
callFulfilled: [Function],
callRejected: [Function] },
QueueItem {
promise: Promise { state: 0, value: undefined, queue: [] },
callFulfilled: [Function],
callRejected: [Function] } ] }
true
true

上例中 queue 数组中有两个对象,这两个对象怎么来的?后面两次打印为何是真?

根据 Promise/A+ 规范(英文) | 翻译 中规定可知,在调用 .then.catch 时 promise 并没有被 resolve。这时会将 .then.catch生成的新 promise,即 a 和 b,和正确时的回调,即onSuccess 包装成callFulfilled,和错误时的回调,即 onError 包装成 callRejected,生成一个QueueItem 实例,并 push 到 queue 数组里。找到了对应关系,就不难理解上面个 console.log 会打印出 true 了。

当 promise 状态改变时会遍历内部 queue 数组,若变为 FULFILLED 执行成功回调callFulfilled,或变为 REJECTED,执行失败回调 callRejected,并传入promise 的 value 值,设置 Promise 实例 a , b 的 state 和value,详细参考下面的执行过程

以上就是 Promise 实现的基本原理。

实现原理简析二

上一个例子中只对 promise 实例进行了一次 .then 方法调用,所以 queue 数组中子对象 QueueItem 的 promise 的 queue 为空。
举一个多次调用的例子。

1
2
3
4
5
6
7
8
9
10
11
var Promise = require('appoint')
var promise = new Promise((resolve) => {
setTimeout(() => {
resolve('abc')
}, 1000)
})
promise
.then(() => {})
.then(() => {})
.then(() => {})
console.dir(promise, { depth: 10 })
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
// 打印输出
Promise {
state: 0,
value: undefined,
queue:
[ QueueItem {
promise:
Promise {
state: 0,
value: undefined,
queue:
[ QueueItem {
promise:
Promise {
state: 0,
value: undefined,
queue:
[ QueueItem {
promise: Promise { state: 0, value: undefined, queue: [] },
callFulfilled: [Function],
callRejected: [Function] } ] },
callFulfilled: [Function],
callRejected: [Function] } ] },
callFulfilled: [Function],
callRejected: [Function] } ] }

调用了 3 次 then,每个 then 将它生成的 promise 放到了调用它的 promise 队列里,形成了 3 层调用关系。

当最外层的 promise 状态改变时,遍历它的 queue 数组调用对应的回调,设置子 promise 的 state 和 value 并遍历它的 queue 数组调用对应的回调,然后设置孙 promise 的 state 和 value 并遍历它的 queue 数组调用对应的回调,依次类推。

安全的执行 then 函数 (safelyResolveThen)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function safelyResolveThen(self, then) {
var called = false;
try {
then(function (value) {
if (called) {
return;
}
called = true;
doResolve(self, value);
}, function (error) {
if (called) {
return;
}
called = true;
doReject(self, error);
});
} catch (error) {
if (called) {
return;
}
called = true;
doReject(self, error);
}
}

safelyResolveThen 顾名思义用来『安全的执行 then 函数』,这里的 then 函数指『一个函数,其第一参数是 resolve 函数且第二参数是 reject 函数』,如下两种情况

  1. 构造函数的参数,即函数resolver

    1
    2
    3
    4
    5
    new Promise(function resolver(resolve, reject) {
    setTimeout(() => {
    resolve('haha')
    }, 1000)
    })
  2. promise 的 then

    1
    promise.then(resolve, reject)

safelyResolveThen 有 3 个作用

  • try…catch 捕获抛出的异常
  • called 控制 resolve 或 reject 只执行一次,多次调用没有任何作用, 也是状态转变不可逆的实现原理。
  • 触发执行过程,没有错误则执行 doResolve,有错误则执行 doReject

执行过程 (doResolve & doReject)

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 doResolve(self, value) {
try {
var then = getThen(value);
if (then) {
safelyResolveThen(self, then);
} else {
self.state = FULFILLED;
self.value = value;
self.queue.forEach(function (queueItem) {
queueItem.callFulfilled(value);
});
}
return self;
} catch (error) {
return doReject(self, error);
}
}

function doReject(self, error) {
self.state = REJECTED;
self.value = error;
self.queue.forEach(function (queueItem) {
queueItem.callRejected(error);
});
return self;
}
  • doResolve 结合 safelyResolveThen 使用不断地解包 promise,直至返回值是非 promise 对象后,设置 promise 的状态和值,把 value 值传入子 promise,然后子 promise 根据传入的值设置自己的状态和值。

  • doReject 会设置 promise 的 state 为 REJECTED,value 为 error,调用callRejected 把 error 传给子 promise,然后子 promise 根据传入的错误设置自己的状态和值。

获取当前 then (getThen)

在 doResolve 中有一个辅助函数 getThen。

1
2
3
4
5
6
7
8
function getThen(obj) {
var then = obj && obj.then;
if (obj && (isObject(obj) || isFunction(obj)) && isFunction(then)) {
return function appyThen() {
then.apply(obj, arguments);
};
}
}

Promise/A+ 规范(英文) | 翻译 中规定可知

如果 then 是函数,将 x 作为函数的作用域 this 调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise。

规定中说的 x 就是上面代码中的 obj。

详解 then 函数实现原理

then 和 catch 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Promise.prototype.then = function (onFulfilled, onRejected) {
if (!isFunction(onFulfilled) && this.state === FULFILLED ||
!isFunction(onRejected) && this.state === REJECTED) {
return this;
}
var promise = new this.constructor(INTERNAL);
if (this.state !== PENDING) {
var resolver = this.state === FULFILLED ? onFulfilled : onRejected;
unwrap(promise, resolver, this.value);
} else {
this.queue.push(new QueueItem(promise, onFulfilled, onRejected));
}
return promise;
};

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

then 方法可以分为4部分。

  1. 第一部分实现了值穿透,即 promise 已经是 FULFILLED 或 REJECTED 时,通过 return this 实现的值穿透。
  2. 第二部分生成一个新的 promise。
  3. 第三部分实现 promise 状态改变时,执行的逻辑。即状态改变则调用 unwrap,否则将生成的 promise 加入到当前 promise 的回调队列 queue 里,实现原理简析一二已经讲过了如何消费 queue。
  4. 按照 规范 | 翻译 要求,then 方法最后会返回一个新生成的 promise。

Tips:promise.catch(onRejected) 其实就是 promise.then(null, onRejected) 的语法糖。

then 方法详解

then 方法的实现中,新建 promise、unwrap 函数、QueueItem 构造函数,这三点需要详细说明下。

  1. Promise 构造函数传入了一个 INTERNAL 即空函数,这个新生的 promise 是函数内部的 promise,会根据外部的 promise 的状态和值产生自身的状态和值,不需要传入回调函数。而外部 Promise 需要传入回调函数来决定它的状态和值,所以之前 Promise 的构造函数里做了判断区分外部调用还是内部调用:

    1
    2
    3
    if (resolver !== INTERNAL) {
    safelyResolveThen(this, resolver);
    }
  2. 以下为 unwrap 函数实现,顾名思义,这是个解包函数,第一参数是子 promise,第二参数是父 promise 的 then 的回调,即 onFulfilled 或 onRejected,第三参数是父 promise 的值,即 value 或 error。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function unwrap(promise, func, value) {
    immediate(function () {
    var returnValue;
    try {
    returnValue = func(value);
    } catch (error) {
    return doReject(promise, error);
    }
    if (returnValue === promise) {
    doReject(promise, new TypeError('Cannot resolve promise with itself'));
    } else {
    doResolve(promise, returnValue);
    }
    });
    }

    有几点需要注意:

    1. immediate 在基本实现中已经说过,是一个将同步代码变为异步的api。
    2. 使用 try catch 用来捕获 then/catch 内抛出的异常,并调用 doReject。
    3. 返回的值不能是 promise 本身,否则会造成死循环。
  3. 以下为 QueueItem 实现,参数中 promise 为 then 函数中新生的子 promise, onFulfilled 和 onRejected 则为 then 函数参数的透传。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function QueueItem(promise, onFulfilled, onRejected) {
    this.promise = promise;
    this.callFulfilled = function (value) {
    doResolve(this.promise, value);
    };
    this.callRejected = function (error) {
    doReject(this.promise, error);
    };
    if (isFunction(onFulfilled)) {
    this.callFulfilled = function (value) {
    unwrap(this.promise, onFulfilled, value);
    };
    }
    if (isFunction(onRejected)) {
    this.callRejected = function (error) {
    unwrap(this.promise, onRejected, error);
    };
    }
    }

    结合then 和 catch 的实现then 方法详解可知,当 promise 状态变为 FULFILLED 时,之前注册的 then 函数,用 callFulfilled 调用 unwrap 进行解包最终得出子 promise 的状态和值,之前注册的 catch 函数,用 callFulfilled 直接调用 doResolve,设置队列里子 promise 的状态和值。当 promise 状态变为 REJECTED 时类同。

promise 一些常用 api 实现原理

all 方法

用来并行执行多个 promise 或 value,当所有 promise 或 value 执行完毕,或有一个错误发生,执行返回处理。

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
Promise.all = all;
function all(iterable) {
var self = this;
if (!isArray(iterable)) {
return this.reject(new TypeError('must be an array'));
}

var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
}

var values = new Array(len);
var resolved = 0;
var i = -1;
var promise = new this(INTERNAL);

while (++i < len) {
allResolver(iterable[i], i);
}
return promise;
function allResolver(value, i) {
self.resolve(value).then(resolveFromAll, function (error) {
if (!called) {
called = true;
doReject(promise, error);
}
});
function resolveFromAll(outValue) {
values[i] = outValue;
if (++resolved === len && !called) {
called = true;
doResolve(promise, values);
}
}
}
}

简析:

  • Promise.all 内部生成了一个新的 promise 返回。
  • called 用来控制即使有多个 promise reject 也只有第一个生效。
  • values 用来存储结果。
  • 当最后一个 promise 得出结果后,使用 doResolve(promise, values) 设置 promise 的 state 为 FULFILLED,value 为结果数组 values。

race 方法

接受一个数组,当数组中有一个 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
32
33
34
Promise.race = race;
function race(iterable) {
var self = this;
if (!isArray(iterable)) {
return this.reject(new TypeError('must be an array'));
}

var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
}

var i = -1;
var promise = new this(INTERNAL);

while (++i < len) {
resolver(iterable[i]);
}
return promise;
function resolver(value) {
self.resolve(value).then(function (response) {
if (!called) {
called = true;
doResolve(promise, response);
}
}, function (error) {
if (!called) {
called = true;
doReject(promise, error);
}
});
}
}

跟 Promise.all 代码相近,使用扩展简单说过区别,这里从代码上强调一下,race 的 called 与 all 的不同,这里 called 控制只要有任何一个 promise onFulfilled 或 onRejected 立即去设置 promise 的状态和值。

Promise.resolve 和 Promise.reject

1
2
3
4
5
6
7
8
9
10
11
12
13
Promise.resolve = resolve;
function resolve(value) {
if (value instanceof this) {
return value;
}
return doResolve(new this(INTERNAL), value);
}

Promise.reject = reject;
function reject(reason) {
var promise = new this(INTERNAL);
return doReject(promise, reason);
}

以上都是 Promise 原本写法的一些语法糖,需要了解的是,当 Promise.resolve 参数是一个 promise 时,会直接返回该值。