前言
不知不觉搁了 5 个月了,今年(农历)真的是很忙,忙的都没有兴致写博客了。最近心血来潮,按照自己的思路实现了一个
Promise
,写的很快,代码也相当简单,特来记录分享一下,作为 2022 年开篇博文。
初想实现
functiongetData(){returnnewPromise((resolve, reject)=>{setTimeout(()=>{resolve({code:200, data:{desc:'Hello world!'}})},2000)// setTimeout(() => {// reject({code: 400, data: {desc: 'Error!'}})// }, 4000)})}getData().then(res=>{
console.log(res);}).catch(err=>{
console.log(err);})
最初就是想实现上述的功能,**两秒后打印
{code: 200, data: {desc: 'Hello world!'}}
** ,那就同我一起一点一点去探索。
我在 2021 年开篇博文 简单实现Vue中的双向绑定、生命周期以及计算属性!!!中曾写过这样一句话:我们尝试模拟一个已存在的框架的时候,可以通过现有的结果去推断我们的代码如何设计 ,那我们想实现一下
Promise
,就需要剖析一下原生
Promise
的基本表现形式,按照上述代码:
- 实现一个
Promise
函数或类,并接收一个函数作为参数,并且这个函数会提供两个参数,这两个参数都是函数,可以执行一些操作 new Promise()
对象具有then
和catch
方法,并支持链式调用
类和回调
描述还是很简单的,根据上述描述可以实现
Promise
的基本雏形,先实现第一条,如下:
classSelfPromise{constructor(executor){executor(val=>{// resolve 函数的操作},val=>{// reject 函数的操作})}}
构造方法
constructor
接收一个
executor
函数并执行。
executor
函数接收两个参数,分别是
resolve
函数 和
reject
函数。 OK,第一条实现完毕。
基于原型的链式调用
第二条链式调用就更好实现,如下:
classSelfPromise{constructor(executor){executor(val=>{// resolve 函数的操作},val=>{// reject 函数的操作})}then(){returnthis}catch(){returnthis}}
实例对象是可以调用原型上的方法的,而在这里,原型方法
then
里面的
this
也就是它的调用者
new Promise()
,这就实现了链式调用。
回调函数和异步
好,那么如何实现 **两秒后打印
{code: 200, data: {desc: 'Hello world!'}}
**,我们再来结合原生
Promise
的表现形式仔细掰扯掰扯:
resolve
函数接收一个参数,then
函数接收一个回调函数作为参数,该回调函数又接收resolve
传入的参数作为入参
这里描述的比较绕,但确实就是这样,代码实现如下:
classSelfPromise{
#resolveCallback =nullconstructor(executor){executor(val=>{// resolve 函数的操作this.#resolveCallback &&this.#resolveCallback(val)},val=>{// reject 函数的操作})}then(callback){this.#resolveCallback = callback
returnthis}catch(){returnthis}}newSelfPromise((resolve)=>{setTimeout(()=>resolve('success'),2000)}).then(res=>{
console.log(res)})
以上的简短代码就已经实现了 **两秒后打印
{code: 200, data: {desc: 'Hello world!'}}
** 这个功能,可直接复制代码在浏览器调试。
实现原理非常简单:
无论是
then
还是
catch
函数全是同步任务,真正异步执行的是它们的回调函数。
将
then
的入参缓存在一个私有变量
#resolveCallback
中,
resolve
函数在一个定时器中被调用,甚至可以这样理解
this.#resolveCallback = callback
这一行执行完毕后,2 秒后才能执行
resolve
函数。
所以,根据上述的代码,当执行到
resolve
后,
#resolveCallback
早已准备好等待调用了。
将
resolve
入参传递给
#resolveCallback
函数并执行,就实现了 **两秒后打印
{code: 200, data: {desc: 'Hello world!'}}
** 这个功能。这里的
#resolveCallback
函数异步执行实际上是蹭了
resolve
的车。
具体实现
执行状态规范
基本雏形上面已实现,接下来稍微写个具体,这里我参考了网上的一个规范:
异步操作“未完成”(pending)
异步操作“已完成”(resolved,又称fulfilled)
异步操作“失败”(rejected)这三种的状态的变化途径只有两种:
异步操作从“未完成”到“已完成”
异步操作从“未完成”到“失败”这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。因此,Promise 对象的最终结果只有两种:
异步操作成功,Promise 对象传回一个值,状态变为 resolved
异步操作失败,Promise 对象抛出一个错误,状态变为 rejected
根据上述规范,编写出如下代码:
classSelfPromise{
#promiseState ='pending'
#resolveCallback =null
#rejectCallback =null
#finallyCallback =nullstaticisFunc(func){returntypeof func ==='function'}constructor(executor=(resolve, reject)=>{}){executor(successValue=>{// 如果状态是 rejected,则 then 不执行if(this.#promiseState ==='rejected')returnthis.#promiseState ='resolved'setTimeout(()=>{
SelfPromise.isFunc(this.#resolveCallback)&&this.#resolveCallback(successValue)
SelfPromise.isFunc(this.#finallyCallback)&&this.#finallyCallback()})},errorValue=>{// 如果状态是 resolved,则 catch 不执行if(this.#promiseState ==='resolved')returnthis.#promiseState ='rejected'setTimeout(()=>{
SelfPromise.isFunc(this.#rejectCallback)&&this.#rejectCallback(errorValue)
SelfPromise.isFunc(this.#finallyCallback)&&this.#finallyCallback()})})}then(callback){this.#resolveCallback = callback
returnthis}catch(callback){this.#rejectCallback = callback
returnthis}finally(callback){this.#finallyCallback = callback
returnthis}}
示例代码-1
OK,根据上面的规范,我们给
Promise
加入了一个执行状态,并且补充了
catch
和
finally
方法,用以下示例代码试一下:
console.log('start')const p =newSelfPromise((resolve, reject)=>{
console.log('promise')resolve('success')}).then(res=>{
console.log(res,'then');}).catch(err=>{
console.log(err,'catch');}).finally(()=>{
console.log('finally')})setTimeout(()=>{
console.log('setTimeout')})
console.log('end')
看看结果:
示例代码-2
Nice!完美符合原生
Promise
的表现形式,但是如果改成以下这样试试呢?
console.log('start')setTimeout(()=>{
console.log('setTimeout')})const p =newSelfPromise((resolve, reject)=>{
console.log('promise')resolve('success')}).then(res=>{
console.log(res,'then');}).catch(err=>{
console.log(err,'catch');}).finally(()=>{
console.log('finally')})
console.log('end')
按照 JavaScript 中的事件循环机制,
setTimeout
是宏任务,
new Promise().then()
是微任务,微任务优先于宏任务执行。这里我初步采用
setTimeout
模拟
new Promise().then()
的微任务,按照第一种示例代码,放在
new Promise()
下面,两个定时器时间都一样,很明显两个宏任务会按照代码顺序自上而下依次执行。
但是,第二个示例代码中,
setTimeout
是放在
new Promise()
上面的,如果
timeout
参数都是 0 的话,那么就不可能实现原生
Promise
的表现形式,这并没有实现真正意义上的 微任务。
实现真正的微任务
微任务探索
依据上面一节,我们清楚,只有将下述代码放在微任务中执行,才算真正的模拟了原生
Promise
的表现形式:
SelfPromise.isFunc(this.#resolveCallback)&&this.#resolveCallback(successValue)
SelfPromise.isFunc(this.#finallyCallback)&&this.#finallyCallback()
那么 JavaScript 有哪些微任务呢?
process.nextTick
NodeJS 专用,PASS
Object.observe() 老早就被废弃的一个方法,PASS
MutationObserver 监听一个指定的DOM,并在发生变化时被调用。嗯?需要创建 dom 元素??要不要添加到页面中??会不会影响性能??
es6-promise 作为 ES6 Promise 的 Polyfill 库,代码质量和权威性毋庸置疑,这里参考一下 它的源码 确认一下:
const BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;functionuseMutationObserver(){var iterations =0;var observer =newBrowserMutationObserver(flush);var node = document.createTextNode('');
observer.observe(node,{ characterData:true});returnfunction(){
node.data = iterations =++iterations %2;};}let scheduleFlush;// Decide what async method to use to triggering processing of queued callbacks:if(isNode){
scheduleFlush =useNextTick();}elseif(BrowserMutationObserver){
scheduleFlush =useMutationObserver();}elseif(isWorker){
scheduleFlush =useMessageChannel();}elseif(browserWindow ===undefined&&typeof require ==='function'){
scheduleFlush =attemptVertx();}else{
scheduleFlush =useSetTimeout();}
根据上述代码,我们可以得出,除了 NodeJS 环境,
MutationObserver
竟然是首选,另外即使
setTimeout
不能完美实现,也被当成最后的备选方案。那是因为,只要业务代码中
setTimeout
的
timeout
值大于 1 的话,也可以稍微模拟一下原生
Promise
的表现形式(谷歌浏览器中亲测通过)。
基于
MutationObserver
的微任务
结合 MDN 文档了解一下
MutationObserver
基本用法,写出下述代码:
functionuseMutationObserver(callback){let iterations =0;const observer =newMutationObserver(callback);const node = document.createTextNode('');// characterData 设为 true 以监视指定目标节点或子节点树中节点所包含的字符数据的变化。
observer.observe(node,{characterData:true});returnfunction(){
node.data = iterations =++iterations %2;};}
console.log('start')setTimeout(()=> console.log('setTimeout'))let microtask =useMutationObserver(()=>{
console.log('Hello MutationObserver!')})microtask()
console.log('end')
打印结果如下:
Amazing!由于这里借助于
MutationObserver
接口,仅仅在创建一个空白文本节点的情况下就能实现微任务调用,性能开销几乎可以忽略不计 (๑•̀ㅂ•́)و✧ (๑•̀ㅂ•́)و✧ (๑•̀ㅂ•́)و✧。
PS:
node.data = iterations = ++iterations % 2;
这行代码是一个不错的编程技巧。
最终实现代码
functionuseMutationObserver(callback){let iterations =0;const observer =newMutationObserver(callback);const node = document.createTextNode('');// characterData 设为 true 以监视指定目标节点或子节点树中节点所包含的字符数据的变化。
observer.observe(node,{characterData:true});returnfunction(){
node.data = iterations =++iterations %2;};}classSelfPromise{
#promiseState ='pending'
#resolveCallback =null
#rejectCallback =null
#finallyCallback =nullstaticisFunc(func){returntypeof func ==='function'}constructor(executor=(resolve, reject)=>{}){executor(successValue=>{// 如果状态是 rejected,则 then 不执行if(this.#promiseState ==='rejected')returnthis.#promiseState ='resolved'useMutationObserver(()=>{
SelfPromise.isFunc(this.#resolveCallback)&&this.#resolveCallback(successValue)
SelfPromise.isFunc(this.#finallyCallback)&&this.#finallyCallback()})()},errorValue=>{// 如果状态是 resolved,则 catch 不执行if(this.#promiseState ==='resolved')returnthis.#promiseState ='rejected'useMutationObserver(()=>{
SelfPromise.isFunc(this.#rejectCallback)&&this.#rejectCallback(errorValue)
SelfPromise.isFunc(this.#finallyCallback)&&this.#finallyCallback()})()})}then(callback){this.#resolveCallback = callback
returnthis}catch(callback){this.#rejectCallback = callback
returnthis}finally(callback){this.#finallyCallback = callback
returnthis}}
再来试试以下代码的打印:
console.log('start')setTimeout(()=>{
console.log('setTimeout')})const p =newSelfPromise((resolve, reject)=>{
console.log('promise')reject('error')}).then(res=>{
console.log(res,'then');}).catch(err=>{
console.error(err,'catch');}).finally(()=>{
console.log('finally')})
console.log('end')
100% 还原ES6原生
Promise
执行结果!!!
后记
开始之初是只想实现 **两秒后打印
{code: 200, data: {desc: 'Hello world!'}}
** 这样的功能,也没想到最后竟一比一模拟出了原生
Promise
的执行结果。当然,这其中有很多细节我没有深入去实现,也没有按照更完整的 Promises/A+ 规范来编写代码。
但是深入思考一些未知的事物并实现解决真的是能给我带来极大的乐趣,把自己踩的坑、思考的过程、学到的思想整理成文字分享出来,并能给他人带来帮助也能给我带来极大的满足。
知识有穷尽,编程思想、创造力无穷尽!无论是
Vue
还是
es6-promise
,都是其作者掌握着极其广度、深度的知识点再加上自己的思想所创造出来的。所以,2022 年都卷起来吧!!!
参考
es6-promise 源码
版权归原作者 帝尊菜鸟 所有, 如有侵权,请联系我们删除。