IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 手写 Promise ?∑(っ °Д °;)っ 这么简单 -> 正文阅读

[JavaScript知识库]手写 Promise ?∑(っ °Д °;)っ 这么简单

前言

不知不觉搁了 5 个月了,今年(农历)真的是很忙,忙的都没有兴致写博客了。最近心血来潮,按照自己的思路实现了一个 Promise,写的很快,代码也相当简单,特来记录分享一下,作为 2022 年开篇博文。

初想实现

function getData() {
    return new Promise((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 的基本表现形式,按照上述代码:

  1. 实现一个 Promise 函数或类,并接收一个函数作为参数,并且这个函数会提供两个参数,这两个参数都是函数,可以执行一些操作
  2. new Promise() 对象具有 thencatch 方法,并支持链式调用

类和回调

描述还是很简单的,根据上述描述可以实现 Promise 的基本雏形,先实现第一条,如下:

class SelfPromise {
    constructor(executor) {
        executor(val => {
            // resolve 函数的操作
        },val => {
            // reject 函数的操作
        })
    }
}

构造方法 constructor 接收一个 executor 函数并执行。executor 函数接收两个参数,分别是 resolve 函数 和 reject 函数。 OK,第一条实现完毕。

基于原型的链式调用

第二条链式调用就更好实现,如下:

class SelfPromise {
    constructor(executor) {
        executor(val => {
            // resolve 函数的操作
        },val => {
            // reject 函数的操作
        })
    }
    
    then() { return this }
    
    catch() { return this }
}

实例对象是可以调用原型上的方法的,而在这里,原型方法 then 里面的 this 也就是它的调用者 new Promise() ,这就实现了链式调用。

回调函数和异步

好,那么如何实现 两秒后打印 {code: 200, data: {desc: 'Hello world!'}},我们再来结合原生 Promise 的表现形式仔细掰扯掰扯:

  1. resolve 函数接收一个参数, then 函数接收一个回调函数作为参数,该回调函数又接收 resolve 传入的参数作为入参

这里描述的比较绕,但确实就是这样,代码实现如下:

class SelfPromise {
    #resolveCallback = null

    constructor(executor) {
        executor(val => {
            // resolve 函数的操作
            this.#resolveCallback && this.#resolveCallback(val)
        }, val => {
            // reject 函数的操作
        })
    }

    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 => {
            // 如果状态是 rejected,则 then 不执行
            if (this.#promiseState === 'rejected') return
            this.#promiseState = 'resolved'
            setTimeout(() => {
                SelfPromise.isFunc(this.#resolveCallback) && this.#resolveCallback(successValue)
                SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
            })
        }, errorValue => {
            // 如果状态是 resolved,则 catch 不执行
            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 加入了一个执行状态,并且补充了 catchfinally 方法,用以下示例代码试一下:

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;
// Decide what async method to use to triggering processing of queued callbacks:
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 不能完美实现,也被当成最后的备选方案。那是因为,只要业务代码中 setTimeouttimeout 值大于 1 的话,也可以稍微模拟一下原生 Promise 的表现形式(谷歌浏览器中亲测通过)。

基于 MutationObserver 的微任务

结合 MDN 文档了解一下 MutationObserver 基本用法,写出下述代码:

function useMutationObserver(callback) {
    let iterations = 0;
    const observer = new MutationObserver(callback);
    const node = document.createTextNode('');
    // characterData 设为 true 以监视指定目标节点或子节点树中节点所包含的字符数据的变化。
    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('');
    // characterData 设为 true 以监视指定目标节点或子节点树中节点所包含的字符数据的变化。
    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 => {
            // 如果状态是 rejected,则 then 不执行
            if (this.#promiseState === 'rejected') return
            this.#promiseState = 'resolved'
            useMutationObserver(() => {
                SelfPromise.isFunc(this.#resolveCallback) && this.#resolveCallback(successValue)
                SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
            })()
        }, errorValue => {
            // 如果状态是 resolved,则 catch 不执行
            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 源码

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-01-29 22:59:45  更:2022-01-29 22:59:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 13:29:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码