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,finally 的参数函数都会被执行。
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