【2023】Promise最全面试手写题

188 阅读8分钟

最近在找工作,但面试机会少的可怜。闲来无事,于是将promise相关的手写题整理了一遍,可能有些题目的解法不是最优,但是从我个人来说是最容易理解的思路。

如文中有错误之处还请不厌其烦的指出哦

实现promise

  • promise接收一个函数fn,fn有resolvereject两个参数;
  • 当promise的状态为PENGDING时,执行resolve方法可以将promise的状态变为FULFILLED,并执行所有的成功回调函数;
  • 当promise的状态为PENGDING时,执行reject方法可以将promise的状态变为REJECTED,并执行所有的拒绝回调函数;
  • 在执行resolve、reject的时候,发现状态已经不为PENDING,则不做任何操作;
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
function MyPromise (fn) {
    this.status = PENDING
    this.value = undefined
    this.error = null
    // 收集的所有成功回调函数
    this.resolveCallbackList = []
    // 收集的所有拒绝回调函数
    this.rejectCallbackList = []

    const resolve = (val) => {
       // 只有状态为pending时才改变
      if (status === PENDING) {
        this.status = FULFILLED
        this.value = val
        // 执行所有的resolve回调
        this.resolveCallbackList.forEach(cb => cb(this.value))
      }
    }

    const reject = (e) => {
      if (status === PENDING) {
        this.status = REJECTED
        this.error = e
        // 执行所有的reject 回调
        this.rejectCallbackList.forEach(cb => cb(this.error))
      }
    }

    try {
      // 执行传入的函数,并将resolve和reject作为参数传递给fn
      fn(resolve, reject)
    } catch (e) {
      throw new Error(e)
    }
}

实现then方法

  • 接收两个参数,第一个参数是promise状态变为fulfilled的回调;第二个参数是promise状态变为rejected的回调;
  • 返回一个新的Promise,用于链式调用;
  • 根据调用then方法时的promise的状态,在then方法中对三种状态做如下处理:
    • 状态为fulfilled:执行onFulfilled函数并将其返回值保存为x,调用resolvePromise,若执行失败直接reject
    • 状态为rejected:执行onRejected函数并将其返回值保存为x,调用resolvePromise,若执行失败直接reject
    • 状态为pending:将上面两步的逻辑封装为函数分别添加到promise的成功和拒绝回调列表中;
MyPromise.prototype.then = function(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  onRejected = typeof onRejected === 'function' ? onRejected : e => {
    throw e
  }
  // then方法返回的新promise 用于链式调用
  let bridgePromise;
  if(this.status === FULFILLED) {
    return bridgePromise = new MyPromise((resolve, reject) => {
      // 浏览器的微任务
      queueMicrotask(() => {
        try{
          let x = onFulfilled(this.value)
          // 因为x的值可能是多种多样的 可能是promise,可能是多层嵌套promise,原始值等
          // 因此需要根据x值的不同情况,来决定then方法返回的新promise(bridgePromise)的状态为fulfilled还是rejected
          resolvePromise(bridgePromise, x, resolve, reject)
        }catch(e) {
          reject(e)
        }
      })
    })
  }

  // 注释同上面fulfilled
   if(this.status === REJECTED) {
    return onRejected = new MyPromise((resolve, reject) => {
      queueMicrotask(() => {
        try{
          let x = onRejected(this.value)
          resolvePromise(bridgePromise, x, resolve, reject)
        }catch(e) {
          reject(e)
        }
      })
    })
  }

  	// 这里跟上面不一样的地方只是把回调处理函数添加进了回调队列里面
   if(this.status === PENDGIN) {
    return bridgePromise = new MyPromise((resolve, reject) => {
          this.fulfilledCbList.push((value) => {
            try {
              let x = onFulfilled(value)
              // 解决x
              resolvePromise(bridgePromise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })
          this.rejectedCbList.push((error) => {
            try {
              let x = onRejected(error)
              resolvePromise(bridgePromise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })
      })
  }
}

resolvePromise

因为x的值可能是promise,可能是多层嵌套promise,原始值等,因此需要根据x的不同情况,来决定then方法返回的新bridgePromise的状态。resolvePromise接收以下四个参数:

  • bridgePromise:then方法返回的新promsie;
  • x:执行onFulfilled或者onRejected的返回值;
  • resolve:将bridgePromise状态变为fulfilled的方法;
  • reject:将bridgePromise状态变为rejected的方法;
    具体判断流程如下:

image.png

function resolvePromise(bridgePromise, x, resolve, reject) {
    if(x === bridgePromise) {
        throw new Errow('ciycle reference')
    }
    if(x !== null && (typeof x === 'object' || typeof x === 'function')) {
           try {
               let then = x.then
               if(typeof then === 'function') {
                   // x是一个thenable对象,将then方法的this指向x并执行then方法
                   then.call(x, 
                   y => {
                      // y 是then状态变为了fulfilled之后返回的值,此时无法确定y的值到底是什么
                     // 类型,因此需要再次判断返回值y,获取最终状态作为bridgePromise的状态
                       resolvePromise(bridgePromise, y, resolve, reject)
                   },
                   e => {
                       reject(e)
                   })
               } else {
                   resolve(x)
               }
               
           } catch(e) {
               reject(e)
           }
    } else {
        resolve(x)
    }
}
  • 循环引用出现的场景
const promise = new Promise(resolve => {
  resolve()
})

const promise1 = promise.then(() => {
  return promise1
})

实现catch方法

MyPromise.prototype.catch = function (onCatch) {
    this.then(undefined, onCatch)
}

实现Promise.resolve方法

  • 接受一个参数param,若参数类型:
    • 为promise,直接返回param;
    • 为thenable对象,并且then方法是一个函数,执行then方法;
    • 否则,直接resolve(param);
MyPromise.resolve = function(paran) {
    if(param instanceof MyPromise) return param;
    return new MyPromise((resolve, reject)) {
        if(param && param.then && typeof param.then === 'function') {
            param.then(resolve, reject)
        } else {
            resolve(param)
        }
    }
}

实现Promise.reject方法

MyPromise.reject = function(error) {
  return new MyPromise((_, reject) => {
    reject(error)
  })
}

实现finally方法

  • 无论前面的promise是onfulfilled还是onrejected都会执行传入的回调函数;
  • 执行完成后将前面promise的value或者error继续向下传递;
MyPromise.prototype.finally = function(onFinally) {
  return this.then(
    value => MyPromise.then(onFinally()).then(() => value),
    e => MyPromise.then(onFinally()).then(() => {
      throw e
    })
  )
}

实现Promise.all方法

  • 输入为一个promise数组,返回一个新的promise
  • 若数组中所有promise的状态都变为fulfilled后,新promise状态变为fulfilled,并且将所有结果按照数组顺序进行返回;
  • 若数组中有一个promise状态变为rejected,新promise的状态变为rejected,并以执行失败的promise的error进行rejected;
MyPromise.all = function (promiseList = []) {
    return new MyPromise((resolve, reject) => {
       // 所有promise状态变为fulfilled接收到的值
      const resultList = []
      const len = promiseList.length
      // 执行成功的promise的数量
      let resolveLen = 0
      for (let i = 0; i <= len - 1; i++) {
        const promiseItem = Promise.resolve(promiseList[i])
        // 避免promises[i]不是promise实例, resolve返回的是一个promise, 
        // 需要从then中拿到返回值
        promiseItem.then((data) => {
          resultList[i] = data
          resolveLen++
          // 执行成功的数量等于promise个数 返回
          if (resolveLen === len) {
            resolve(resultList)
          }
        }).catch(e => {
          return reject(e)
        })
      }
    })
  }

实现Promise.race方法

  • 输入为一个promise数组,返回一个新的promise;
  • 数组中一个promise状态改变就返回,如果状态为fulfilled,则以执行成功的promise的返回值resolve;如果状态为rejected,则以err为原因拒绝;
MyPromise.race = function (promiseList = []) {
  return new MyPromise((resolve, reject) => {
    const resultList = []
    const len = promiseList.length
    for (let i = 0; i <= len - 1; i++) {
      const promiseItem = Promise.resolve(promiseList[i])
      promiseItem.then((data) => {
        // 有一个执行成功就返回
        resolve(data)
        return
      }).catch(e => {
      // 有一个执行失败就返回
        reject(e)
        return
      })
    }
  })
}

实现romise.any方法

  • 输入为一个promise数组,返回一个新的promise
  • 当所有promise都reject时,以errorList拒绝新的promise
  • 当数组中一个promise状态变为fulfilled时,以该promise的返回值执行resolve
MyPromise.any = function (promiseList = []) {
  return new MyPromise((resolve, reject) => {
     // 存储每个promise执行失败的原因
    const reasonList = []
    // 执行失败的promise个数
    let rejectedLen = 0
    const len = promiseList.length
    for (let i = 0; i <= len - 1; i++) {
      const promiseItem = Promise.resolve(promiseList[i])
      promiseItem.then((data) => {
      // 有一个promise执行成功,resolve
        resolve(data)
        return
      }).catch(e => {
        reasonList[i] = e
        rejectedLen++
        // 如果执行失败的个数等于传入的promise个数,则代表所有的promise都执行失败
        if (rejectedLen === len) {
          reject(new AggregateError(reasonList))
        }
        return
      })
    }
  })
}

实现Promise.allSettled方法

执行allSettled方法返回的值如下:

  • 输入为一个promise数组,返回一个新的promise
  • 当某个promise状态变为rejected时,将其状态和错误原因保存;
  • 当某个promise状态变为fulfilled时,将其状态和返回值保存;
  • 所有的promise执行完成,返回直接结果;
MyPromise.allSettled = function (promiseList = []) {
  return new MyPromise((resolve, reject) => {
    // 执行promise的个数
    let execCount = 0
    // 返回值列表
    const resultList = []
    const len = promiseList.length
    
    // 所有的promise执行完毕 resolve
    const handleExecFinished = () => {
      if (execCount === len) {
        resolve(resultList)
      }
    }

    for (let i = 0; i <= len - 1; i++) {
      const promiseItem = Promise.resolve(promiseList[i])
      promiseItem.then((data) => {
        resultList[i] = { status: 'fulilled', value: data }
        execCount++
        handleExecFinished()
      }).catch(e => {
        execCount++
        resultList[i] = { status: 'rejected', reason: e }
        handleExecFinished()
      })
    }
  })
}

实现一个promise在失败指定次数内重试

需要实现一个retry函数,这个函数的输入是一个promise函数fn,重试次数times。

Promise.retry = function(fn, times) {
  return new Promise((resolve, reject) => {
    // 当前是第几次重试
    let retryCount = 0
    function next() {
      fn().then(data => {
        resolve(data)
      }).catch(e => {
         // 如果重试次数小于times,进行重试否则reject
        if(retryCount < times) {
          retryCount++
          next()
        } else {
          reject(e)
        }
      })
    }
    next()
  })
}

实现取消promise

promise一旦发起是不能够取消的,因此这里说的取消本质上是立即改变promise的状态,不再进行处理。

实现思路:

  • 利用Promise.race的特性:只要其中一个promise状态改变就返回;
  • 构造一个abortPromise将其添加到需要一起执行的promise数组中;
  • 将abortPromise的reject方法作为obj的属性保存下来,返回obj对象;
  • 需要中断的时候调用obj.abort方法将abortPromise的状态变为rejected,整个过程也就中断了;
function getPromiseWithAbort(promistList) {
  const obj = {}
  const abortPromise = new Promise((_, reject) => {
    obj.abort = reject
  })
  obj.promise = Promise.race([...promiseList, abortPromise])
  return obj
}
// 验证
var promise  = new Promise((resolve)=>{
 setTimeout(()=>{
  resolve('123')
 },3000)
})

var obj = getPromiseWithAbort(promise)

obj.promise.then(res=>{console.log(res)})

//如果要取消
obj.abort('取消执行')

JS实现一个带并发限制的异步调度器Scheduler,保证同时运行的任务最多有两个

完善代码中Scheduler类,使得以下程序能正确输出。

class Scheduler {
    constructor() {
    }

    add (task) {
    }

    run (task) {
    }
  }

  const timeout = (time) => new Promise(resolve => {
    setTimeout(resolve, time)
  })

  const scheduler = new Scheduler()
  const addTask = (time, order) => {
    scheduler
      .add(() => timeout(time))
      .then(() => console.log(order))
  }

  addTask(1000, '1')
  addTask(500, '2')
  addTask(300, '3')
  addTask(400, '4')
  // output: 2 3 1 4

实现如下:

  class Scheduler {
    constructor() {
      // 等待执行的任务队列
      this.pendingTaskList = []
      // 正在执行的任务个数
      this.isWorkingTaskCount = 0
      // 并发数量
      this.limit = 2
    }

    add (task) {
      // 需要返回一个promise 因为add在使用的时候后面接了then方法
      return new Promise(resolve => {
        // 保存下该promise的resolve方法,用于当task执行完成后打印order
        task.resolve = resolve
        if (this.isWorkingTaskCount < this.limit) {
          this.run(task)
        } else {
          this.pendingTaskList.push(task)
        }
      })
    }

    run (task) {
      this.isWorkingTaskCount++
      task().then(() => {
        this.isWorkingTaskCount--
        task.resolve()
        // 从待执行任务队列中取出任务执行
        if (this.pendingTaskList.length > 0) {
          this.run(this.pendingTaskList.shift())
        }
      })
    }
  }

实现并行3个下载图片

多个图片资源的url,已经存储在数组urls中。urls类似于['url1', 'url2',...]。 已经有一个函数function loadImg,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject。

但有一个要求,任何时刻同时下载的链接数量不可以超过3个
请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。

思路:

  • 先发起limit个请求,定义nextLoadIndex来代表下一个需要下载的图片索引;
  • 在每个请求完成后,则请求nextLoadIndex索引所代表的图片,并更新nextLoadIndex为数组中的下一张图片索引;
var urls = [
    "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
    "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
    "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
    "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png",
    "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png",
    "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
    "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png",
    "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png",
  ];

function loadImg (url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = function () {
      resolve(index);
    };
    img.onerror = function () {
      reject(new Error('Could not load image at' + url));
    };
    img.src = url;
  })
}

function limitLoad(limit) {
  return new Promise(resolve => {
    // 记录下一个需要下载的图片索引
    let nextLoadIndex = 0

    function loadNext() {
      // 如果下一张要下载的图片索引已经大于图片数组的长度了,则代表所有的图片已经下载完成了
      if(nextLoadIndex < urls.length) {
        const url = urls[nextLoadIndex]
        loadImg(url).then(data => {
        // 每张图片下载完成后 下载下一张
          nextLoadIndex++
          loadNext()
        }).catch(e => {
          nextLoadIndex++
          loadNext()
        })
      } else {
        resolve()
      }
    }
    // 先同时发起3张图片的下载请求
    for(let i = 0; i< limit; i++) {
      loadNext()
      nextLoadIndex++
    }
  })
}

使用promise实现一个红绿灯

实现红灯3秒亮一次,绿灯2秒亮一次,黄灯1秒亮一次,重复以上流程。

function red () {
  console.log('red');
}
function green () {
  console.log('green');
}
function yellow () {
  console.log('yellow');
}

function light(cb, time) {
  return new Promise(resolve => {
    setTimeout(() => {
      cb()
      resolve()
    }, time)
  })
}

function step() {
  Promise.resolve().then(() => {
    return light(red, 3000)
  }).then(() => {
    return light(green, 2000)
  }).then(() => {
    return light(yellow, 1000)
  }).then(() => {
    step()
  })
}

使用promsie实现Koa洋葱模型

let middleware = []
middleware.push((next) => {
    console.log(1)
    next()
    console.log(1.1)
})
middleware.push((next) => {
    console.log(2)
    next()
    console.log(2.1)
})
middleware.push((next) => {
    console.log(3)
    next()
    console.log(3.1)
})

let fn = compose(middleware)

function compose (promiseList) {
  // 下一个要执行的回调函数索引
  let index = 0
  function dispatch (i) {
    if (index <= i) {
      // 说明一个promise执行了多次
      throw new Error('called more than one time')
    }
    index = i
    const fn = promiseList[i]
    // 是最后一个函数 该函数执行完成后,要把这个函数的状态变为fulfilled,表明这个函数执行完成;
    if (!fn) {
      return Promise.resolve()
    }
    try {
      // 将下一个promise执行后的返回值作为前一个promise的状态 
      return Promise.resolve(fn(this, dispatch.bind(null, i + 1)))
    } catch (e) {
      return Promise.reject(e)
    }
  }
}