手写Promise,一文搞懂Promise

1,795 阅读8分钟

「时光不负,创作不停,本文正在参加2022年中总结征文大赛

前言

Promise是ES6的非常重要一个知识点,本文从 Promise的概念、Promise解决的痛点问题即为什么会出现Promise、Promise的基本使用方法、手写Promis来介绍Promise。

1、举一个生活中的栗子,引出话题

Promise,翻译过来就是承诺、保证、发誓的意思。红宝书则翻译成期约。我们一般直接就称呼它为Promise,dddd。

我中午去新丰小吃点了一碗大排面,付款之后前台会给我一张取餐小票。这张小票其实就是一个承诺,小吃店承诺给我一碗大排面。我拿着小票走到小店的最里面的窗口排队,通过小票可以换取大排面。 需要注意的是:

这个承诺是有三种不同的状态

1、等待状态    在窗口前排队等待,面条还没做好,我可以做其他的事情,比如跟同事聊天
2、成功状态    交出小票,取餐成功
3、失败状态   &nbsp窗口师傅说没有大排面了,555555

状态一旦改变就不能逆转,比如你成功拿到大排面之后就不能再去排队进入等待状态了。 这里的取餐小票就是一个Promise,它是小店承诺给你餐品的凭证,不管结果是成功失败,他都会给你一个结果。在得到结果之前,你可以做别的事。

2、Promise的概念

Promise是一个类,它可以更优秀地处理异步任务,承诺给调用者回调数据。
当我们创建一个Promise对象,需要给对象传入一个回调函数。
这个回调函数会立即执行,并传入另外两个回调函数resolve和reject
当我们调用 resolve函数时,会执行对象的then方法的内容
当我们调用 reject函数时,会执行对象的 catch方法的内容

2.1 Promises/A+规范

Promises/A+规范是 ECMAScript 6 规范实现的范本。ECMAScript 6 增加了对 Promises/A+规范的完善支持,即 Promise 类型。一经推出,Promise 就大受欢迎,成为了主导性的异步编程机制。 [Promise/A+规范](Promises/A+ (promisesaplus.com))

3、为什么会出现Promise

每个技术的出现都是为了解决先前旧技术的痛点问题,对旧技术而言有一定的革命性。那到底Promise解决了什么样的问题呢? 在以往的异步编程中,通常用回调函数来实现异步操作。
当回调函数出现返回值依赖时,往往有多个回调,一不小心就有回调地狱的nightmare。下面用定时器来模拟异步操作。

// request.js
function requestData(url, successCallBack, failCallBack) {
  setTimeout(() => {
    if(url === 'htttp://baidu.com') {
      const names = ['1', '2', '3']    // 这边把 数据传不出去
      successCallBack(names)                // 只能通过回调函数来把数据传递出去
    }
    else {
      const errorMsg = '错误的返回'
      failCallBack(errorMsg)
    }
  }, 3000);
}
// main.js
requestData('htttp://baidu.com', (res)=> {console.log(res)}, (err)=> { console.log(err)})

4、Promise的基本使用

const promise = new Promise((resolve, reject) => {
  // do other things
  if(`成功的回调`) {
    resolve()
  } else {
    reject()  // 失败的回调
  }
})
promise.then(res =>{
  console.log(res, '成功的结果') 
}).catch(err => {
  console.log(err, '失败的结果')
})

3.1 三种状态

pending(待定) 最初始的状态,该状态可以变成下面任意一个状态
fullfiled/resolved(兑现/解决) 即回调成功的状态
rejected (拒绝) 即回调失败的状态

需要注意的是,状态的改变是不可逆的。从初始的pending(待定状态)可以转变为 fullfilled/resolved(解决状态);或者从初始的pending(待定状态)      可以转变成 rejected(拒绝状态)。处于pending状态可以做其他的事情。

可以用常量表示这三种状态

const PROMISE_STATUS_PENDING = "pengding"
const PROMISE_STATUS_FULLFILLED = "fullfilled"
const PROMISE_STATUS_REJECTED = "rejected"

当创建Promise对象时,我们需要传入一个回调函数,我们称之为executor;
这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、reject;所以大致框架是:

class LAPromise {
  constructor(executor) {
   // ....
    executor(resolve, reject)
  }
}

完整的
resolve方法 改变状态为 fullfilled
rejected方法 改变状态为 rejected

const PROMISE_STATUS_PENDING = "pengding"
const PROMISE_STATUS_FULLFILLED = "fullfilled"
const PROMISE_STATUS_REJECTED = "rejected"
class LAPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING
    this.value = undefined
    this.reason = undefined
    const resolve = (value) => {
      if(this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_FULLFILLED
        console.log('执行resolve方法')
      }
    }
    const reject = (reason) => {
      if(this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_REJECTED
        this.reason = reason
        console.log('执行了reject方法')
      }
    }
    executor(resolve, reject)
  }
}

3.2 对象方法- then()方法

then方法是定义在 Promise.prototype.then里面
then方法接受两个参数:
fullfilled的回调函数:当状态变成fulfilled时会回调的函数 rejeted的回调函数:当状态变成 rejected时会回调的函数

promise.then(onFulfilled, onRejected)

class LAPromise {
  then(onFulfilled, onRejected) {
    if(this.status === PROMISE_STATUS_FULLFILLED) {
      onFulfilled(this.value)
     } else if(this.status === PROMISE_STATUS_REJECTED ) {
      onRejected(this.reason)
    }
  }
}

运行试试看

const p = new LAPromise((resolve, reject) => {
  resolve('abc')
  rejected('xyz')
})
p.then(res => {
  console.log('res', '请求成功')
}, err => {
  console.log('err', '请求失败')
})

打印: 执行resolve方法
            res 请求成功

继续优化 then方法

3.2.1、当参数onFulfilled或者onRejected 不是函数时,则忽略这个参数

class LAPromise {
then(onFulfilled, onRejected) {
 onFulfilled = Object.prototype.toString.call(onFulfilled) === '[object Function]' ? onFulfilled : value => value
 onRejected = Object.prototype.toString.call(onFulfilled) === '[object Function]' ? onRejected : error => {throw error}
}
}

3.2.2、then 方法可以多次调用

每次调用我们都可以传入对应的fulfilled回调;
当Promise的状态变成fulfilled的时候,这些回调函数都会被执行

promise.then(res => {
  console.log('res1', res)
})
promise.then(res => {
  console.log('res2', res)
})
promise.then(res => {
  console.log('res3', res)
})

用数组将onFulfilled, onRejected 收集起来,再依次执行,达到可以多次调用的目的。

class LAPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING
    this.value = undefined
    this.reason = undefined
    this.onFulfilledFns = []
    this.onRejectedFns = []
    const resolve = (value) => {
      if(this.status === PROMISE_STATUS_PENDING ) {
        this.value = value
        this.status = PROMISE_STATUS_FULLFILLED
        this.onFulfilledFns.forEach(fn => fn(this.value))
        console.log('resolve方法执行')
      }
    }
    const reject = (reason) => {
      if(this.status  === PROMISE_STATUS_PENDING) {
        this.reason = reason
        this.status = PROMISE_STATUS_REJECTED
        this.onRejectedFns.forEach(fn => fn(this.reason))
        console.log('rejected方法执行')
      }
    }
    executor(resolve, reject)
  }
  then(onFulfilled, onRejected) {
    onFulfilled = Object.prototype.toString.call(onFulfilled) === '[object Function]' ? onFulfilled : value => value
    onRejected = Object.prototype.toString.call(onFulfilled) === '[object Function]' ? onRejected : error => {throw error}
    if(this.status === PROMISE_STATUS_PENDING) {
      this.onFulfilledFns.push(onFulfilled)
      this.onRejectedFns.push(onRejected)
     } else if(this.status === PROMISE_STATUS_FULLFILLED ) {
      onFulfilled(this.value)
    } else {
      onRejected(this.reason)
    }
  }
}

运行试试看

const p = new LAPromise((resolve, reject)=> {
  resolve('123')
  reject('456')
})

p.then(res => {
  console.log('res1', res)
})
p.then(res => {
  console.log('res2', res)
})

打印结果
     res1 123
     res2 123

3.3.3 then的链式调用

then可以进行链式调用是因为在then方法返回了新的 Promise作为回调返回值

// 链式调用的使用
const p = new Promise((resolve, reject) => {
  resolve('111')
})
p.then((res) => {
  console.log('res1', res)
  return 'a'     // 可以返回一个返回值
}).then((res) => {
  console.log('res2', res)  // 接受上面返回的值
})

实现: 改写 then方法

then(onFulfilled, onRejected){
    return new HYPromise((resolve, reject) => {
      if(this.status === PROMISE_STATUS_PENDING) {            
        this.onFulfilledFns.push(() => {
          try {
            const value = onFulfilled(this.value)
            resolve(value)  
          } catch(err) {
            reject(err)
          }
        })
        this.onRejectedFns.push(() => {
          try {
            const value = onRejected(this.reason)
            resolve(value)
          } catch(err) {
            reject(err)
          }
        })
      } else if(this.status === PROMISE_STATUS_FULLFILLED) {
          try {
            const value = onFulfilled(this.value)
            resolve(value)  
          } catch(err) {
            reject(err)
          }
      } else {
      try{
        const value = onRejected(this.reason)
        resolve(value)
      } catch(err){
          reject(err)
      }
    }    
    })
  }

优化点: 太多的try...catch...,而且格式类似,可以抽成函数

运行试试看

const promise = new LAPromise((resolve, reject) => {
  resolve('aaa')
})
promise.then(res => {
  console.log('res1', res)
  return '1'
}).then(res => {
  console.log('res2', res)
})

    res1 aaa
     res2 1

3.3.4 事件循环

JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外, 还依靠任务队列(task queue)来决定另外一些代码的执行。整个执行过程,我们称为事件循环过程。
事件循环中,不仅只有一个队列,有两个队列,一个微任务一个宏任务

  • 宏任务队列:
  • 微任务队列:
宏任务队列微任务队列
定时器queuequeueMicrotask
ajaxPromise.then
dom(比如点击事件)mutation observer api
  • 事件循环中,两队列的优先级
  • main script 先执行
  • 在执行任何一个宏任务之前,都会查看微任务队列中是否有任务需要执行
  • 也就是宏任务执行之前,必须保证微任务队列是空的
  • 如果不为空就执行微任务队列的任务

下面举个例子说明一下

new Promise(function(resolve) {
 console.log("promise1")
 resolve()
}).then(function() {
   console.log('then1')
})

setTimeout(function(){
 console.log("setTimeout1")
});

console.log(2)

queueMicrotask( ()=> {
 console.log("queueMicrotask1")
})

答案: promise1    2   then1   queueMicrotask1   setTimeout1

main script微任务宏任务
promise1then1setTimeout1
2queueMicrotask1

用queueMicrotask实现微任务

class HYPromise {
  constructor(exector) {
    this.status = PROMISE_STATUS_PENDING 
    this.value = undefined
    this.reason = undefined
    this.onFulfilledFns = []
    this.onRejectedFns = []
    const resolve = (value) => {
      queueMicrotask(() => {
        if(this.status === PROMISE_STATUS_PENDING && this.onFulfilledFns){
          this.status = PROMISE_STATUS_FULLFILLED
          this.value = value
          this.onFulfilledFns.forEach(fn => {
            fn(this.value)
          })
        }
       
      })
    }
    const reject = (reason) => {
        queueMicrotask(() => {
          if(this.status === PROMISE_STATUS_PENDING && this.onRejectedFns) {
            this.status = PROMISE_STATUS_REJECTED
            this.reason = reason
            this.onRejectedFns.forEach(fn =>{
            fn(this.reason)
          })
          }
          
        })
    }
    try{
        exector(resolve, reject)
    } catch(err){
        reject(err)
    }
    // exector(resolve, reject)
  }
  

  then(onFulfilled, onRejected){
    return new HYPromise((resolve, reject) => {
      if(this.status === PROMISE_STATUS_PENDING) {            
        this.onFulfilledFns.push(() => {
          try {
            const value = onFulfilled(this.value)
            resolve(value)  
          } catch(err) {
            reject(err)
          }
        })
        this.onRejectedFns.push(() => {
          try {
            const value = onRejected(this.reason)
            resolve(value)
          } catch(err) {
            reject(err)
          }
        })
      } else if(this.status === PROMISE_STATUS_FULLFILLED) {
          try {
            const value = onFulfilled(this.value)
            resolve(value)  
          } catch(err) {
            reject(err)
          }
      } else {
      try{
        const value = onRejected(this.reason)
        resolve(value)
      } catch(err){
          reject(err)
      }
    }    
    })
  }
  
}

总结

以上就是我们基于Promises/A+规范实现的Promise,我们还可以进一步优化,对照着规范练习。
Promise是对以往异步编程造成回调地狱的提供的解决办法,他可以为异步任务添加回调来得到异步任务的结果。异步任务的结果出现在Promise.then里面。结构清晰容易理解。

今日父亲节

我一直很庆幸自己有一个很理想的父亲。张爱玲说过,“中年的男人,时常会觉得孤独,因为他一睁开眼睛,周围都是要依靠他的人,却没有他可以依靠的人。” 在父亲的节日里,跟父亲说一点贴心sweet的话吧。
我的父亲在年轻的时候还是很帅的,眼睛深邃有神大双眼皮,还有酒窝。
我的父亲没什么学历,小时候只给我讲过一个故事,鲁班发明锯子的故事。但是他勤劳能干肯吃苦,辛苦赚钱养家。
我的父亲虽然喜欢叨叨叨的,但是他爱妻子,爱孩子,孝顺爷爷和姥姥姥爷。首先他很尊重我妈妈的想法,重视妈妈的声音,其实我妈妈就是家里的领导,我们时常感觉到领导的pua。我爸爸特别喜欢做家务,喜欢刷碗刷锅喜欢做菜扫地拖地,这其实让我这个懒人很匪夷所思。
感谢爸爸一直以来的付出,给我们一个完整有爱的家。