Promise 对于前端来说,是个老生常谈的话题,Promise 的出现解决了 js 回调地狱(Callback Hell)的问题。
Promise 非常好用,不过要自己去理解它的源码实现可以说是非常蛋疼,本文尝试换个角度,从 Promise 的 使用角度 v.s 源码角度 来剖析源码具体实现,给你一个直观清晰的解释。
目前市面上有很多 Promise 库,但其最终实现都要遵从 Promise/A+ 规范,这里对规范不做解读,有兴趣的可以查看链接内容。Promise/A+规范链接Promise/A+规范中文链接
方便讲解找了一个极其轻量级的 Promise polyfill 实现解析, 源码地址 promise-polyfill,本文就从它开始分析源码。
先看一下 API 列表,相信这些方法你都了解过,如若没有请自行面壁思过(不了解的话前端面试一般就 over 了...):
Promise // 构造函数
Promise.prototype.then
Promise.prototype.catch
Promise.prototype.finally
// 静态方法
Promise.resolve
Promise.reject
Promise.race
Promise.all
咱们先看一下构造函数的样子。
从使用角度:Promise 的第一步当然是构造实例了,传入 Function 形参,形参接收两个 Function 类型参数 resolve
, reject
const asyncTask = () => {};
const pro = new Promise((resolve, reject) => {
asyncTask((err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
从源码角度:对比这个使用,我们看一下对应的 源码部分,内容有点儿长,请耐心阅读(关键部分我都给了中文注释)
/** 这玩意就是构造函数? **/
function Promise(fn) {
/** 常规操作,如果不是 Promise 实例,就先甩锅(报错)**/
if (!(this instanceof Promise))
throw new TypeError('Promises must be constructed via new');
/** 如果入参 `fn` 不是函数,也报错,“我不管,就是你的错” **/
if (typeof fn !== 'function') throw new TypeError('not a function');
/** 初始化一系列状态变量,想象成各种监视 Promise 内部运转行为的各种监视器 **/
this._state = 0;
this._handled = false;
this._value = undefined;
this._deferreds = [];
/** 开始调用传入 `fn` 函数,doResolve 方法解释在下方 ? **/
doResolve(fn, this);
}
/** 大伙儿咱们继续 **/
function doResolve(fn, self) {
/** 先初始化 `done` 变量为 false,这个 `done` 变量挺有用的,确保 `resolve` 和 `reject` 只执行一次 **/
var done = false;
try {
/** 立即执行传入的 fn(resolve,reject),注意这个 “立即执行”,敲黑板,这是考试重点!**/
fn(
/** 这里是 fn 的 resolve 回调**/
function(value) {
/** done 变量发挥了它的作用 **/
if (done) return;
done = true;
/** 这里是 Promise 内部 resove 方法的调用,注意区分!!这个函数待会儿会讲**/
resolve(self, value);
},
/** 这里是 fn 的 reject 回调**/
function(reason) {
/** done 变量再一次发挥了它的作用 **/
if (done) return;
done = true;
/** 同样这里是 Promise 内部 reject 方法的调用**/
reject(self, reason);
}
);
} catch (ex) {
if (done) return;
done = true;
reject(self, ex);
}
}
总结一下:
Promise
必须通过构造函数实例化来使用fn
在 doResolve
方法内是 立即调用执行 的,并没有异步(指放入事件循环队列)处理doResolve
内部针对 fn
函数的回调参数做了封装处理,done
变量 保证了 resolve reject 方法只执行一次,这在后面说到的 Promise.race()
函数实现有很大用处。一言以蔽之:Promise 的构造函数是比较 “鸡贼” 的,如果你不用 Promiser 而直接调 fn(resolve,reject)
方法就是普通的函数执行,但如果你将这个 fn
函数传入 Promise 构造函数,Promise 会帮你立即执行你的 fn
,但是!它会 “狸猫换太子”,把你传给 fn
的 resolve
和 reject
更换成它自己内部的!—— 这样就方便操控 resolve 和 reject 的执行时机,顺带注入很多状态变量来监控运行状态;
Promise 实例的内部变量介绍,就是刚说的想象成各种监视 Promise 内部运转行为的各种 “监视器”:
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
_state | Number | 0 | Promise 内部状态码,枚举值,可能值 0-3 |
_handled | Boolean | false | onFulfilled,onRejected是否被处理过 |
_value | Any | undefined | Promise 内部值,resolve 或者 reject返回的值 |
_deferreds | Array | [] | 存放 Handle 实例对象的数组,缓存 then 方法传入的回调 |
_state
枚举值_state
枚举值类型及其解释如下:
_state === 0 // pending,当前 Promise 正在执行中
_state === 1 // fulfilled, 表示执行了 `resolve` 函数,并且 `_value` instanceof Promise === true
_state === 2 // rejected, 表示执行了`reject` 函数
_state === 3 // fulfilled, 执行了 `resolve 函数,并且_value instanceof Promise === false
注意:这里 _state
区分了 1 和 3 两种状态,下面会解释原因,这里留个悬念。
这个 _state
枚举值和其所代表的的含义,最好记住,下面源码讲解中会反复查询这个变量值的含义。(我估计你后续还是会反复跳到这一小节查看的 ?)
Handler
构造函数/**
* Handle 构造函数
* @param onFulfilled resolve 回调函数
* @param onRejected reject 回调函数
* @param promise 下一个 promise 实例对象
* @constructor
*/
function Handler(onFulfilled, onRejected, promise) {
this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
this.promise = promise;
}
这个构造函数没有多少内容,作用也很直接,就是将 onFulfilled
、onRejected
和 promise
三个内容 “打包起来” 作为一个整体方便后面调用 —— 这就是面向对象编程中“封装” 特性的体现。(额...那个...编程我会了,请问对象在哪儿领?)
_deferreds
数组想象你中午去热门餐厅吃饭,等位的人很多,此时服务员会让你取个号,等叫到你的号了再入馆点餐。
这个_deferreds
数组的用法和餐厅的叫号等位功能是一模一样的:
pro.then(onFulfilled, onRejected)
传入的两个函数不会立即执行;类比排位等号场景,当还没叫你号时,服务员并不关心你是否真的要点餐吃饭(onFulfilled),还是你因为前方排队人数太多而放弃点餐(onRejected),你的号码只是作为一个冰冷的数字放在等号系统里resolve
或者 reject
触发调用后,才会去 forEach 这个 _deferreds
数组中的每个 Handle
实例去处理对应的 onFulfilled
, onRejected
方法。类比排位等号场景,只有当服务员喊到你的号的时候,她才会关心这个号码的主人是还在坚持等位(onFulfilled)呢,还是已经放弃了(onRejected)看到这里你还没放弃,那么恭喜你,距离吊打面试官的境界又进一步了!
resolve
/ reject
/ finale
方法上面说到,doResolve
内部做了 fn
的 立即执行(再强调一次),并保证 resolve
和 reject
方法只执行一次。
resolve
和 reject
那么,我们接下来说说 resolve
和 reject
内部具体做了什么:
function resolve(self, newValue) {
try {
/** resolve 的值不能为本身 this 对象,不然要死循环了;虽说你可以狠起来连自己都打,但在 Promise 里不行 **/
if (newValue === self)
throw new TypeError('A promise cannot be resolved with itself.');
/** 如果被 resolve 值为 Promise 对象的情况,特殊处理 **/
if (
newValue &&
(typeof newValue === 'object' || typeof newValue === 'function')
) {
var then = newValue.then;
if (newValue instanceof Promise) {
/** 如果是 promise 对象,_state = 3 **/
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
/** 兼容类 Promise 对象的处理方式,对其 then 方法继续执行 doResolve **/
doResolve(bind(then, newValue), self);
return;
}
}
/** 被 resolve 的为正常值时的流程,_state = 1 **/
self._state = 1;
self._value = newValue;
finale(self);
} catch (e) {
reject(self, e);
}
}
function reject(self, newValue) {
self._state = 2;
self._value = newValue;
finale(self);
}
虽说 resolve
看上去比较长,但你会发现它函数核心就三句代码,和 reject
是一致的:
self._state
newValue
赋值给 self._value
finale
方法毕竟 resolve
和 reject
这两个函数的作用是等同的,所以操作的内容本质也必然等同,都是 三步走 —— 将变量转换成内部变量,方便 Promise 内部不同函数之间消费。
其实本文的读者内涵和这两个方法的 三步走 也有着异曲同工之妙:各位小哥哥小姐姐在读完本篇内容记得一键三连:评论、转发、再看!!
finale
我们回来继续分析,由于 resolve
和 reject
这两个方法最终都会调用 finale
方法(只是_state
状态会有所不同),所以让我们探究一下 finale
方法:
/** finale 函数核心就是调用 Promise 内部的 handle 消费 self._deferreds 队列 **/
function finale(self) {
/** Promise reject 时,如果此时 then 方法没提供 reject 回调函数参数 或者 未实现 catch 函数,就给警告 —— 你想想,作为一个 Promise 如果不知错就改,那还不得给个警告?**/
if (self._state === 2 && self._deferreds.length === 0) {
Promise._immediateFn(function() {
if (!self._handled) {
/** 这里给警告 **/
Promise._unhandledRejectionFn(self._value);
}
});
}
/** 还记得上面等号点餐的例子不?这里相当于服务员执行 “叫号” 的操作 **/
for (var i = 0, len = self._deferreds.length; i < len; i++) {
/** self._deferreds[i] 存放的是 then 方法传入的 onFulfilled, onRejected 函数,类比于每个手持等位号的顾客 **/
/** 这个 handle 方法是核心中的核心,待会儿专门有一节来讲 **/
handle(self, self._deferreds[i]);
}
/** 处理完就废弃掉这个队列,类比于当点餐等位号都叫完了,喊号的服务员也就可以下班休息了 **/
self._deferreds = null;
}
以刚才的排队点餐为例,这个 finale
函数相当于让服务员执行 “叫号” 操作,每个顾客需要看一下自己手里的号,从而做出不同的选择,要么沉默,要么喊“这是我的号~”
resolve
, reject
是由用户在异步任务里面触发的回调函数 ,在调用 resolve
、reject
方法有以下几点注意事项。
注意事项 1:
newValue 不能为当前的 this 对象,即下面的这样写法是错误的,分分钟给你抛出一个错误:
const pro = new Promise((resolve)=>{
setTimeout(function () {
resolve(pro);
},1000)}
);
pro.then(data => console.log(data)).catch(err => {console.log(err)});
因为 resolve
做了 try catch
的操作,直接会进入 reject 流程。
注意事项 2:
newValue 可以为另一个 Promise 对象类型实例, resolve
的值返回的是另一个 Promise 对象实例的内部的 _value
,而不是其本身 Promise 对象。即可以这样写:
const pro1 = new Promise((resolve)=>{
setTimeout(function () {
resolve(100);
},2000)});
const pro = new Promise((resolve)=>{
setTimeout(function () {
/** 调用上个 promise 实例对象 **/
resolve(pro1);
},1000)});
pro.then(data => console.log('resolve' + data)).catch(err => {console.log('reject' + err)});
// 输出结果:resolve 100
// data 并不是 pro1 对象
具体原因就在 resolve
方法体内部做了 newValue instanceof Promise
的判断,并将当前的 _state=3
、self._value = newValue
,然后进入 finale
方法体;最后在 handle
方法做了核心处理,这个下面介绍 handle
方法会说到;
注意事项 3:
这里有一个注意点,resolve
的 value
可能是其他框架的 Promise (比如:global.Promise,nodejs 内部的 Promise 实现)构造实例,所以在 typeof then === 'function'
条件下做了 doResolve(bind(then, newValue), self);
的重新调用,继续执行当前类型的 Promise then
方法,即又重新回到了 doResolve
流程。
如果这里的实现方式稍微调整下,即不管newValue是自身的 Promise 实例还是其他框架实现的 Promise
实例,都执行 doResolve(bind(then, newValue), self)
也能行得通,只不过会多执行 then 方式一次,从代码性能上说,上面的实现方式会更好。参照代码如下
function resolve(self, newValue) {
try {
...
if (
newValue &&
(typeof newValue === 'object' || typeof newValue === 'function')
) {
/** 这里简单粗暴处理,无论是 Promise 还是 global.Promise 都直接调用doResolve **/
var then = newValue.then;
if (typeof then === 'function') {
doResolve(bind(then, newValue), self);
return;
}
}
...
}
注意事项 4:当 Promise 出现reject的情况时,而没有提供 onRejected
函数时,内部会打印一个错误出来,提示要捕获错误。看一下两种作死现场:
const pro = new Promise((resolve,reject)=>{
setTimeout(function () {
reject(100);
},1000)});
pro.then(data => console.log(data)); // ? 会报错
pro.then(data => console.log(data)).catch(); // ? 会报错
再看一下正确打开方式:
pro.then(data => console.log(data)).catch(()=>{}); // ? 不会报错
pro.then(data => console.log(data),()=>{}) // ? 不会报错
上面源码解读中,有个 handle
内部方法,是核心中的核心,待会儿专门有一节会讲。
接下来咱们先看一下其他 API 方法的源码。
then
、catch
、finally
方法理解上面两小节的内容,那么理解 then
、catch
、finally
方法方法就水到渠成,小菜一碟~
Promise 实例对象支持 then
方法来处理回调,支持无限链式调用;then
方法第一个参数成功回调,第二个参数失败或者异常回调:
function noop() {}
Promise.prototype.then = function(onFulfilled, onRejected) {
var prom = new this.constructor(noop);
handle(this, new Handler(onFulfilled, onRejected, prom));
return prom;
};
Promise.prototype['catch'] = function(onRejected) {
return this.then(null, onRejected);
};
Promise.prototype['finally'] = function(callback) {
var constructor = this.constructor;
return this.then(
function(value) {
return constructor.resolve(callback()).then(function() {
return value;
});
},
function(reason) {
return constructor.resolve(callback()).then(function() {
return constructor.reject(reason);
});
}
);
};
handle
方法。catch
方法在 then
方法上做了一个简单的封装,所以从这里也可以看出,then
方法的形参并不是必传的,catch
只接收 onRejected
。finally
方法不管是调用了 then
还是 catch
,最终都会执行到 finally
的 callbackhandle
方法上面说了这么多,最终的 resolve
、reject
回调处理都会进入到 handle
方法中,来处理 onFulfilled
和 onRejected
,毫无疑问这个 handle
方法是 Promise 源码中的 C 位。
接下来我们来解读一下这名默默在后台发挥作用的函数,走进它那波澜不惊的内心世界。
前方高能警告,虽说该 handle 源码不长,但阅读该
handle
源码将消耗你不少脑力,请及时补充能量~
先看源码:
/** 该方法你就认为是 setImmediate 的别名 **/
Promise._immediateFn =
(typeof setImmediate === 'function' &&
function(fn) {
setImmediate(fn);
}) ||
function(fn) {
setTimeoutFunc(fn, 0);
};
/** 类比排队等号场景,这个 deferred 就是一个排位号子,该 handle 的操作过程就相当于:服务员检查当前拿号子的人是否可以进入餐厅点菜吃饭 **/
function handle(self, deferred) {
/** 如果当前的self._value instanceof Promise,则将 self._value => self,接下来处理新 Promise,将执行权交给新的 Promise **/
while (self._state === 3) {
self = self._value;
}
/** self._state=== 0 说明还没有执行 resolve || reject 方法,让 deferred 放入等候队列 —— 相当于新来了一个人想就餐,服务员让他取号等位,不能让他插队 **/
if (self._state === 0) {
self._deferreds.push(deferred);
return;
}
/** 如果不是上述情况,标记当前进行的 promise._handled 状态量为 true **/
self._handled = true;
/** 通过事件循环异步来做回调的处理 **/
Promise._immediateFn(function() {
/** 如果自己有onFulfilled||onRejected方法,则执行自己的方法;如果没有,则调用下一个 Promise 对象的onFulfilled||onRejected **/
var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
/** 自己没有回调函数,进入下一个 Promise 对象的回调 **/
if (cb === null) {
(self._state === 1 ? resolve : reject)(deferred.promise, self._value);
return;
}
/** 自己有回调函数,进入自己的回调函数 **/
var ret;
try {
ret = cb(self._value);
} catch (e) {
reject(deferred.promise, e);
return;
}
/** 处理下一个 Promise 的 then 回调方法,ret 作为上一个Promise then 回调 return的值 => 返回给下一个Promise then 作为输入值 **/
resolve(deferred.promise, ret);
});
}
情景 1:self._state === 3
,说明当前 resolve(promise)
方法回传的值类型为 Promise 对象, 即 self._value instanceOf Promise === true
, 将 self=self._value, 即当前处理变更到了新的 Promise 对象上
情景 2:如果当前 promise
对象内部状态是 fulfilled
或者 rejected
,则直接处理 onFulfilled
或者 onRejected
回调;如果仍然是 pendding
状态,则继续等待
这就很好的解释了下面代码中为什么 resolve(pro1)
, pro.then
的回调取的值却是 pro1._value
:
const pro1 = new Promise(resolve=>{
setTimeout(()=>{resolve(100)}, 1000);
}) // 执行耗时1s 的异步任务
pro.then(()=>pro1)
.then(data => console.log(data))
.catch(err => {});
// 输出结果: 正常打印了100,data 并不是当前的 pro1 对象
pro1
内部是耗时1s 的异步任务,此时 self._state === 0
,即内部是 pendding 状态,则将 deferred
对象 push 到 _deferreds
数组里面,然后继续等待 pro1 内部调用 resolve(100)
后,这才继续上面 resolve
方法体执行:
const pro1 = new Promise(resolve=>resolve(100)}) // 执行同步任务
pro.then(()=>pro1)
.then(data => console.log(data))
.catch(err => {});
// 输出结果: 正常打印了 100,data 并不是当前的 pro1 对象
但是如果 pro1
内部是同步任务,立即执行的话,当前的 self._state === 1
,即调过 push 到 _deferreds
数组的操作,执行最后的 onFulfilled
, onRejected
回调,onFulfilled
, onRejected
会被放入到事件循环队列里面执行,即执行到了 Promise._immediateFn
Promise._immediateFn
回调函数放到了事件循环队列里面来执行 这里的 deferred
对象存放了当前的 onFulfilled
和 onRejected
回调函数和下一个 promise 对象。
情景 3:当前对象的 onFulfilled
和 onRejected
,如果存在时,则执行自己的回调;
pro.then(data => data).then(data => data).catch(err => {});
// 正确写法: 输出两次 data
注意:then 方法一定要做 return 下一个值的操作,因为当前的 ret 值会被带入到下一个 Promise 对象,即 resolve(deferred.promise, ret)。如果不提供返回值,则第二个 then 的 data 会变成 undefined,即这样的错误写法:
pro.then(data => {}}).then(data => data).catch(err => {});
// 错误写法: 第二个 then 方法的 data 为 undefined
如果 onFulfilled
和 onRejected
回调不存在,则执行下一个 promise 的回调并携带当前的 _value
值。即可以这样写:
pro.then().then().then().then(data => {}).catch(err => {});
// 正确写法: 第四个 then 方法仍然能取到第一个pro 的内部_value 值
// 当然前面的三个 then 写起来毫无用处
所以针对下面的情况:当第一个 then 提供了 reject
回调,后面又跟了个 catch
方法。当 reject
时,会优先执行第一个 Promise 的 onRejected
回调函数,catch
是在下一个 Promise 对象上的捕获错误方法:
pro.then(data => data,err => err).catch(err => err);
最终总结: resolve 要么提供带返回值的回调,要么不提供回调函数
核心的 handle
方法已经到这里已经讲完了,好好消化。。。
最后接下来还剩下两个静态方法,理解起来稍微轻松一些。
Promise.race = function(values) {
return new Promise(function(resolve, reject) {
for (var i = 0, len = values.length; i < len; i++) {
/** 因为doResolve方法内部 done 变量控制了对 resolve reject 方法只执行一次的处理 **/
/** 所以这里实现很简单,清晰明了,最快的 Promise 执行了 resolve||reject,后面相对慢的 Promise 都不执行 **/
values[i].then(resolve, reject);
}
});
};
用法
Promise.race([pro1,pro2,pro3]).then()
race 的实现非常巧妙,对当前的 values
(必须是 Promise 数组) for 循环执行每个 Promise 的 then 方法,resolve, reject
方法对于所有 race
中 promise 对象都是公用的,从而利用 doResolve
内部的 done 变量,保证了 最快执行的 Promise 能做 resolve reject 的回调,从而达到了多个Promise race 竞赛的机制,谁跑的快执行谁。
Promise.all = function(arr) {
return new Promise(function(resolve, reject) {
if (!arr || typeof arr.length === 'undefined')
throw new TypeError('Promise.all accepts an array');
var args = Array.prototype.slice.call(arr);
if (args.length === 0) return resolve([]);
var remaining = args.length;
function res(i, val) {
try {
/** 如果 val 是 Promise 对象的话,则执行 Promise,直到 resolve 了一个非 Promise 对象 **/
if (val && (typeof val === 'object' || typeof val === 'function')) {
var then = val.then;
if (typeof then === 'function') {
then.call(
val,
function(val) {
res(i, val);
},
reject
);
return;
}
}
/** 用当前resolve||reject 的值重写 args[i]{Promise} 对象 **/
args[i] = val;
/** 直到所有的 Promise 都执行完毕,则 resolve all 的 Promise 对象,返回args数组结果 **/
if (--remaining === 0) {
resolve(args);
}
} catch (ex) {
/** 只要其中一个 Promise 出现异常,则全部的 Promise 执行退出,进入 catch异常处理 **/
/** 因为 resolve 和 reject 回调有 done 变量的保证只能执行一次,所以其他的 Promise 都不执行 **/
reject(ex);
}
}
for (var i = 0; i < args.length; i++) {
res(i, args[i]);
}
});
};
用法
Promise.all([pro1,pro2,pro3]).then()
all
等待所有的 Promise 都执行完毕,才会执行 Promise.all().then()
回调,只要其中一个出错,则直接进入错误回调,因为对于所有 all 中 promise 对象 reject
回调是公用的,利用 doResolve
内部的 done
变量,保证一次错误终止所有操作。
但是对于 resolve 则不一样, resolve 回调函数通过 res 递归调用自己,从而保证其值 _value
不为 Promise 类型才结束,并将 _value
赋值到 args
数组,最后直到所有的数组 Promise
都处理完毕由统一的 resolve 方法结束当前的 all 操作,进入 then 处理流程。
上面针对 Promise
的所有 api 做了详细的代码解释和使用场景。
完