1. 回调地狱
在讲述Promise之前,我们有必要了解到为什么会有Promise的产生?Promise解决了什么问题?带着这样的问题,我们来看两个🌰:
1.1 模拟回调地狱
现在有个方法 doSth,我想在执行这个方法几次之后执行一个回调函数 callback。再写个方法 workSth,打印一些内容。然后将workSth作为参数之一,并执行方法 doSth,用变量fn来接收,只有在fn执行4次后,才会显示打印的内容。
let doSth = function(t, callbak) {
return function() {
if (--t === 0) {
callbak();
}
}
}
function workSth() {
console.log('Symon is working from home.');
}
let fn = doSth(4, workSth);
fn();
fn();
fn();
fn();
现在,我要在workSth中写入函数行参cb,在打印内容后我执行这个cb,这时候还需要一个方法studySth,那么我希望studySth作为实参传入workSth。也就是说在fn执行四次后,callbak即workSth开始执行,那么它对应的cb即studySth开始执行。
function workSth(cb) {
console.log('Symon is working from home.');
cb();
}
function studySth() {
console.log('Symon has not posted a technical article on Juejin for one week.');
}
let fn = doSth(4, workSth.bind(null, studySth));
那如果在studySth中再传入一个参数呢?那这时候又需要一个方法healthyHabitSth。
function studySth(cb) {
console.log('Symon has not posted a technical article on Juejin for one week.');
cb();
}
function healthyHabitSth() {
console.log('Symon should remember to form the good habit of going to bed early and getting up early.');
}
这时候需要我们对doSth进行改写:
function doSth(t) {
return function() {
if (--t === 0) {
workSth(function() {
studySth(function() {
healthyHabitSth();
});
});
}
}
}
let fn = doSth(4);
输出的结果:
那么如果有更多的sth方法呢?
我们发现上面代码大量使用了回调函数(将一个函数作为参数传递给另个函数)并且有许多 '})' 结尾的符号,使得代码看起来很混乱,呈现金字塔状,我们很亲切地称它为——“回调地狱”。
1.2 基于回调函数的方式封装ajax发送请求
上述情况是模拟的回调地狱,接下来我们在异步请求中看看回调地狱的情形:
这里我们以ajax串形(多个ajax请求间存在依赖,只有上一个请求成功,才能发送下一个请求)的场景来实现一下:
$.ajax({
url: 'api/test1',
method: 'GET',
datatype: 'json',
success(value) {
console.log('第一个请求结果:', value);
$.ajax({
url: 'api/test2',
method: 'GET',
datatype: 'json',
success(value) {
console.log('第二个请求结果:', value);
$.ajax({
url: 'api/test3',
method: 'GET',
datatype: 'json',
success(value) {
console.log('第三个请求结果:', value);
}
});
}
});
}
});
回调地狱简单来说就是回调函数里嵌套回调函数再嵌套...
在没有Promise之前,基于回调函数的方式管理ajax请求,在串行的需求中,很容易产生“回调地狱”。而Promise是ES6中专门用来管理异步编程的,基于它可以避免"回调地狱"问题。
同样的需求,如果用Promise怎么写呢?
const p1 = () => {
return new Promise(resolve => {
$.ajax({
url: 'api/test1',
method: 'GET',
datatype: 'json',
// success(value) {
// resolve(value);
// }
success: resolve
});
})
};
const p2 = () => {
return new Promise(resolve => {
$.ajax({
url: 'api/test2',
method: 'GET',
datatype: 'json',
success: resolve
});
})
};
const p3 = () => {
return new Promise(resolve => {
$.ajax({
url: 'api/test3',
method: 'GET',
datatype: 'json',
success: resolve
});
})
};
p1()
.then(value => {
console.log('第一个请求结果:', value);
return p2();
})
.then(value => {
console.log('第二个请求结果:', value);
return p3();
})
.then(value => {
console.log('第三个请求结果:', value);
});
每个请求都用Promise包裹起来,通过Promise把jquery的异步ajax请求去管理起来。
上述代码可以利用Promise语法糖做到精简效果:
(async function() {
let value = await p1();
console.log('第一个请求结果:', value);
value = await p2();
console.log('第二个请求结果:', value);
value = await p3();
console.log('第三个请求结果:', value);
})()
明明是异步的代码,通过 async、await 写出了同步的效果。
2.初识Promise
2.1 Promise的含义
Promise是ES6新增的内置类,是异步编程的一种解决方案。用来规划异步编程代码,解决回调地狱等问题。
Promise中文翻译过来是“承诺”,意思是在未来某一个时间点承诺返回数据给你。比如说我要和你做个约定,下周末一起去看电影,这就是一个承诺。承诺会有三种状态,一种是实现承诺😄,一种是承诺石沉大海😣,另一种是承诺等待结果中...😓...苦苦等待。
Promise有以下几个特点:
- 不兼容IE浏览器(edge可以兼容)
- 使用的时候是创建这个类的实例:实例具有私有属性,可以使用Promise.prototype上的方法,Promise作为对象存在一些静态的私有属性方法
- 状态不受外界影响。Promise代表一个异步操作,有三种状态:
fulfilled(已成功)、rejected(已失败)和pending(进行中)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。 首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
2.2 基本用法
2.2.1 创建实例
ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。
let p = new Promise(() => {
});
Promise 接受一个函数作为参数,这个函数叫做 executor(执行器)。而该函数有两个参数,分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
let p = new Promise(() => {
console.log(1);
})
console.log(2);
输出的结果是 1、2
executor 必须是一个函数,而且 new Promise 的时候会将其立即执行。
来打印一下 Promise 实例 p:
由上图可以看出,实例 p 有两个内置私有属性,分别是:
- [[PromiseState]]: "pending"/"fulfilled"/"rejected" 实例的状态
- [[PromiseResult]]: undefined 实例的值(成功的结果或者失败的原因)
实例 p 的公共属性方法 Promise.prototype:
- then
- catch
- finally
- Symbol(Symbol.toStringTag): "Promise"
拓展:Q:Symbol(Symbol.toStringTag): "Promise" 是用来干嘛的?
A:检测数据类型
当我们使用 Object.prototype.toString.call() 这个万能的检测数据类型的方法检测 p 实例的时候,得到👇的结果:
得到这样的结果就是通过 Symbol(Symbol.toStringTag) 这个属性来决定的。当前实例对象的该属性的值就对应打印的结果。没有该属性的时候,才会按照内部规则去找其对应所属类。
2.2.2 修改实例的状态和值
修改 Proise 实例的状态和值的方法有三种(或者说Promise实例变成成功/失败的状态取决于哪三种情况):
1.手动执行 executor 函数中的 resolve/reject
在 [executor] 中执行 resolve/reject 都是为了改变 Promise 实例的状态和值,一但状态被改变成 fulfilled/rejected 则不能在改为其他的状态。
let p = new Promise((resolve, reject) => {
resolve('OK');
});
console.log(p);
let p = new Promise((resolve, reject) => {
reject('NO');
});
console.log(p);
总结:
- resolve('OK'); [[PromiseState]]:fulfilled [[PromiseResult]]:'OK'
- reject('NO'); [[PromiseState]]:rejected [[PromiseResult]]:'NO'
2.executor函数中出现报错的代码
[executor] 函数执行报错,Promise实例状态也会发生改变,实例值是报错原因。
- [[PromiseState]]: rejected
- [[PromiseResult]]: 报错原因
let p = new Promise((resolve, reject) => {
console.log(a);
});
console.log(p);
Promise内部做了异常信息捕获(try/catch)
let p = new Promise((resolve, reject) => {
console.log(a)
});
console.log(p);
p.then(() => {}).catch(() => {});
这时候再看控制台,就没有报错了
3.通过.then存放的 onfulfilledCallback/onrejectedCallback 执行是否报错,影响了通过.then返回的那个全新Promise实例是成功/失败
这一种情况在下文中会提到。
2.3 Promise.prototype.then()
2.3.1 then方法的基本使用
Promise 实例具有then方法,也就是说,then方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。
then 方法的第一个参数是状态成功的回调函数 onfulfilledCallback,第二个参数是状态失败的回调函数onrejectedCallback,它们都是可选的。状态改变后执行对应的回调函数并且将 [[PromiseResult]] 的值传递给方法。
let p = new Promise((resolve, reject) => {
resolve('OK');
});
p.then(result => {
console.log('成功 ——>', result);
}, reason => {
console.log('失败 ——>', reason);
});
let p = new Promise((resolve, reject) => {
reject('NO');
});
p.then(result => {
console.log('成功 ——>', result);
}, reason => {
console.log('失败 ——>', reason);
});
2.3.2 Promise中的同步异步问题
接下来探究一下关于 Promise 同步异步的问题,先看道开胃菜:
let p = new Promise((resolve, reject) => {
resolve('OK'); // *
});
console.log(p); // $
resolve 是同步还是异步呢?如果它是异步的,执行 * 处代码后什么效果都没有,继续执行 $ 处代码,那么 p 的状态没有发生改变。
但是通过打印发现 p 的状态发生了改变。所以 resolve 是同步的,也就是说在 [executor] 函数中我们没有管控异步代码。
接下来,我们来看一个稍微复杂一些的场景:
let p = new Promise((resolve, reject) => {
console.log(1);
resolve('OK');
console.log(2);
});
console.log(p); // &
p.then(result => {
console.log('成功 ——>', result);
});
console.log(3);
分析:首先,new Promise 的时候立即执行 [executor] 函数,因为里面是同步任务,所以先输出1,执行 resolve 的时候会同步修改其状态和值,修改完之后按理来说应该通知then方法里面的onfulfilledCallback 执行,但执行了 resolve 之后并没有执行then,因此接下来输出2。来到 & 处代码,因为在执行 resolve 的时候就已经同步修改了实例的状态和值,所以这时候输出的就是实例对象 p,并且是成功状态,值为 'OK'。
接下来执行 p.then(onfulfilledCallback,onrejectedCallback),内部做了两件事:
- 首先把传递进来的
onfulfilledCallback和onrejectedCallback存储起来(存储在一个容器中:因为可以基于then给其存放多个回调函数) - 其次再去验证当前实例的状态
- 如果实例状态是pending,则不做任何的处理
- 如果已经变为 fulfilled/rejected,则会通知对应的回调函数执行(
但是不是立即执行,而是把其放置在 EventQueue 中的微任务队列中)
所以这时先输出后面的同步代码得到3,同步代码都执行完之后,再去将 EventQueue 中的微任务执行。
最终的打印结果:
基于此,我们可以得出一个结论:
promise本身不是异步的,是用来管理异步的,但是then方法是异步的(微任务)
我们难度升级一下,看👇代码:
let p = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
resolve('OK');
console.log(p);
console.log(4);
}, 1000);
console.log(2);
});
console.log(p);
p.then(result => {
console.log('成功 ——>', result);
});
console.log(3);
分析:new Promise 的时候立即执行 [executor] 函数,先输出1,定时器放到异步宏任务,输出2。因为这时候实例状态并没有发生改变且代码执行也没有报错,所以输出的实例 p 的状态为 pending,值为 undefined。接下来,p.then() 接受onfulfilledCallback的时候,状态还是pending,此时只把方法存储起来,不做其他处理。输出后面同步代码3。等1000ms后,把异步宏任务拿出来执行,即执行定时器中的函数 resolve,这时候做了两件事:
- 同步改变实例的状态和值
- 通知之前基于then存放的
onfulfilledCallback执行(异步的微任务:也是把执行方法的事情放置在 EventQueue 中的微任务队列中)
通知基于then存放的onfulfilledCallback执行是个异步伟任务,所以先输出实例 p,状态是 fulfilled,值为 'OK'。然后输出4,最后执行 EventQueue 中的微任务。
最终打印结果:
Q:那么执行 resolve/reject 是同步还是异步?
A:修改状态和结果是同步的,但是通知方法执行是异步的。
所以Promise中的异步指的是 then、resolve、reject
2.3.3 then方法的链式调用
let p1 = new Promise((resolve, reject) => {
resolve('OK');
// reject('NO'); // !
});
let p2 = p1.then(result => {
console.log('p1成功 ——>', result);
// return Promise.reject('xxx'); // *
// return 10; // @
}, reason => {
console.log('p1失败 ——>', reason);
});
console.log(p2);
按照之前一样的分析,我们可以知道最先输出的是实例 p2。
最终打印结果:
那么 p2 恒等于 p1 么?
显然,二者并不相等,即 p2 是全新的 Promise 实例。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
Q:p2的状态和值是怎么改变的?
A:不论执行的是基于 p1.then 存放的 onfulfilledCallback/onrejectedCallback 两个方法中的哪一个
- 只要方法执行不报错
- 如果方法中返回一个全新的Promise实例,则“全新的Promise实例”的成功和失败决定p2的成功和失败(eg: 加上 * 处代码)
- 如果不是返回promise呢?则 [[PromiseState]]:fulfiled [[PromiseResult]]:返回值(eg: 加上 @ 处代码)
- 如果方法执行报错:p2的 [[PromiseState]]:rejected [[PromiseResult]]:报错原因
综上,也就不难发现如果只执行 ! 处代码,即只执行 reject 函数,最终输出的实例 p2 的状态一定是 fulfiled !!!
Promise.resolve() 返回成功的Promise实例;Promise.reject() 返回失败的Promise实例
牛刀小试:
let p1 = new Promise((resolve, reject) => {
resolve('OK');
});
let p2 = p1.then(result => {
console.log('p1成功 ——>', result);
return Promise.reject(10);
}, reason => {
console.log('p1失败 ——>', reason);
});
let p3 = p2.then(result => {
console.log('p2成功 ——>', result);
}, reason => {
console.log('p2失败 ——>', reason);
return Promise.resolve(10);
});
p3.then(result => {
console.log('p3成功 ——>', result);
}, reason => {
console.log('p3失败 ——>', reason);
return Promise.resolve(10);
});
接下来,看下这两种场景:
new Promise((resolve, reject) => {
resolve('OK');
}).then(null, reason => {
console.log('失败 ——>', reason);
}).then(result => {
console.log('成功 ——>', result);
}, reason => {
console.log('失败 ——>', reason);
}).then(result => {
console.log('成功 ——>', result);
}, reason => {
console.log('失败 ——>', reason);
});
打印的结果:
new Promise((resolve, reject) => {
reject('NO');
}).then(result => {
console.log('成功 ——>', result);
}, null)
.then(result => {
console.log('成功 ——>', result);
}, reason => {
console.log('失败 ——>', reason);
}).then(result => {
console.log('成功 ——>', result);
}, reason => {
console.log('失败 ——>', reason);
});
打印的结果:
如果 onfulfilledCallback/onrejectedCallback 不传递,则状态和结果都会
“顺延/穿透”到下一个同等状态应该执行的回调函数上(内部其实是自己补充了一些实现效果的默认函数)
2.4 Promise.prototype.catch()
Promise.prototype.catch()方法用于指定发生错误时的回调函数。也就是说,catch只处理状态为失败时做的事情。
Promise.prototype.catch = function (onrejectedCallback) {
return this.then(null, onrejectedCallback);
};
因此,平时写关于.then的代码时,里面只放成功状态下做的事情,失败状态下的事情写到catch里。
new Promise((resolve, reject) => {
reject('NO');
}).then(result => {
console.log('成功 ——>', result);
}).then(result => {
console.log('成功 ——>', result);
}).catch(reason => {
console.log('失败 ——>', reason);
});
2.5 Promise.prototype.finally()
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
new Promise(resolve => {
...
})
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
finally本质上是then方法的特例。
new Promise(resolve => {
...
})
.finally(() => {
...
});
// 等同于
new Promise(resolve => {
...
})
.then(
result => {
return result;
},
reason => {
throw error;
}
);
3.Promise进阶
3.1 Promise.all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
let p = Promise.all([p1, p2, p3]);
Promise.all()方法传递的参数是由多个 Promise 实例组成的集合,一般是数组。如果集合中的某一项不是 Promise 实例,则默认状态是成功他、值是本身的 Promise 实例。Promise.all()方法会返回一个新的 Promise 实例,该实例的状态取决于数组中每一个Promise实例的状态。只要有一个失败的状态,那么新的 Promise 实例的状态就是失败的,只有数组中每个Promise实例都是成功的状态时,新的 Promise 实例状态才会是成功。
function fn(interval) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(interval);
}, interval)
});
}
let p1 = fn(3000);
let p2 = fn(1000);
let p3 = Promise.resolve(0); // ~
Promise.all([p1, p2, p3])
.then(result => {
console.log(result);
})
.catch(reason => {
console.log(reason);
})
打印的结果:
不论数组中的哪个 Promise 实例先知道状态,最后结果的顺序和传递数组的顺序要保持一致。
如果将 ~ 处的 resolve 改为 reject,打印的结果是 0
在对数组中的 Promise 实例只要遇到有一个是失败状态的,那么Promise.all()方法返回的新的实例的状态就是失败的,结果就是数组中状态失败的 Promise 实例失败的原因。
使用场景:ajax并行,比如一个页面需要请求两个甚至更多的Ajax请求数据后才能正常显示。
3.2 Promise.race()
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
let p = Promise.race([p1, p2, p3]);
Promise.race()方法和Promise.all()方法不同之处在于,其返回的新的 Promise 的实例的状态取决于传递的数组参数中最先知道状态的 Promise 实例,不管这个状态是成功还是失败。
使用场景:把异步操作和定时器放到一起,如果定时器先触发,认为超时,告知用户。
3.3 Promise.any()
Promise.any()方法和前两者不同之处在于传入的 Promise 状态只要有一个为 resolve,整个则返回成功状态;全为 reject,即返回 reject 状态。
使用场景:从最快的服务器检索资源,如果存在多台服务器,从最快的一台服务器获取资源。
3.手撕Promise
手写出一套符合 PromiseA+ 规范的代码
因为Promise是ES6新增的API,所以不兼容IE浏览器(IE11都不兼容),那如果平时项目中需要兼容IE浏览器呢?
比如说做一个 to C 的项目,那肯定要考虑IE,最起码IE10及以上,处理兼容需要用到babel。正常情况下babel-preset只能把ES6的语法转换为ES5,对于ES6中的内置API是无法转换的,此时需要@babel/polyfill,它会将ES6中很多内置的API基于ES5进行了重写,让其可以兼容IE浏览器。这其中就有对Promise的重写(基于PromiseA+ 规范)。接下来基于这一规范我们来对Promise进行重写。
首先为了避免自己的写的代码和外面发生冲突,写个闭包将函数包裹起来。创建Promise类,并将API暴露出去。Promise必须要有一个函数类型的参数executor,所以需要对传入的参数进行校验,且要保证Promise这个类只能通过new来执行。
拓展: ES6新增的一个语法:new.target,用来记录new的目标值(存储new的那个构造函数),如果是当作普通函数来执行,则值为undefined
但我们手写的话需要考虑兼容性,所以一律不用ES6语法。那怎么判断是不是通过new执行的呢?new Promise的特点是Promise这个构造函数当中会默认创建一个当前类的实例对象,并且this会指向这个实例对象;如果当作普通函数执行的话,其中的this指向的是window或undefined(严格模式下)。
给实例加上state和result两个属性。立即执行executor函数,而executor函数要传入两个函数参数resolve和reject。
(function() {
'use strict'
/** 核心代码 */
function Promise(executor) {
var self = this;
// init params
if (typeof executor !== 'function') throw new TypeError('Promise resolve executor is not a function');
if (!(self instanceof Promise)) throw new TypeError('undefined is not a promise');
// property
self.state = 'pending';
self.result = undefined;
// 执行 executor
executor(
function resolve() {},
function reject() {}
);
}
/** 暴露API */
if (typeof window !== 'undefined') window.Promise = Promise;
if (typeof module === 'object' && typeof module.exports === 'object') module.exports = Promise;
})();
此时看的不是resolve/reject执行,而是优先考虑到executor函数执行是否会报错的问题,我们用try...catch来捕获异常。
无论是executor函数执行报错,还是resolve/reject函数执行,都会改变实例的状态,那我们统一写一个改变实例的状态和值的方法,叫做change。当执行change方法的时候,需要传递两个参数,一个是状态state,一个是值result
另外要考虑到实例的状态一旦从pending变成fulfilled/rejected将不再改变,所以在change方法中要做个判断。
var change = function change(state, result) {
if (self.state !== 'pending') return;
self.state = state;
self.result = result;
};
try {
executor(function resolve(value) {
change('fulfilled', value);
}, function reject(reason) {
change('rejected', reason);
});
} catch(err) {
change('rejected', err);
}
接下来,向Priomise原型对象上添砖加瓦。finally一般不用,所以我们只实现then方法和catch方法即可。👇图中圈住的浅色的方法表示都是不可枚举的。
Q:为啥必须是不可枚举的呢?
A:防止使用for...in或Object.keys的时候将公共属性也迭代到
正常情况下,我们可以通过直接在原型对象上挂方法就行了
Promise.prototype.then = function then() {};
但要做到是否可枚举,我们就要用到ES5中的Object.defineProperty这个API。Object.defineProperty可以给当前对象(即Promise的原型对象)设置属性then/catch,然后去写它的规则。Object.defineProperty一方面可以给当前对象的属性值做劫持,另一方面设置一些属性,比如说enumerable表示是否可枚举,writable表示是否可以重写值,configurable表示能否被删除。另外通过value来设置值。
可以通过Object.getOwnPropertyDescriptor来查看内置的Promise原型对象上then方法的具体规则
console.log(Object.getOwnPropertyDescriptor(Promise.prototype, 'then'));
手写的属性规则就按照👆图中来即可
无论是then还是catch,都是要往原型对象上扩展方法,所以抽出来写一个方法define,传递两个参数 key(属性名)和 value(属性值)
/** 原型对象 */
var define = function define(key, value) {
Object.defineProperty(Promise.prototype, key, {
enumerable: false,
writable: true,
configurable: true,
value: value
});
};
define(Symbol.toStringTag, 'Promise');
define('then', function then() {});
define('catch', function myCatch() {});
注:因为catch是关键字,所以换名myCatch
then方法里面接收两个参数onfulfilled和onrejected。首先会校验当前方法中this是不是Promise实例,写一个工具方法checkInstance。
其次,需要知道状态是成功还是失败,这时候需要做判断。而对于状态还不知道是成功/失败的情况,要先存储起来,存储的目的是当resolve/reject执行,即这里是执行了change方法的时候,状态和值就会进行修改,修改完了后就会通知之前存储的方法执行。那么存储到呢?因为then方法中的self和change方法中的self指向的都是当前类的实例,只要都存储到实例上,以后就可以直接在change方法中通知执行了。因此可以在实例上加上两个集合,然后在then方法中通过push将参数存储到集合中,执行change方法将状态和值立即修改之后,我们要异步通知集合中的方法执行。根据当前状态来确定执行哪个集合中的方法。
然后,因为这一块是一个异步微任务,在不考虑兼容的情况下,可以基于 queueMicrotask 来实现。
queueMicrotask(() => {
// 创建一个异步微任务
});
因为手写需要考虑兼容,所以用定时器来模拟异步微任务。
...
// property
self.state = 'pending';
self.result = undefined;
self.onfulfilledCallbacks = [];
self.onrejectedCallbacks = [];
var change = function change(state, result) {
if (self.state !== 'pending') return;
self.state = state;
self.result = result;
// 异步通知集合中的方法执行
var callbacks = state === 'fulfiled' ? self.onfulfilledCallbacks : self.onrejectedCallbacks;
if (callbacks.length > 0) {
setTimeout(function() {
callbacks.forEach(function(callback) {
callback(self.result);
});
});
}
};
...
/** 工具方法 */
// 检测是否为Promise实例
var checkInstance = function checkInstance(self) {
if(!(self instanceof Promise)) {
throw new TypeError('Method then called on incompatible receiver #<Promise>');
}
};
define('then', function then(onfulfilled, onrejected) {
checkInstance(this);
var self = this;
switch(self.state) {
case 'fulfilled':
setTimeout(function() {
onfulfilled(self.result);
});
break;
case 'rejected':
setTimeout(function() {
onrejected(self.result);
})
break;
default:
self.onfulfilledCallbacks.push(onfulfilled);
self.onrejectedCallbacks.push(onrejected);
}
});
Q: then方法里面传进来的成功/失败时的参数不是只有一个吗,为什么要用数组去存储呢? A:同一个实例可以多次调用then方法(注意和then链的区别)
至此,Promise基础版的手写完成!