前言
不知不觉搁了 5 个月了,今年(农历)真的是很忙,忙的都没有兴致写博客了。最近心血来潮,按照自己的思路实现了一个 Promise ,写的很快,代码也相当简单,特来记录分享一下,作为 2022 年开篇博文。
初想实现
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({code: 200, data: {desc: 'Hello world!'}})
}, 2000)
})
}
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 的基本雏形,先实现第一条,如下:
class SelfPromise {
constructor(executor) {
executor(val => {
},val => {
})
}
}
构造方法 constructor 接收一个 executor 函数并执行。executor 函数接收两个参数,分别是 resolve 函数 和 reject 函数。 OK,第一条实现完毕。
基于原型的链式调用
第二条链式调用就更好实现,如下:
class SelfPromise {
constructor(executor) {
executor(val => {
},val => {
})
}
then() { return this }
catch() { return this }
}
实例对象是可以调用原型上的方法的,而在这里,原型方法 then 里面的 this 也就是它的调用者 new Promise() ,这就实现了链式调用。
回调函数和异步
好,那么如何实现 两秒后打印 {code: 200, data: {desc: 'Hello world!'}} ,我们再来结合原生 Promise 的表现形式仔细掰扯掰扯:
resolve 函数接收一个参数, then 函数接收一个回调函数作为参数,该回调函数又接收 resolve 传入的参数作为入参
这里描述的比较绕,但确实就是这样,代码实现如下:
class SelfPromise {
#resolveCallback = null
constructor(executor) {
executor(val => {
this.#resolveCallback && this.#resolveCallback(val)
}, val => {
})
}
then(callback) {
this.#resolveCallback = callback
return this
}
catch() {
return this
}
}
new SelfPromise((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
根据上述规范,编写出如下代码:
class SelfPromise {
#promiseState = 'pending'
#resolveCallback = null
#rejectCallback = null
#finallyCallback = null
static isFunc(func) {
return typeof func === 'function'
}
constructor(executor = (resolve, reject) => {
}) {
executor(successValue => {
if (this.#promiseState === 'rejected') return
this.#promiseState = 'resolved'
setTimeout(() => {
SelfPromise.isFunc(this.#resolveCallback) && this.#resolveCallback(successValue)
SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
})
}, errorValue => {
if (this.#promiseState === 'resolved') return
this.#promiseState = 'rejected'
setTimeout(() => {
SelfPromise.isFunc(this.#rejectCallback) && this.#rejectCallback(errorValue)
SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
})
})
}
then(callback) {
this.#resolveCallback = callback
return this
}
catch(callback) {
this.#rejectCallback = callback
return this
}
finally(callback) {
this.#finallyCallback = callback
return this
}
}
示例代码-1
OK,根据上面的规范,我们给 Promise 加入了一个执行状态,并且补充了 catch 和 finally 方法,用以下示例代码试一下:
console.log('start')
const p = new SelfPromise((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 = new SelfPromise((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;
function useMutationObserver() {
var iterations = 0;
var observer = new BrowserMutationObserver(flush);
var node = document.createTextNode('');
observer.observe(node, { characterData: true });
return function () {
node.data = iterations = ++iterations % 2;
};
}
let scheduleFlush;
if (isNode) {
scheduleFlush = useNextTick();
} else if (BrowserMutationObserver) {
scheduleFlush = useMutationObserver();
} else if (isWorker) {
scheduleFlush = useMessageChannel();
} else if (browserWindow === undefined && typeof require === 'function') {
scheduleFlush = attemptVertx();
} else {
scheduleFlush = useSetTimeout();
}
根据上述代码,我们可以得出,除了 NodeJS 环境,MutationObserver 竟然是首选,另外即使 setTimeout 不能完美实现,也被当成最后的备选方案。那是因为,只要业务代码中 setTimeout 的 timeout 值大于 1 的话,也可以稍微模拟一下原生 Promise 的表现形式(谷歌浏览器中亲测通过)。
基于 MutationObserver 的微任务
结合 MDN 文档了解一下 MutationObserver 基本用法,写出下述代码:
function useMutationObserver(callback) {
let iterations = 0;
const observer = new MutationObserver(callback);
const node = document.createTextNode('');
observer.observe(node, {characterData: true});
return function () {
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; 这行代码是一个不错的编程技巧。
最终实现代码
function useMutationObserver(callback) {
let iterations = 0;
const observer = new MutationObserver(callback);
const node = document.createTextNode('');
observer.observe(node, {characterData: true});
return function () {
node.data = iterations = ++iterations % 2;
};
}
class SelfPromise {
#promiseState = 'pending'
#resolveCallback = null
#rejectCallback = null
#finallyCallback = null
static isFunc(func) {
return typeof func === 'function'
}
constructor(executor = (resolve, reject) => {
}) {
executor(successValue => {
if (this.#promiseState === 'rejected') return
this.#promiseState = 'resolved'
useMutationObserver(() => {
SelfPromise.isFunc(this.#resolveCallback) && this.#resolveCallback(successValue)
SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
})()
}, errorValue => {
if (this.#promiseState === 'resolved') return
this.#promiseState = 'rejected'
useMutationObserver(() => {
SelfPromise.isFunc(this.#rejectCallback) && this.#rejectCallback(errorValue)
SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
})()
})
}
then(callback) {
this.#resolveCallback = callback
return this
}
catch(callback) {
this.#rejectCallback = callback
return this
}
finally(callback) {
this.#finallyCallback = callback
return this
}
}
再来试试以下代码的打印:
console.log('start')
setTimeout(() => {
console.log('setTimeout')
})
const p = new SelfPromise((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 源码
|