手写promise-面试版

面试官:"promise有了解吗?请手动实现一下..."。此时面试官就可以去可以心安理得的去倒茶了。

首先在面试的时候需要冷静处理,即使遇到不会的,先思考,后下笔。把想到的思路用代码表示出来即可,不要求写出多么标准正确的答案,目的是让面试官看到你是有思路。

状态分解

首先对promise进行分解

  1. 状态机:3种状态
  2. 状态不可逆

前期我们只需要先大概知道这几个特性,然后想办法用代码实现。

js 复制代码
class myPromise {
  constructor(executor) {
    this.status = 'pending'
    this.res = null
    executor(this.resolve, this.reject)
  }
  resolve(val) {
    if(this.status !== 'pending') return
    this.status = 'fulfilled'
    this.res = val
  }
  reject(val) {
    if(this.status !== 'pending') return
    this.status = 'rejected'
    this.res = val
  }
}

其实写到这里很多面试官是认可的,后面写不出来的话面试官会进行提示。堂堂手写promise在前端八股地位还是很高的,可不是这么简简单单敷衍了事。

this指向问题

上述代码其实是有问题的,兄台请看:

js 复制代码
new myPromise((resolve, reject) => {
  resolve(100)
})

因为 resolve(100) 函数执行的位置在全局作用域,全局作用域中没有this,因此无法找到status。所以这个函数需要进行this绑定到promise实例。该函数是从如下图片显示的位置作为参数传入的,所以需要在这里进行this绑定。

this绑定后

then回调

执行 resolve/reject 函数需要进行then回调,也就是状态发生变化的时候执行then回调中的函数,我们基于这个思路继续完善promise。

js 复制代码
...
resolve() {...}
reject() {...}

then(resolveCb, rejectCb) {
  if(this.status === 'fulfilled') resolveCb(this.res)
  if(this.status === 'rejected') rejectCb(this.res)
}

定时器问题

这样写会有一个问题,当new myPromise时,传入一个定时器,在定时中在进行resolve/reject 时then回调会出现问题。因为定时器是一个异步任务状态会延迟更新 ,而执行then函数时myPromise的状态还是 pending,此时then的res拿到的是undefined。

解决方案 :用一个队列来维护定时器resolve/reject的then回调,当then回调检测到状态没有发生改变则将回调函数push到各种的队列中,状态发生改变后在从队列中取出回调函数执行。

一、定义回调队列

二、then回调,若为初始状态则push到回调队列

三、定时器结束后执行函数时从队列中取出回调函数执行

链式调用

then回调是支持链式调用的,也就是then.then.then,并且由api特性可知then是return一个promise的,并且这个promise的res是上一个promise的resolve/rejecte后的res,我们基于这个思路继续完善then回调。

js 复制代码
then(resolveCb, rejectCb) {
    return new Promise((resolve, reject) => {
      // 处理then中的resolve和reject回调 
      const handleThenCb = (thenCb) => {
        try {
          const res = thenCb(this.res)
          // 若返回的结果是promise的话需要等待这个promise状态的改变
          // promise状态会在哪里发生改变?在promise.then中
          // 所以把resolve和rejecte作为then回调,当状态改变后执行reslove/reject
          if(res instanceof myPromise) {
            res.then(resolve, reject)
          } else {
            // 返回结果不是promise则直接reslove
            resolve(res)
          }
        } catch (error) {
          // 发生异常则reject
          reject(error)
        }
      }

      if(this.status === 'fulfilled') handleThenCb(resolveCb)
      if(this.status === 'rejected') handleThenCb(rejectCb)
      if(this.status === 'pending') {
        this.resolveCbQueue.push(handleThenCb(resolveCb))
        this.rejectCbQueue.push(handleThenCb(rejectCb))
      }
    })
}

当你试图运行时会发生错误,仔细看这一步,我们之前的想法是让这个定时器回调push到队列中,但如果这样写的话相当于将回调执行后的返回值push到队列中,仔细想想是不是这样呢!

解决方案 :使用 bind 函数传入上下文和对应参数,结果是将bind返回的这个新函数push到队列中,这个新函数其实还是执行原来的 handleThenCb 并且this指向也正确从而完美解决。

且慢,你是不是忘了then回调是一个微任务,使用了回调队列解决定时器,这里在给then回调封装一层定时器,达到微任务的效果。

合并效果

js 复制代码
class myPromise {
  constructor(executor) {
    this.status = 'pending'
    this.res = null
    this.resolveCbQueue = []
    this.rejectCbQueue = []
    executor(this.resolve.bind(this), this.reject.bind(this))
  }

  resolve(val) {
    if(this.status !== 'pending') return
    this.status = 'fulfilled'
    this.res = val

    while(this.resolveCbQueue.length) {
      this.resolveCbQueue.shift()(this.res)
    }
  }

  reject(val) {
    if(this.status !== 'pending') return
    this.status = 'rejected'
    this.res = val

    while(this.resolveCbQueue.length) {
      this.resolveCbQueue.shift()(this.res)
    }
  }

  then(resolveCb, rejectCb) {
    return new Promise((resolve, reject) => {
      const handleThenCb = (thenCb) => {
        try {
          const res = thenCb(this.res)
          if(res instanceof myPromise) {
            res.then(resolve, reject)
          } else {
            resolve(res)
          }
        } catch (error) {
          reject(error)
        }
        
      }
      
      if(this.status === 'fulfilled') setTimeout(() => handleThenCb(resolveCb))
      if(this.status === 'rejected') setTimeout(() => handleThenCb(rejectCb))
      if(this.status === 'pending') {
        this.resolveCbQueue.push(handleThenCb.bind(this, resolveCb))
        this.rejectCbQueue.push(handleThenCb.bind(this, rejectCb))
      }
    })
    
  }
}

最后将上述代码思路进行合并看看效果。

彩蛋:当setTimeout 的延迟时间为0时,then微任务还不够完美...

相关推荐
理想不理想v19 分钟前
vue经典前端面试题
前端·javascript·vue.js
不收藏找不到我20 分钟前
浏览器交互事件汇总
前端·交互
YBN娜34 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=34 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
minDuck39 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!1 小时前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。1 小时前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼1 小时前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13582 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端