资讯专栏INFORMATION COLUMN

山寨一个 Promise

XFLY / 2697人阅读

摘要:其次要记录状态,判断消息是否已被发布,如果未发布消息,则通过来注册回调时,是将回调函数添加到内部的回调队列中如果消息已发布,则通过来注册回调时,直接将消息传至回调函数,并执行规范中采用的状态机制是可以转化为或,并且只能转化一次。

一点感悟

Promise 是编写异步的另一种方式,鄙人愚见,它就是 Callback 的一种封装

相比 Callback ,它有以下特点

Promise 将异步结果保存起来,可以随时获取

链式调用 then 方法会返回一个新的 Promise ,从而避免了回调地狱

决定一次异步有两个环节

发起异步事件

处理异步结果

Promise 可以给一个异步事件注册多个处理函数,举个栗子,就像这样

let p1 = new Promise((resolve) => {
  fs.readFile("./test.js", "utf8", (err, data) => {
    resolve(data)
  })
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))

用 Callback 实现一样的效果

用 callbacks 将所有注册的函数保存

待异步事件返回结果,再遍历 callbacks ,依次执行所有注册的函数

就像这样

let callbacks = []
function resolve(data){
  callbacks.forEach(cb => cb(data))
}

fs.readFile("./test.js", "utf8", (err, data) => {
  resolve(data)
})

callbacks.push(data => console.log(data))
callbacks.push(data => console.log(data.toUpperCase()))

将上述代码封装一下

const fs = require("fs")

class FakePromise {
  constructor(fn){
      this.callbacks = []
      resolve = resolve.bind(this)
    function resolve(data){
      this.callbacks.forEach(cb => cb(data))
    }
    fn(resolve)
  }
  
  then(onFulfilled){
    this.callbacks.push(onFulfilled)
  }
}

let p1 = new FakePromise(resolve => {
  fs.readFile("./test.js", "utf8", (err, data) => {
    resolve(data)
  })
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))

哈?是不是和真的 Promise 有点像

从发布-订阅模式的角度来看:

FakePromise 中通过 .then(onFulfilled) 来订阅消息,注册处理异步结果的函数

通过 resolve(data) 来发布消息,触发处理异步结果的函数去执行,发布的时机是异步事件完成时

延时 resolve

先前的代码存在一个问题,如果在执行 p1.then(data => console.log(data)) 之前,resolve(data) 就已经执行了,那么再通过 .then(onFulfilled) 注册的处理异步结果的函数将永远不会执行

为了避免这种情况,改造 resolve 函数,在其内部添加 setTimeout,从而保证那些注册的处理函数是在下一个事件队列中执行,就像这样

function resolve(value) {
    setTimeout(() => {
        this.callbacks.forEach(cb => cb(value))
    }, 0)
}

通过延时执行 resolve 内部的函数,保证了先订阅消息,再发布消息

但是 Promise 还有个额外的功能是在发布消息后,仍然可以订阅消息,并且立即执行,就像这样

const fs = require("fs")

let p1 = new Promise(resolve => {
    fs.readFile("./test.js", "utf8", (err, data) => resolve(data))
})

p1.then(data => console.log(data))
setTimeout(function(){
    p1.then(data => console.log(data.toUpperCase()))
}, 5000)

5s之内,文件早已读取成功,但是在5s之后,依然可以通过 .then 注册处理事件,并且该事件会立即执行

先发布,再订阅

实现先发布,再订阅的基础是将消息保存下来。其次要记录状态,判断消息是否已被发布,如果未发布消息,则通过 .then 来注册回调时,是将回调函数添加到内部的回调队列中;如果消息已发布,则通过 .then 来注册回调时,直接将消息传至回调函数,并执行

Promise 规范中采用的状态机制是 pendingfulfilledrejected

pending 可以转化为 fulfilledrejected ,并且只能转化一次。

转化为 fulfilledrejected 后,状态就不可再变

修改代码如下

class FakePromise {
    constructor(fn) {
        this.value = null
        this.state = "pending"
        this.callbacks = []
        resolve = resolve.bind(this)

        function resolve(value) {
            setTimeout(() => {
                this.value = value
                this.state = "fulfilled"
                this.callbacks.forEach(cb => cb(value))
            }, 0)
        }
        fn(resolve)
    }

    then(onFulfilled) {
        if (this.state === "pending") {
            this.callbacks.push(onFulfilled)
        } else {
            onFulfilled(this.value)
        }
    }
}

既然实现了先发布,再订阅,那么 resolve 中的 setTimeout 是不是可以去掉了?

并不可以,因为人家正经的 Promise 是这样的

let p1 = new Promise(resolve => {
    resolve("haha")
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))
console.log("xixi")
// xixi
// haha
// HAHA

只有保留 resolve 中 setTimeout 才能使 FakePromise 实现相同的效果

let p1 = new FakePromise(resolve => {
    resolve("haha")
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))
console.log("xixi")
// xixi
// haha
// HAHA

没有 setTimeout 的输出结果

// haha
// HAHA
// xixi
链式 Promise

正经的 Promise 可以链式调用,从而避免了回调地狱

let p1 = new Promise(resolve => {
    fs.readFile("./test.js", "utf8", (err, data) => {
        resolve(data)
    })
}).then(res => {
    return new Promise(resolve => {
        fs.readFile("./main.js", "utf8", (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})

正经的 Promise 调用 then 方法会返回一个新的 Promise 对象

我们伪造的 FakePromise 并没有实现这一功能,原来的 then 方法

...
    then(onFulfilled){
        if (this.state === "pending") {
            this.callbacks.push(onFulfilled)
        } else {
            onFulfilled(this.value)
        }
    }
...

原来的 then 方法就是根据 state 判断是注册 onFulfilled 函数,还是执行 onFulfilled 函数

为了实现 FakePromise 的高仿,我们要改造 then 方法,使其返回一个新的 FakePromise ,为了方便区分,将返回的 FakePromise 取名为 SonFakePromise ,而先前调用 then 的对象为 FatherFakePromise

那么问题来了

那么构造这个 SonFakePromise 的函数参数是什么

这个 SonFakePromise 什么时候 resolve ?

首先,当构造一个新的 SonFakePromise 时,会将传入的函数参数 fn 执行一遍,且这个函数有 resolve 参数

...
    then(onFulfilled){
      if(this.state === "pending"){
        this.callbacks.push(onFulfilled)
        let SonFakePromise = new FakePromise(function fn(resolve){
          
        })
        return SonFakePromise
      }else{
        onFulfilled(this.value)
        let SonFakePromise = new FakePromise(function fn(resolve){
          
        })
        return SonFakePromise
      }
    }
...

现在的问题是这个 SonFakePromise 什么时候 resolve ?即构造函数中的函数参数 fn 如何定义

结合正经 Promise 的例子来看

let faherPromise = new Promise(resolve => {
    fs.readFile("./test.js", "utf8", (err, data) => {
        resolve(data)
    })
}).then(res => {
    return new Promise(resolve => {
        fs.readFile("./main.js", "utf8", (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})
// 等同于
let faherPromise = new Promise(resolve => {
    fs.readFile("./test.js", "utf8", (err, data) => {
        resolve(data)
    })
})
let sonPromise = faherPromise.then(function onFulfilled(res){
    return new Promise(function fn(resolve){
        fs.readFile("./main.js", "utf8", (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})

在例子中,onFulfilled 函数如下,且其执行后返回一个新的 Promise,暂时取名为 fulPromise

function onFulfilled(res) {
  return new Promise(function fn(resolve){
    fs.readFile("./main.js", "utf8", (err, data) => {
      resolve(data)
    })
  })
}

现在来分析一下,fatherPromisesonPromisefulPromise 这三者的关系

sonPromise 是调用 fatherPromise 的 then 方法返回的

而调用这个 then 方法需要传入一个函数参数,取名为 retFulPromise

retFulPromise 函数执行的返回值 fulPromise

希望下面的代码能有助于理解

let fatherPromise = new Promise(function fatherFn(fatherResolve){
  fs.readFile("./test.js", "utf8", (err, data) => {
    fatherResolve(data)
  })
})

let sonPromise = fatherPromise.then(retFulPromise)

function retFulPromise(res) {
  let fulPromise = new Promise(function fulFn(fulResolve){
    fs.readFile("./main.js", "utf8", (err, data) => {
      fulResolve(data)
    })
  })
  return fulPromise
}

fatherPromise 的状态为 fulfilled 时,会执行 retFulPromise,其返回 fulPromise ,当这个 fulPromise 执行 fulResolve 时,即完成读取 main.js 时, sonPromise 也会执行内部的 resolve

所以可以看成,sonPromise 的 sonResolve 函数,也被注册到了 fulPromise 上

So,了解了整个流程,该怎么修改自己的 FakePromise 呢?

秀操作,考验技巧的时候到了,将 sonResolve 的引用保存起来,注册到 fulFakePromise 上

const fs = require("fs")

class FakePromise {
    constructor(fn) {
        this.value = null
        this.state = "pending"
        this.callbacks = []
        resolve = resolve.bind(this)

        function resolve(value) {
            setTimeout(() => {
                this.value = value
                this.state = "fulfilled"
                this.callbacks.forEach(cb => {
                    let returnValue = cb.onFulfilled(value)
                    if (returnValue instanceof FakePromise) {
                        returnValue.then(cb.sonResolveRes)
                    }
                })
            })
        }
        fn(resolve)
    }

    then(onFulfilled) {
        if (this.state === "pending") {
            let sonResolveRes = null
            let sonFakePromise = new FakePromise(function sonFn(sonResolve) {
                sonResolveRes = sonResolve
            })
            this.callbacks.push({
                sonFakePromise,
                sonResolveRes,
                onFulfilled
            })
            return sonFakePromise
        } else {
            let value = onFulfilled(this.value)
            let sonResolveRes = null
            let sonFakePromise = new FakePromise(function sonFn(sonResolve) {
                sonResolveRes = sonResolve
            })
            if (value instanceof FakePromise) {
                value.then(sonResolveRes)
            }
            return sonFakePromise
        }
    }
}
多角度测试
let fatherFakePromise = new FakePromise(resolve => {
    fs.readFile("./test.js", "utf8", (err, data) => {
        resolve(data)
    })
})
let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
    return new FakePromise(function fn(resolve) {
        fs.readFile("./main.js", "utf8", (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})
let fatherFakePromise = new FakePromise(resolve => {
    fs.readFile("./test.js", "utf8", (err, data) => {
        resolve(data)
    })
})
setTimeout(function () {
    let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
        return new FakePromise(function fn(resolve) {
            fs.readFile("./main.js", "utf8", (err, data) => {
                resolve(data)
            })
        })
    }).then(res => {
        console.log(res)
    })
}, 1000)
let fatherFakePromise = new FakePromise(resolve => {
    resolve("haha")
})

let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
    return new FakePromise(function fn(resolve) {
        fs.readFile("./main.js", "utf8", (err, data) => {
            resolve(data)
        })
    })
}).then(res => {
    console.log(res)
})
let fatherFakePromise = new FakePromise(resolve => {
    resolve("haha")
})

setTimeout(function () {
    let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
        return new FakePromise(function fn(resolve) {
            fs.readFile("./main.js", "utf8", (err, data) => {
                resolve(data)
            })
        })
    }).then(res => {
        console.log(res)
    })
}, 1000)
参考资料

30分钟,让你彻底明白Promise原理

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107249.html

相关文章

  • 历史首次!联发科打败高通成为全球第一,凭什么?

    摘要:据调研机构数据,年第三季度,全球智能手机芯片市场占有率中,联发科力压高通,历史首次登顶全球第一。年月,联发科发布全球首款十核处理器,以及它的升级版。联发科本月表示,其最新的旗舰芯片将于明年第一季度发布,希望在农历新年前推出。在被喊了一年的MTK YES后,联发科终于迎来了自己的YES时刻。据调研机构Counterpoint数据,2020年第三季度,全球智能手机芯片市场占有率中,联发科力压高通...

    Tecode 评论0 收藏0
  • 原生JS模拟Bootstrap中的折叠(Collapse)插件

    摘要:以前实习的时候因为赶时间直接用的插件做了个折叠菜单,对于一个原生控来说还是更倾向于自己写一个,毕竟为了个折叠菜单引入和有点太臃肿了。原版的效果其实也不难,主要是在开合的过程中添加了的过渡效果。 以前实习的时候因为赶时间直接用bootstrap的插件collapse.js做了个折叠菜单, 对于一个原生控来说还是更倾向于自己写一个, 毕竟为了个折叠菜单引入jq和bootstrap有点太臃肿...

    IntMain 评论0 收藏0
  • 乐字节Java反射之一:反射概念与获取反射源头class

    摘要:一反射机制概念程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言,如,是动态语言显然,,不是动态语言,但是有着一个非常突出的动态相关机制。相关的为二获取源头重点打开权限所有类的对象其实都是的实例。 一、Java反射机制概念 程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言,如Python, Ruby是动态语言;显然C++,Java,C#不是动态语言,但是JAVA有...

    caikeal 评论0 收藏0
  • [phaser3入门探坑]使用phaser3制作山寨马里奥

    摘要:前言是一个优秀的前端库,封装了很多底层的实现,可以用来制作游戏,场景等。今年月新发布了,到今天为止已经更新到了。声明本游戏来自于小站的官方教程,加入了一些个人的注释,本文旨在帮助各位观众老爷快速上手。 前言 phaser是一个优秀的前端canvas库,封装了很多底层的实现,可以用来制作游戏,h5场景等。今年1月新发布了phaser3,到今天为止已经更新到了3.30。 声明 本游戏来自于...

    szysky 评论0 收藏0

发表评论

0条评论

XFLY

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<