目前看到的好多博客都是教怎么使用的,没有涉及到他的实现以及原理部分,篇幅较短,知识比较零碎,想出一个较完整的 Promise 的教程,帮助新上手的同学快速入门,当然具体的流程也可以去看 Promise A+ 规范,但是 Promise A+ 规范文档 逻辑过于复杂,一时半会串不起来,就算了解了部分,不一会可能又看到另个一个规范把自己串好的逻辑打破,就又得重新串了,所以,要学就要所有规范一起学,长痛不如短痛,xdm,冲鸭!
AboveAll
如果您已经熟悉了观察者模式、高阶函数、Promise 各种静态方法等知识点,或者下面代码的打印结果以及原因,这篇文章可以直接略过,相反 2740 字的文章还请耐心看完哈,持续完善晦涩点。
let temp = new Promise((reslove, reject) => {
console.log('1')
reslove('value1')
})
let p1 = new Promise((reslove, reject) => {
console.log('2')
reslove(temp)
}).then((res) => {
console.log('a1')
}).then((res) => {
console.log('a2')
}).then((res) => {
console.log('a3')
})
let p2 = new Promise((reslove, reject) => {
console.log('3')
reslove('valu2e')
}).then((res) => {
console.log('b1')
}).then((res) => {
console.log('b2')
}).then((res) => {
console.log('b3')
})
流程
便于快速理解,暂且将 Promise 划分为三步流程,解析流程、then 流程、观察流程
解析流程
声明了一个 Promise ,当拿到回调值,需要用 reslove 或 reject 来将值注入到 Promise 内,并其使状态发生转变,一般写法如下:
let p = new (function(reslove, reject){
reslove(value)
})
- 调用 reject (value)时,会直以 value 为拒因来将此状态改变 rejected,
- 调用 reslove(value) 时,会将 value 作为结果,并改变 Promise 的状到 fulfilled ,
- 如果 value 是对象,且有 then 方法,称之为 thenable ,那他可能是个 Promise ,尝试继续解析,调用该 value 的 then 方法,直到拿到非 thenable 值,怎样继续解析 thenabel 这块在 then 详细介绍。
- 如果 value 是非 thenable 值,则直接将此值注入
源码状态图
图中红色和绿色部分发生了状态的改变,由 pending 状态分别转变成失败(rejected)状态和成功(fulfilled)状态,在发生状态转变之前历经一些列的判断,
注入非 thenable 的值
let p = new Promise(function (resolve, reject) {
resolve('成功')
})
// p 状态改变为 fulfilled,值为 '成功'。
因为 '成功' 不是 thenable,就直接将‘成功’ 以 fulfilled 的状态注入到 promise 里,最终的 Promise p 如下
注入 thenable 值
let p2 = new MyPromise(function (resolve, reject) {
resolve('haha')
})
let p = new MyPromise(function (resolve, reject) {
resolve(p2)
})
注入的是 thenable 的值,如注入了一个 Promise p2,p2 本身是具有 then 方法,所以不能作为终值来进行注入,需要调用 p2 的 then 方法拿到最终值,再将最终值注入到本 Promise 中,这块需要了解 then 流程和观察者流程(难点①)。
then 流程
Promise 的 then 方法使用来获取 Promise 的终值的,会根据 Promise 的状态调用不同的方法,如下面代码
let p = new Promise(function(reslove, reject){
reslove('hah')
})
p.then((res) => {
console.log(res) // 'hah'
},(err) => {
console.log(err)
})
- 当 js 引擎执行到 p.then 方法时,会将 then 方法的两个参数添加到微任务队列中,在真正执行的时候,必定先返回个新 Promise
- fulfilled 状态:会将终值作为 then 方法第一个回调函数的参数传递
- 根据 then 方法带一个回调函数的返回值的类型决定新 Promise 的状态和终值
- 返回 thenable ,会继续解析该 thenable(难点②)
- 返回非 thenable ,直接以该值以 fulfilled 状态注入_ _
- 根据 then 方法带一个回调函数的返回值的类型决定新 Promise 的状态和终值
- rejected 状态:会将结果通过第二个参数数传递
- rejected 失败状态,Promise 链条将会中断,就不会有后续的结果;
- Pending 状态:会将 then 方法的参数(两个回调函数)作为观察者添加到 原Promise 的队列中,等待原理 Promise 状态决定后,会通知所有观察者执行,注入值和改变状态。
- fulfilled 状态:会将终值作为 then 方法第一个回调函数的参数传递
源码流程图
非 Pending 状态调用 then 方法
非 Pending 状态也就是状态已经改变,这时候调用 then 方法就直接可以拿到终值,如下面代码
let p = new MyPromise(function (reslove, reject) {
console.log('1')
reslove('hah')
})
let temp = p; // 转存下 p 到 temp
p = p.then((res) => {
console.log(res)
return 'bbb'
}, (err) => {
console.log(err)
})
// 将会返回一个新的 promise
console.log('2')
console.log(temp === p) // false
先进行 Promise 解析流程,将非 thenable 的值直接注入,接下来调用 then 方法
Promise 除了解析流程外,then 流程和观察者流程都是微任务,根具这个特点,上面代码打印的顺序应该为 '1' '2' 'hah'。
Pending 状态时调用 then 方法
处于 Pending 状态也就是说,结果还没有决定时就调用了 then 方法,这种状态也是比较常见的,这也是异步操作的基本场景,
let p = new Promise(function(reslove, reject){
setTimeout(()=>{
reslove('两秒后显示')
}, 2000)
})
p.then((res)=>{
console.log(res) // 两秒后打印
},(err)=>{
console.log(err)
})
模拟了下异步请求,当执行到 p.then 时,此时的 p 还没有注入终值,但为了让链条不断掉,就必须得继续,所以 Promise 内部将 then 方法的回调函数,也就是成功失败的回调函数包起来作为观察者,推入 p 的观察者队列中,等待 p 的状态改变在通知观察者,根据 p 状态做出选择,这就类似非 pending 状态调用 p .then 了, 非常巧妙的利用了观察者解决了如何链接没有注入终值的 Promise 。
观察者流程
Js 引擎执行到 p,then 流程时,p 的终值未确定,就将成功失败的回调函数包起来作为观察者,存放如 P 的观察者队列,这在上文都有讲过,当 p 的状态发生改变后,就通知观察者队列,让每个观察者根据自己的值和状态去决定执行哪一个回调函数。
这块其实是暂缓执行 then 流程,内部流程和 then 流程基本是一样的,p 的状态改变后在去通知观察者就相当于走了then 流程的非 Peding 状态调用 then 方法那步。
按什么规则添加观察者
为了解按什么规则添加观察者的,尝试写出面代码的打印。
let p1 = new Promise(function(reslove, reject){
setTimeout(()=>{
reslove('1秒后显示')
}, 1000)
})
p1.then((res)=>{
console.log('p1 then:',res)
})
let p2 = new Promise(function(reslove, reject){
console.log(2)
reslove(p1)
})
p2.then((res)=>{
console.log('p2 then:',res)
})
let p4 = new Promise(function(reslove, reject){
reslove(p1)
})
p4.then((res)=>{
console.log('p4 then:',res)
})
let p3 = new Promise(function(reslove, reject){
reslove(p2)
})
p3.then((res)=>{
console.log(res)
},(err)=>{
console.log(err)
})
console.log(1)
首先 P1 模拟异步请求数据, 现在上每层都索引上层,当上层数据改变,会通知下层都更改数据,绿色点好说, then 流程已经讲过,就是将自己的回调方法作为观察者添加到上层 Promsie 中,等待图示上层 Promise 状态发生改变后在去通知。
按上图逻辑,通知厚的回调方法排序会是这样推入微任务队列的,如下微任务队列和打印结果。
p.then 这样添加时可以理解的但是,p2 Promise 注入 P1 是怎样添加的?
两个 Promise 同时执行情况
let p1 = new Promise((reslove, reject) => {
reslove('suc')
}).then((res) => {
console.log(res, 'a1')
}).then((res) => {
console.log(res, 'a2')
}).then((res) => {
console.log(res, 'a3')
}).then((res) => {
console.log(res, 'a4')
})
let p2 = new Promise((reslove, reject) => {
reslove('suc')
}).then((res) => {
console.log(res, 'b1')
}).then((res) => {
console.log(res, 'b2')
}).then((res) => {
console.log(res, 'b3')
}).then((res) => {
console.log(res, 'b4')
})
一定要记住的是:
- then 流程在第一次宏任务执行完成后就已经返回完毕,就可以理解为链接起来所有的 Promise 了
- 当状态改变时才会通知观察者,同时,也会将该微任务添加到微任务队列中
根据上述两点就可与了解上面的代码执行流程了
因为第一次执行宏任务的时候,所有 Promise 都已经返回,并且已经链接起来,这块没有画出来(可以尝试在浏览器上调试下),直接看微任务队列,虽然宏任务时已经链接起来,但是第一次宏任务只改变了两个 Promise 的状态,所以目前微任务队列只有两个微任务,第一个微任务执行完改变另一个 Promise 状态就将其观察值的微任务添加到微任务队列中,每次微任务队列同时存在两个微任务,那样持续添加就形成了交叉打印了。
对比在解析时将所有任务直接添加到宏任务队列,微任务队列边执行边微任务添加到微任务队列中这样高优先级的特性就有了上述代码交叉打印这样的结果。
注入 thenable 时如何添加观察者
瞧瞧下面代码的打印顺序:
let temp = new Promise((reslove, reject) => {
console.log('1')
reslove('value1')
})
let p1 = new Promise((reslove, reject) => {
console.log('2')
reslove(temp)
}).then((res) => {
console.log('a1')
}).then((res) => {
console.log('a2')
}).then((res) => {
console.log('a3')
})
let p2 = new Promise((reslove, reject) => {
console.log('3')
reslove('valu2e')
}).then((res) => {
console.log('b1')
}).then((res) => {
console.log('b2')
}).then((res) => {
console.log('b3')
})
如上如 7 行注入了 temp,是一个 Promise 相当于注入了 thenable ,在内部需要调用 thenable 的 then 方法来取接受此 Promise 的值,所以第 7 行相当于多了一步 temp.then(onfulfilled, onRejected) ,为其添加了观察者,和 then 流程一样的道理。
源码实现
函数方式实现源码
var
PENDING = 'PENDING',
FULFILLED = 'FULFILLED',
REJDECTED = 'REJDECTED'
function MyPromise(fn) {
var state = PENDING,
value = null,
handlers = [];
// 解析流程
doResolve(fn, resolve, reject)
function doResolve(fn, onFulfilled, onRejected) {
let done = false;
try {
fn(function (value) {
if (done) return
done = true;
onFulfilled(value)
}, function (err) {
if (done) return
done = true;
onRejected(err)
})
} catch (err) {
if (done) return
done = true
onRejected(err)
}
}
// 尝试注入 value
function resolve(value) {
try {
var then = getThen(value)
if (then) {
doResolve(then.bind(value), resolve, reject)
return
}
fulfill(value)
} catch (error) {
reject(error)
}
}
// thenable
function getThen(value) {
var t = typeof value;
if (value && (t === 'object' || t === 'function')) {
var then = value.then
if (typeof then === 'function') {
return then
}
}
return null;
}
// 状态改变 FULFILLED
function fulfill(result) {
state = FULFILLED;
value = result
handlers.forEach(handle); // 通知观察者处理
handlers = null
}
// 状态改变 REJDECTED
function reject(err) {
state = REJDECTED;
value = err
handlers.forEach(handle); // 通知观察者处理
handlers = null
}
// 添加/通知 观察者
function handle(handler) {
if (state === PENDING) {
handlers.push(handler)
} else {
if (state === FULFILLED &&
typeof handler.onFulfilled === 'function') {
handler.onFulfilled(value)
} else if (state === REJDECTED &&
typeof handler.onRejected === 'function') {
handler.onRejected(value)
}
}
}
// 模拟异步
this.done = function (onFulfilled, onRejected) {
setTimeout(function () {
handle({
onFulfilled: onFulfilled,
onRejected: onRejected
});
}, 0);
}
// then 流程
this.then = function (onFulfilled, onRejected) {
var self = this;
return new MyPromise(function (resolve, reject) {
return self.done(function (result) {
if (typeof onFulfilled === 'function') {
try {
return resolve(onFulfilled(result));
} catch (ex) {
return reject(ex);
}
} else {
return resolve(result);
}
}, function (err) {
if (typeof onRejected === 'function') {
try {
return resolve(onRejected(err))
} catch (ex) {
return reject(ex)
}
} else {
return reject(err)
}
});
})
}
}
使用 setTimeOut 模拟微任务实现的在晚一些情况可能与原生微任务不同,都是用宏任务队列就没有微任务优先级高这样的概念,就导致如 Promise 无法实现双 Promise 交叉打印这样的情况了(上文的两个 Promise 同时执行)
实现验证
通过 promises-aplus-tests A+规范验证测试
使用函数方式实现,868/872 通过率。
涉及原理
触手能级的原理我们也要懂
函数式编程
函数式编程专门有一文讲解,在 Promise 中只用到了高阶函数,无非就两种,函数作为参数,函数作为返回值传递,如下面代码:
函数作为参数:
function fn1(params){ console.log(params) }
function fn2(fn){}
fn2(fn1){
fn(1) // 打印 1
}
函数作为返回值: 一个函数返回另一个函数,看上去是这个样子,在内部执行栈中最终会将返回值传递给栈底的函数,后调用的函数会压入在栈顶,等待执行完毕后弹出。
function fn1(){
// 当然可以在这里又返回另一个函数。
}
function fn2(){
return fn1()
}
在 Promise 内部的实现用到的高阶函数
doResolve(fn, resolve, reject)
// fn, resolve, reject 都是函数
// fn 里又传递两种方法,最终延缓执行
function doResolve(fn, onFulfilled, onRejected) {
let done = false;
try {
fn(function (value) {
// ...
}, function (err) {
// ...
})
} catch (err) {
// ...
}
}
还有好多地方如返回新 Promise 用到的 done 方法等,可以详细看下上文源码实现。
观察者模式
观察者模式属于行为模式之一,在应一篇文章有详细讲解,简单来讲,观察者模式主要有两个角色
- 主题 subject :被观察的对象
- 观察者 watcher :被通知的角色
首先将 观察者添加到要观察的对象中,当主题的状态发生改变时回去通知观察者,简单代码示例如下:
class subject {
construct(){
this.watchers = Object.create(null)
}
addWatcher (w) {
this.watchers.push(w) // 将观察者添加到观察则会队列中
}
notifyAll(){
this.watchers.forEach((item) => {
item.update()
})
}
}
class Watcher {
construct() {
}
update() {
// 通知后观察者的行为
}
}
在实现源码中,在宏任务执行到 Promise.then 方法时,如果 Promise 状态为已改变状态则将该微任务推入微任务队列中,如果此时 Promise 还处于 Pending 状态,那就将 then 暂时放入 Promise 的观察者队列中,等待下次通知的时候再将此微任务推入到微任务队列中,待微任务执行改变其他 Promise 状态,这样的循环就可以形成链式调用了。 那也就是说观察者模式在 Promise 实现中起到非常重要的作用。
Promise 静态方法实现
Promise.all
传入第一个参数
Promise.myAll = function () {
let args = arguments;
return new Promise((reslove, reject) => {
// 检测第一参数是否可迭代
if (!Object.getPrototypeOf(args[0])[Symbol.iterator]) {
throw new Error(`${args[0]} is not iterable`)
}
const iterable = args[0];
let resArr = [],
idx = 0,
falg = 1;
for (let p of iterable) {
// 根据 p 类型放入结果的数组
if (typeof p.then === 'function') {
p.then((res) => {
resArr[idx++] = res
if (++falg === iterable.length) reslove(resArr)
}, (err) => {
reject(esrr)
})
} else {
resArr[idx++] = p;
falg++;
}
}
})
}
Promise.race
Promise.myRace = function () {
let args = arguments;
return new Promise((reslove, reject) => {
// 检测第一参数是否可迭代
if (!Object.getPrototypeOf(args[0])[Symbol.iterator]) {
throw new Error(`${args[0]} is not iterable`)
}
const iterable = args[0];
let resArr = [];
for (let p of iterable) {
// 立即返回最先决定 Promise
if (typeof p.then === 'function') {
p.then((res) => {
reslove(res)
}, (err) => {
reject(err)
})
}
}
})
}
Promise.any
Promise.myAll = function () {
let args = arguments;
return new Promise((reslove, reject) => {
// 检测第一参数是否可迭代
if (!Object.getPrototypeOf(args[0])[Symbol.iterator]) {
throw new Error(`${args[0]} is not iterable`)
}
const iterable = args[0];
let resArr = [],
idx = 0,
falg = 1;
for (let p of iterable) {
// 根据 p 类型放入结果的数组
if (typeof p.then === 'function') {
p.then((res) => {
reslove(res)
}, (err) => {
resArr[idx++] = res
if (++falg === iterable.length) {
reslove('AggregateError: No Promise in Promise.any was resolved')
}
})
} else {
resArr[idx++] = p;
falg++;
}
}
})
}
Promise.allSettled
Promise.myAllSettled = function () {
let args = arguments;
return new Promise((reslove, reject) => {
// 检测第一参数是否可迭代
if (!Object.getPrototypeOf(args[0])[Symbol.iterator]) {
throw new Error(`${args[0]} is not iterable`)
}
const iterable = args[0];
let resArr = [],
idx = 0,
flag = 1;
for (let p of iterable) {
// 每一次状态发生转变是都要去要检测下所有状态是否都全都转变
if (typeof p.then === 'function') {
p.then((res) => {
flag ++;
resArr[idx++] = {
'status': 'fulfiled',
'value': res
}
if(flag === iterable.length) return reslove(resArr)
}, (err) => {
flag ++;
resArr[idx++] = {
'status': 'rejected',
'reason': err
}
if(flag === iterable.length) return reslove(resArr)
})
} else {
flag ++;
resArr[idx++] = {
'status': 'fulfiled',
'value': p
}
if(flag === iterable.length) return reslove(resArr)
}
}
})
}
资料 & 参考
资料: 源码实现地址:gitee.com/zhangboyu30… 状态图地址:www.processon.com/view/link/6… 观察者模式:
参考: 观察者模式:c.biancheng.net/view/1390.h… MDN Promise:developer.mozilla.org/zh-CN/docs/…