从零扒一扒 promise
从零扒一扒 promise
在开发过程中,很多时候都在熟练的使用 Promise/A+ 规范,然而有时在使用的时候发现并不是很了解它的底层实现,下面扒一扒它的实现。
本文基于 AngularJs $q
Promises/A+规范
Promise 是 JS 异步编程中的重要概念,异步抽象处理对象,是目前比较流行 Javascript 异步编程解决方案之一。
术语
- 解决 (fulfill): 指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用 fulfill 来表示解决,但在后世的 promise 实现多以 resolve 来指代之。
- 拒绝(reject): 指一个 promise 失败时进行的一系列操作。
- 拒因 (reason): 也就是拒绝原因,指在 promise 被拒绝时传递给拒绝回调的值。
- 终值(eventual value): 所谓终值,指的是 promise 被解决时传递给解决回调的值,由于 promise 有一次性的特征,因此当这个值被传递时,标志着 promise 等待态的结束,故称之终值,有时也直接简称为值(value)。
- Promise: promise 是一个拥有 then 方法的对象或函数,其行为符合本规范。
- thenable: 是一个定义了 then 方法的对象或函数,文中译作“拥有 then 方法”。
- 异常(exception): 是使用 throw 语句抛出的一个值。
异步回调
Promise 解决的就是异步任务处理问题,简单举例如下(假设有一个异步任务 asyncJob1,它执行完成后要执行 asyncJob2):
// asyncJob2 作为参数传给 asyncJob1,在它完成某些操作后调用
asyncJob1(asyncJob2)
// asyncJob1 的伪代码
function asyncJob1(callback) {
// some task to get the 'result'
callback(result);
}
这样做的问题是 callback 的控制权在 asyncJob1 里面了,并且若有多个异步任务将会有回调地狱的问题,如:
asyncJob1(param, function(job1Result) {
asyncJob2(job1Result, function (job2Result) {
asyncJob3(job2Result, function (job3Result) {
alert('we are done');
// ...
}
});
})
控制反转
稍稍把代码修改一下:
function asyncJob1() {
// some task to get the 'result'
// ...
return function (callback) {
callback(result);
}
}
那么调用的方式就是:
asyncJob1()(function (job1Result) {
// ...
});
写代码的时候,「同步」的写法语义更容易理解,即”干完某件事后,再处理另外一件事”,通过 then 方法来实现链式调用:
function asyncJob1() {
// some task to get the 'result'
return {
then: function (callback) {
callback(result);
}
}
}
可按照如下例子来调用,这样看起来就更有序了。
asyncJob1(param).then(function (job1Result) {
return asyncJob2(job1Result);
}).then(function (job2Result) {
return asyncJob3(job3Result);
}).then(function (job3Result) {
// finally done
});
带着上面的思路,接下来实现一个简版的 Promise。
Promise simple demo
上面的例子,都是函数执行完成后同步执行回调,看下面的例子:
var Promise = function () {
var _callback, _result;
return {
resolve: function (result) {
_result = result;
if (_callback) {
_callback(result);
}
_callback = null;
},
then: function (callback) {
_callback = callback;
}
}
};
于是上面的回调实现就可以变成:
function asyncJob1(param) {
var promise = Promise();
setTimeout(function monkey() {
var result = param + ' with job1';
promise.resolve(result);
}, 100);
return promise;
}
asyncJob1('monkey').then(function (result) {
console.log('we are done', result);
});
Promise 支持多个回调
有时在某件事完成之后,可以同时做其他的多件事情,为此修改 Promise,增加回调队列:
var Promise = function () {
var _pending = [], _result;
return {
resolve: function (result) {
_result = result;
if (_pending) {
for (var i = 0, l = _pending.length; i < l; i++) {
_pending[i](_result);
}
}
_pending = null;
},
then: function (callback) {
_pending.push(callback);
}
}
};
于是在 job1 后可以添加多个回调
var job1 = asyncJob1('monkey');
job1.then(function (result) {
console.log('we are done1:', result);
});
job1.then(function (result) {
console.log('we are done2:', result);
});
这样之后可能还不够,因为如果另外一个回调是异步处理的话,可能就没法得到结果了,比如:
var job1 = asyncJob1('monkey');
job1.then(function (result) {
console.log('we are done1:', result);
});
setTimeout(function () {
job1.then(function (result) { // then 调用时已经报错了
console.log('we are done2:', result); // 此处没有打印
});
}, 1000);
可以在 then 中增加一个判断,如果已经 resolve 过了,则直接执行回调:这样处理后上面的'done2'就可以输出了
var Promise = function () {
...
return {
...
then: function (callback) {
if (_pending) {
_pending.push(callback);
} else {
callback(_result);
}
}
}
};
Promise 作用域安全性
以上的 Promise 返回后,外部可以直接访问 then、resolve 这两个方法,然而外部应该只关心 then,resolve 方法不应该暴露出去,防止外部调用 resolve 修改了 Promise 的状态。代码修整如下:
var Deferred = function () {
...
return {
...
promise: {
then: function (callback) {
...
}
}
}
};
以上只列出了修改的代码,可以看出这个改动很小,其实就是给 then 封装多了一层,调用的方式就变成如下:
function asyncJob1(param) {
var defer = Deferred();
setTimeout(function () {
var result = param + ' with job1';
defer.resolve(result);
}, 100);
return defer.promise;
}
asyncJob1('monkey').then(function (result) {
console.log('we are done', result);
});
Promise 链式调用
截到目前为止,promise 原型还不能实现链式调用,比如这样调用的话,第二个 then 就会报错
asyncJob1('monkey').then(function (job1Result) {
return asyncJob2(job1Result);
}).then(function (job2Result) { // <-- 此处的then会报错
console.log('we are done', job2Result);
});
链式调用是promise很重要的特性,为了实现链式调用,我们要实现:
- then方法也返回一个promise
- 返回的promise必须要用传给then方法函数的返回值,来设置(resolve)自己的result。
- 传递给then方法的函数,必须返回promise或值
- 如果返回的是promise,则必须等待这个promise处理后,才设置result
- 如果返回的是值,则直接设置result
先来看看代码实现:
var Deferred = function () {
var _pending = [], _result;
return {
resolve: function (result) {
_result = result;
if (_pending) {
for (var item, r, i = 0, l = _pending.length; i < l; i++) {
item = _pending[i];
r = item[1](_result);
// 如果回调的结果返回的是 promise(有then方法), 则调用 then 方法并将 resolve 方法传入
if (r && typeof r.then === 'function') {
r.then.call(r, item[0].resolve);
} else {
item[0].resolve(_result);
}
}
}
_pending = null;
},
promise: {
then: function (callback) {
// 创建一个新的 defer 并返回, 并且将 defer 和 callback 同时添加到当前的 pending 中
var defer = Deferred();
if (_pending) {
_pending.push([defer, callback]);
} else {
callback(_result);
}
return defer.promise;
}
}
}
};
执行以下代码,我们能得到:we are all done! monkey with job1 with job2 的输出
asyncJob1('monkey').then(function cbForJob1(job1Result) {
return asyncJob2(job1Result);
}).then(function cbForJob2(job2Result) {
console.log('we are all done!', job2Result);
});
Promise 错误分支
以上的 Promise 都是只有成功的 resolve 调用,在使用的 Promise 都能接受 2 个回调:resolve、reject。
为了实现可以 reject,需要引入一个 promise 的状态,记录它是被 resolve 还是 reject 过。
var Deferred = function () {
var _pending = [], _result, _reason;
var _this = {
resolve: function (result) {
if (_this.promise.status !== 'pending') {
return;
}
_this.promise.status = 'resolved';
_result = result;
for (var item, r, i = 0, l = _pending.length; i < l; i++) {
item = _pending[i];
r = item[1](_result);
// 如果回调的结果返回的是 promise(有then方法), 则调用 then 方法并将 resolve 方法传入
if (r && typeof r.then === 'function') {
r.then.call(r, item[0].resolve, item[0].reject);
} else {
item[0].resolve(_result);
}
}
},
reject: function (reason) {
if (_this.promise.status !== 'pending') {
return;
}
_this.promise.status = 'rejected';
_reason = reason;
for (var item, r, i = 0, l = _pending.length; i < l; i++) {
item = _pending[i];
r = item[2](_reason);
if (r && typeof r.then === 'function') {
r.then.call(r, item[0].resolve, item[0].reject);
} else {
item[0].reject(_reason);
}
}
},
promise: {
then: function (onResolved, onRejected) {
// 创建一个新的 defer 并返回, 并且将 defer 和 callback 同时添加到当前的 pending 中
var defer = Deferred();
var status = _this.promise.status;
if (status === 'pending') {
_pending.push([defer, onResolved, onRejected]);
} else if (status === 'resolved') {
onResolved(_result);
} else if (status === 'rejected') {
onRejected(_reason);
}
return defer.promise;
},
status: 'pending'
}
};
return _this;
};
为了简单起见,reject的代码和resolve差不多,可以抽取一下减少多余的代码。
Promise 融入异步
在上面的所有调用中,resolve 或 reject 里的回调调用都是同步的,这取决于回调的实现。如果回调本身是同步的,就可能会出问题。
比如按上面的 promise 的代码,把 job 的调用中的 setTimeout 去掉,就会得不到结果。
function asyncJob1(param, isOk) {
var defer = Deferred();
//setTimeout(function () {
var result = param + ' with job1';
if (isOk) {
defer.resolve(result);
} else {
defer.reject('job1 fail');
}
//}, 100);
return defer.promise;
}
function asyncJob2(param, isOk) {
var defer = Deferred();
//setTimeout(function () {
var result = param + ' with job2';
if (isOk) {
defer.resolve(result);
} else {
defer.reject('job2 fail');
}
//}, 100);
return defer.promise;
}
asyncJob1('monkey', true).then(function (job1Result) {
return asyncJob2(job1Result, true);
}).then(function (job2Result) {
console.log('we are all done!', job2Result); // 无输出
});
调用时机
resolve 和 reject 只有在执行环境堆栈仅包含平台代码时才可被调用 注1
注1 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保 resolve 和 reject 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
这个事件队列可以采用“宏任务(macro - task)”机制或者“微任务(micro - task)”机制来实现。
由于 promise 的实施代码本身就是平台代码(译者注:即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列。
所以我们要确保这些调用都是异步的,这里只是简单地用 setTimeout 来示意处理,这样之后像上面的调用也有结果输出了。
var Deferred = function () {
var _pending = [], _result, _reason;
var _this = {
resolve: function (result) {
if (_this.promise.status !== 'pending') {
return;
}
_result = result;
setTimeout(function () {
processQueue(_pending, _this.promise.status = 'resolved', _result);
_pending = null;
}, 0);
},
reject: function (reason) {
if (_this.promise.status !== 'pending') {
return;
}
_reason = reason;
setTimeout(function () {
processQueue(_pending, _this.promise.status = 'rejected', null, _reason);
_pending = null;
}, 0);
},
promise: {
then: function (onResolved, onRejected) {
var defer = Deferred();
var status = _this.promise.status;
if (status === 'pending') {
_pending.push([defer, onResolved, onRejected]);
} else if (status === 'resolved') {
onResolved(_result);
} else if (status === 'rejected') {
onRejected(_reason);
}
return defer.promise;
},
status: 'pending'
}
};
function processQueue(pending, status, result, reason) {
var item, r, i, l, callbackIndex, method, param;
if (status === 'resolved') {
callbackIndex = 1;
method = 'resolve';
param = result;
} else {
callbackIndex = 2;
method = 'reject';
param = reason;
}
for (i = 0, l = pending.length; i < l; i++) {
item = pending[i];
r = item[callbackIndex](param);
// 如果回调的结果返回的是promise(有then方法), 则调用then方法并将resolve方法传入
if (r && typeof r.then === 'function') {
r.then.call(r, item[0].resolve, item[0].reject);
} else {
item[0][method](param);
}
}
}
return _this;
};
以上仅仅是简版的 Promise,离我们平常用的promise还差很远,仅仅给自己带来的一些思考。
你能手写一个 Promise 吗?
想要手写一个 Promise 则需要了解 Promise/A+ 规范并遵循这个规范。
简易版(入门)
我们是这样来使用 promise:
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {resolve('success');}, 1000);
})
const p2 = p1.then(val => {
console.log(val)
})
通过 Promise/A+ 规范和对其 API 的熟知,实现一个 Promise 的具体思路是:
- 实例化一个 Promise 后,实例对象有一个 then 原型方法;
- 构建 Promise 对象时,需要传入一个 executor 函数,主要的业务流程都在 executor 函数中执行;
- executor 接收两个参数(resolve and reject),执行成功,则调用 resolve 函数,否则调用 reject 函数;
- Promise 一旦执行,状态将不可逆,即只可能是 pending -> fulfilled or rejected。
一个简易版的 Promise 如下:
// 声明一下三种状态值
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
// 通过发布订阅来执行 then
this.onResolvedCallbacks = [];
this.onRejectedCallbacks= [];
let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
// 兼容一下 executor 同步使用的情况
this.status === FULFILLED && onFulfilled(this.value);
this.status === REJECTED && onRejected(this.reason);
if (this.status === PENDING) {
// 这里通过发布订阅的方式,将 onFulfilled 和 onRejected 函数存放到具体状态的 callback 中
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
});
this.onRejectedCallbacks.push(()=> {
onRejected(this.reason);
})
}
}
}
链式调用
链式调用即当 Promise 的 then 函数中 return 了一个值,不管是什么值,我们都能在下一个 then 中获取到,这就是所谓的then 的链式调用。
回到第一个例子,我们改造一下使用链式写法:
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {resolve('success');}, 1000);
})
const p2 = p1.then(val => {
console.log('p2 success:', val);
// 或者 return 一个 promise/非 promise 的值
throw new Error('失败了')
}).then(
data => {
console.log('success again', data);
},
ex => {
console.log('fail again', data);
}
);
结合 Promise/A+ 规范,咱们需要做到:
- promise 的 then 可以链式调用下去,这就需要每次执行完 promise.then 后返回一个新的 promise 实例;
- 如果 then 原型方法参数 fulfilled or rejected 函数返回的是一个非 promise 的值,则传递给下一个 then 的第一个回调函数(fulfilled);
- 如果返回的是一个 promise,那么等待其执行完毕,成功或者失败的值通过 resolve or reject 分别传递给到下一个 then 的 fulfilled or rejected;
大致的伪代码如下:
// status ...
const resolvePromise = (newPromise, ret, resolve, reject) => {
// 如果是非基础类型的数据,另外判断处理
if ((typeof ret === 'object' && ret != null) || typeof ret === 'function') {
try {
// 如果上一个 then 的参数方法返回的是一个 promise,则等待 promise 执行完毕,再暴露执行结果
let then = ret.then;
if (typeof then === 'function') {
// 返回的 promise 执行后,将成功或者失败的结果暴露给当前 then 的 fulfilled or rejected
then.call(ret, succ => {
resolve(succ);
}, fail => {
reject(fail);
});
} else {
// 如果是引用类型(非 promise 的对象),则直接 resolve
resolve(ret);
}
} catch (ex) {
reject(ex);
}
} else {
// 如果上一个 then 参数中的函数返回值 ret 是个基本类型的值则直接 resolve
resolve(ret)
}
}
class Promise {
constructor(executor) {
// this.xxxs ...
let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
}
let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
// 兼容 onFufilled,onRejected 不传值
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {};
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
// then 最终返回一个新的 promise 实例
let newPromise = new Promise((resolve, reject) => {
// ...
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let ret = onFulfilled(this.value);
resolvePromise(newPromise, ret, resolve, reject);
} catch (e) {
reject(e)
}
}, 0);
});
this.onRejectedCallbacks.push(()=> {
setTimeout(() => {
try {
let ret = onRejected(this.reason);
resolvePromise(newPromise, ret, resolve, reject)
} catch (e) {
reject(e)
}
}, 0);
});
}
});
return newPromise;
}
}
注,使用 setTimeout 模拟了微任务
静态方法
常用的静态方法有 Promise.resolve(),Promise.reject(),可实现如下:
static resolve/*or reject**/(data) {
return new Promise((resolve, reject) => {
resolve(data); // or reject(data);
})
}
Promise.all
实现思路是:
- all 返回一个 promise;
- 如果每一个并发的 promise 都正常返回,则把值维护到一个数组中
- 否则直接 reject 即可
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let resultArr = [];
let orderIndex = 0;
const processResultByKey = (value, index) => {
resultArr[index] = value;
if (++orderIndex === promises.length) {
resolve(resultArr);
}
}
for (let i = 0; i < promises.length; i++) {
let value = promises[i];
if (value && typeof value.then === 'function') {
value.then((value) => {
processResultByKey(value, i);
}, reject);
} else { // 如果并发的数组并不是一个 promise,则直接加到记录结果的数组中即可
processResultByKey(value, i);
}
}
});
}
Promise.race
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
let val = promises[i];
if (val && typeof val.then === 'function') {
val.then(resolve, reject);
} else {
resolve(val)
}
}
});
}
- 用来处理多个并发的值,遍历执行,如果是非 promise 实例,直接 resolve 即可;
- 否则的话,等待 promise 执行完毕,任何一个有结果直接 resolve or reject 即可;
- 注,promise 里边有控制仅 resolve 或 reject 一次了,这里不用再判断。