ES6 使用 Promise/A+ 规范。
规范解读
Promise/A+ 规范主要分为术语、要求和注意事项三个部分,我们主要看一下第二部分也就是要求部分(Requirements),更详细的细节参照完整版 Promise/A+ 规范。
1. Promise 状态
Promise 有三种状态:pending
,fulfilled
以及rejected
.(fulfilled
以及 resolved
的区别查看)。
状态转换只能是 pending
到 fulfilled
或者 pending
到 rejected
;
状态一旦转换完成,不能再次转换。
2. then
方法
Promise 必须拥有一个 then
方法,来处理 fulfilled 或 rejected 状态下的值。
Promise 的 then
方法接受两个参数:
promise.then(onFulfilled, onRejected);
-
两个参数均为可选参数,类型均为函数,如果不是将被忽略;
-
如果
onFulfilled
是个函数,则必须(must)在 promise 是 fulfilled 状态后调用,并将 promise 的值作为第一个参数。不能(must not be)在 promise 是 fulfilled 状态之前调用。只能执行一次。 -
同理,如果 onRejected 是个函数,则必须(must)在 promise 是 rejected 状态后调用,并将 promise 的值作为第一个参数。不能(must not be)在 promise 是 rejected 状态之前调用。只能执行一次。
-
onFulfilled
和onRejected
只有在执行环境堆栈仅包含平台代码时才可被调用 -
then
方法必须返回一个新的promise
,记作promise2
。promise2 = promise1.then(onFulfilled, onRejected);
这也就保证了
then
方法可以在同一个promise
上多次调用。(ps:规范只要求返回promise
,并没有明确要求返回一个新的promise
,这里为了跟 ES6 实现保持一致,我们也返回一个新promise
); -
onFulfilled/onRejected 有返回值则把返回值定义为 x,并执行
[[Resolve]](promise)
; -
onFulfilled/onRejected 运行出错,则把 promise2 设置为 rejected 状态;
-
onFulfilled/onRejected 不是函数,则需要把 promise1 的状态传递下去;
3. Promise 解决过程
函数标识为 [[Resolve]](promise,x)
,promise 为要返回的新 promise 对象,x 为 onFulfilled/onRejected 的返回值。如果 x 有 then 方法且看上去像一个 promise,我们就把 x 当成一个 promise 对象,即 thenable 对象,这种情况下尝试让 promise 接收 x 的状态。如果 x 不是 thenable 对象,就用 x 的值来执行 promise。
[[Resolve]](promise,x)
函数具体运行规则:
- 如果 promise 和 x 指向同一对象,以
TypeError
为拒因(reason)拒绝执行 promise; - 如果 x 为 promise,则使 promise 接受 x 的状态;
- 如果 x 为 pending 状态,promise 必须保持 pending 状态直到 x 变更为 fulfilled 或者 rejected
- 如果 x 为 fulfilled 状态,以相同的值 fulfill promise
- 如果 x 为 rejected 状态,以相同的原因(reason),reject promise
- 如果 x 为对象或者函数,取 x.then 的值,如果取值时出现错误,则让 promise 进入 rejected 状态。
- 如果 then 不是函数,说明 x 不是 thenable 对象,直接以 x 的值 fulfill;
- 如果 then 存在并且为函数,则把 x 作为 then 函数的作用域 this 调用。then 方法接收两个参数,第一个为 resolvePromise 以及第二个 rejectPromise;
- 如果 resolvePromise 被执行,则以 resolvePromise 的参数 value 作为 x 继续调用
[[Resolve]](promise,value)
,直到 x 不是对象或者函数; - 如果 rejectedPromise 被执行则让 promise 进入 rejected 状态;
- 如果 resolvePromise 被执行,则以 resolvePromise 的参数 value 作为 x 继续调用
- 如果 x 不是对象或者函数,直接就用 x 的值来执行(fulfill) promise
代码实现
规范第一条代码实现
规范解读第一条的代码实现:
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function Promise(executor) {
let self = this;
self.status = PENDING;
self.data = "";
// 异步处理成功调用的函数
// PromiseA+ 2.1 状态只能由Pending转为fulfilled或rejected;fulfilled状态必须有一个value值;rejected状态必须有一个reason值。
function resolve(value) {
if (self.status === PENDING) {
self.status = FULFILLED;
self.data = value;
}
}
// PromiseA+ 2.2.6.2 相同promise的then可以被调用多次,当promise变为rejected状态,全部的onRejected回调按照原始调用then的顺序执行
function reject(reason) {
if (self.status === PENDING) {
self.status = REJECTED;
self.data = reason;
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
规范第二条代码实现
规范解读第二条的代码实现:
/**
* 拥有一个then方法
* then方法提供:状态为 fulfilled 时的回调函数 onResolved,状态为 rejected 时的回调函数 onRejected
* 返回一个新的 Promise
*/
// 接上面实现
Promise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === "function"
? onFulfilled
: function (v) {
return v;
};
onRejected =
typeof onRejected === "function"
? onRejected
: function (r) {
throw r;
};
let self = this;
let promise2 = new Promise((resolve, reject) => {
if (self.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(self.data);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else if (self.status === REJECTED) {
setTimeout(() => {
try {
let r = onRejected(self.data);
resolvePromise(promise2, r, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else if (self.status === PENDING) {
self.onFulfilled.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(self.data);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
self.onRejected.push(() => {
setTimeout(() => {
try {
let r = onRejected(self.data);
resolvePromise(promise2, r, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
};
需要注意的问题:
- 根据规范第三条,实现 resolvePromise 函数
- then 方法执行时如果 promise 仍然处于 pending 状态,则把处理函数进行存储,等 resolve/reject 函数真正执行的时候在调用;
- promise.then 属于微任务,这里为了方法,使用宏任务 setTimeout 来代替实现异步,具体细节详见
以上,规范中关于then
的部分就全部实现完毕。
规范第三条的代码实现
这一步骤非常简单,只要按照规范转换成代码即可:
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
reject(new TypeError("chining cycle"));
}
if (x instanceof Promise) {
if (x.status === PENDING) {
x.then(function (value) {
resolvePromise(promise2, value, resolve, reject);
}, reject);
}
}
if ((x && typeof x === "object") || typeof x === "function") {
let used;
try {
let then = x.then;
if (typeof then === "function") {
then.call(
x,
(v) => {
if (used) return;
used = true;
resolvePromise(promise2, v, resolve, reject);
},
(r) => {
if (used) return;
used = true;
reject(r);
}
);
} else {
if (used) return;
used = true;
resolve(x);
}
} catch (e) {
if (used) return;
used = true;
reject(e);
}
} else {
resolve(x);
}
}
最后,完整的Promise
按照规范就实现完毕了。
我们需要测试我们的实现是否符合规范:
使用 Promises/A+ Compliance Test Suite 测试
使用 Promises/A+ Compliance Test Suite 测试所写 promise 是否合乎规范。
首先,在 promise 实现的代码中,增加以下代码:
Promise.defer = Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
npm i -g promises-aplus-tests
promises-aplus-tests myPromise.js
由于规范里并没有规定catch
、Promise.resolve
、Promise.reject
、Promise.all
等方法。所以我们的实现可以通过全部的 872 个测试。那接下来,我们就看一看Promise
的这些常用方法。
Promise 其他方法实现
1、catch 方法
catch
方法是对then
方法的封装,只用于接收reject(reason)
中的错误信息。因为在then
方法中onRejected
参数是可不传的,不传的情况下,错误信息会依次往后传递,直到有onRejected
函数接收为止,因此在写promise
链式调用的时候,then
方法不传onRejected
函数,只需要在最末尾加一个catch()
就可以了,这样在该链条中的promise
发生的错误都会被最后的catch
捕获到。
catch(onRejected) {
return this.then(null, onRejected);
}
2、done 方法
catch
在promise
链式调用的末尾调用,用于捕获链条中的错误信息,但是catch
方法内部也可能出现错误,所以有些promise
实现中增加了一个方法done
,done
相当于提供了一个不会出错的catch
方法,并且不再返回一个promise
,一般用来结束一个promise
链。
done() {
this.catch(reason => {
console.log('this is done method', reason);
throw reason;
});
}
3、finally 方法
finally
方法用于无论是resolve
还是reject
,finall
y 的参数函数都会被执行。
Promise.prototype.finally = function (callback) {
return this.then((value) => {
return Promise.resolve(callback()).then(() => {
return value;
});
}, (err) => {
return Promise.resolve(callback()).then(() => {
throw err;
});
});
}
4、Promise.all 方法
Promise.all
方法接收一个promise
数组,返回一个新promise2
,并发执行数组中的全部promise
,所有promise
状态都为resolved
时,promise2
状态为resolved
并返回全部promise
结果,结果顺序和promise
数组顺序一致。如果有一个promise
为rejected
状态,则整个promise2
进入rejected
状态。
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
let index = 0;
let result = [];
if (promises.length === 0) {
resolve(result);
} else {
function processValue(i, data) {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
}
for (let i = 0; i < promises.length; i++) {
//promises[i] 可能是普通值
Promise.resolve(promises[i]).then((data) => {
processValue(i, data);
}, (err) => {
reject(err);
return;
});
}
}
});
}
5、Promise.race 方法
Promise.race
方法接收一个promise
数组, 返回一个新promise2
,顺序执行数组中的promise
,有一个promise
状态确定,promise2
状态即确定,并且同这个promise
的状态一致。
Promise.race = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
return;
} else {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((data) => {
resolve(data);
return;
}, (err) => {
reject(err);
return;
});
}
}
});
}
6、Promise.resolve 方法/Promise.reject
Promise.resolve
用来生成一个rejected
完成态的promise
,Promise.reject
用来生成一个rejected
失败态的promise
。
Promise.resolve = function (param) {
if (param instanceof Promise) {
return param;
}
return new Promise((resolve, reject) => {
if (param && param.then && typeof param.then === 'function') {
setTimeout(() => {
param.then(resolve, reject);
});
} else {
resolve(param);
}
});
}
Promise.reject = function (reason) {
return new Promise((resolve, reject) => {
reject(reason);
});
}
常用的方法基本就这些,Promise
还有很多扩展方法,这里就不一一展示,基本上都是对then
方法的进一步封装,只要你的then
方法没有问题,其他方法就都可以依赖then
方法实现。
Promise 面试相关
面试相关问题,来自 sf。
1、简单介绍下 Promise。
Promise
是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise
对象。有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
也可以简单介绍
promise
状态,有什么方法,callback
存在什么问题等加分项:熟练说出 Promise 具体解决了那些问题,存在什么缺点,应用方向等等。
2、实现一个简单的,支持异步链式调用的 Promise 类。
加分项:基本功能实现的基础上有
onResolved/onRejected
函数异步调用,错误捕获合理等亮点。
3、Promise.then 在 Event Loop 中的执行顺序
JS
中分为两种任务类型:macrotask
和microtask
,其中macrotask
包含:主代码块,setTimeout
,setInterval
,setImmediate
等(setImmediate
规定:在下一次Event Loop
(宏任务)时触发);microtask
包含:Promise
,process.nextTick
等(在node
环境下,process.nextTick
的优先级高于Promise
)
Event Loop
中执行一个macrotask
任务(栈中没有就从事件队列中获取)执行过程中如果遇到microtask
任务,就将它添加到微任务的任务队列中,macrotask
任务执行完毕后,立即执行当前微任务队列中的所有microtask
任务(依次执行),然后开始下一个macrotask
任务(从事件队列中获取)
浏览器运行机制可参考这篇文章
加分项:扩展讲述浏览器运行机制。
4、阐述 Promise 的一些静态方法。
Promise.deferred
、Promise.all
、Promise.race
、Promise.resolve
、Promise.reject
等
5、Promise 存在哪些缺点。
1、无法取消Promise
,一旦新建它就会立即执行,无法中途取消。
2、如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。
3、吞掉错误或异常,错误只能顺序处理,即便在Promise
链最后添加catch
方法,依然可能存在无法捕捉的错误(catch
内部可能会出现错误)
4、阅读代码不是一眼可以看懂,你只会看到一堆then
,必须自己在then
的回调函数里面理清逻辑。
6、使用 Promise 进行顺序(sequence)处理。
1、使用async
函数配合await
或者使用generator
函数配合yield
。
2、使用promise.then
通过for
循环或者Array.prototype.reduce
实现。
function sequenceTasks(tasks) {
function recordValue(results, value) {
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
return tasks.reduce(function (promise, task) {
return promise.then(() => task).then(pushValue);
}, Promise.resolve());
}
- 评分标准:说出任意解决方法即可,其中只能说出
async
函数和generator
函数的可以得到 20%的分数,可以用promise.then
配合for
循环解决的可以得到 60%的分数,配合Array.prototype.reduce
实现的可以得到最后的 20%分数。
7、如何停止一个 Promise 链?
在要停止的promise
链位置添加一个方法,返回一个永远不执行resolve
或者reject
的Promise
,那么这个promise
永远处于pending
状态,所以永远也不会向下执行then
或catch
了。这样我们就停止了一个promise
链。
Promise.cancel = Promise.stop = function() {
return new Promise(function(){})
}
8、Promise 链上返回的最后一个 Promise 出错了怎么办?
catch
在promise
链式调用的末尾调用,用于捕获链条中的错误信息,但是catch
方法内部也可能出现错误,所以有些promise
实现中增加了一个方法done
,done
相当于提供了一个不会出错的catch
方法,并且不再返回一个promise
,一般用来结束一个promise
链。
done() {
this.catch(reason => {
console.log('done', reason);
throw reason;
});
}
加分项:给出具体的
done()
方法代码实现
9、Promise 存在哪些使用技巧或者最佳实践?
1、链式promise
要返回一个promise
,而不只是构造一个promise
。
2、合理的使用Promise.all
和Promise.race
等方法。
3、在写promise
链式调用的时候,then
方法不传onRejected
函数,只需要在最末尾加一个catch()
就可以了,这样在该链条中的promise
发生的错误都会被最后的catch
捕获到。如果catch()
代码有出现错误的可能,需要在链式调用的末尾增加done()
函数。
至此.
总结
Promise 作为所有 js 开发者的必备技能,其实现思路值得所有人学习,通过这篇文章,希望小伙伴们在以后编码过程中能更加熟练、更加明白的使用 Promise。
参考链接:
http://liubin.org/promises-book https://github.com/xieranmaya/blog/issues/3 https://segmentfault.com/a/1190000016550260