深入理解Promise-中(透析篇)

162 阅读10分钟

努力让学习成为一种习惯,自信来源于充分的准备

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

前言

Javascript是一门单线程语言,异步操作是必不可少的。由于 Javascipt自身设计的缺陷。早期都是用回调函数的方式实现异步。但这种方式存在回调地狱回调操作不统一等痛点,代码难以维护的同时也大大增加了心智负担。详情可以参考深入理解Promise-上(起源篇)

本篇文章将深入Promise本身,探究其原理并从零实现一个基于Promise A+规范,满足其核心功能的Promise

什么是Promise

回答这个问题需要从两个角度:1. Promise A+规范 2. 现代浏览器中的Promise即ES6的Promise

Promise A+规范

引入红宝书对这段历史的描述

为了解决异步场景的各种痛点,社区引入了期约机制。早期的期约机制在jQuery和Dojo中是以Deferred API的形式出现 的。到了2010年,CommonJS项目实现的Promises/A规范日益流行起来。Q和Bluebird等第三方JavaScript期约库也越来越得到社区认可。这些库解决的问题是同一个,但实现方式多少有些差异(类似于浏览器间的DOM规范)

虽然这些库的实现多少都有些不同。为弥合现有实现之间的差异,2012 年Promises/A+组织分叉(fork)了CommonJS的Promises/A建议,并以相同的名字制定了Promises/A+规范。这个规范最终成为了ECMAScript 6规范实现的范本

简单来说,Promises/A+组织依据社区内相对著名的第三方期约库,综合制定了更加完善且统一的期约规范 - Promise A+规范

我们来看看规范文档是怎么定义Promise的,为了方便,接下来统一把Promise A+规范简称为A+规范

promise是具有符合本规范的then方法的对象或函数

那么是不是说明以下这些都是Promise

const p = {
  then() {}
}

const arr = []
arr.then = function () {}

function f () {}
f.then = function () {}

严格来说这只是“挂羊头卖狗肉”,因为Promise最关键的点在于then方法的行为。整个A+规范也都是在说明 then方法应该有哪些行为。换句话说,一个对象或者函数,它有一个then方法并且行为交互满足A+规范,那么它就是一个Promise

ES6 Promise

ES6将A+规范写入了语言标准。原生提供了Promise构造函数,通过它我们可以创建一个满足A+规范Promise对象。并且在此基础上,进一步扩展了能力(如Promise.all、Promise.race等)

有关ES6 Promise的API使用可以参考阮一峰的promise文档

从零实现A+规范的Promise(核心部分)

相信到这里,大家对Promise的来龙去脉有了一个感性的认识。接下来我们深入A+规范,从零自己实现一个 Promise,在这个过程,相信对 Promise的理解会进一步提升

相关文档

Promise A+规范

这里我会采用es6的写法实现,原理都是一样,es6语法层面更简洁美观点

首先我们来实现Promise基本的特性

  1. Promise构造函数内部必须传入一个执行器函数(executor),且该执行器会立刻执行
  2. 每一个Promise对象有三个内部状态:pending/fulfilled/rejected
  3. 执行器函数会提供两个内置函数resolve/reject,用于改变Promise对象的状态。执行器调用resolvepromise兑现(fulfilled状态),调用rejectpromise拒绝(rejected状态);如果执行器抛异常同样拒绝Promise对象状态一旦确定了,便不可再次更改
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  #state = PENDING
  #result = undefined
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('is not a function')
    }
    const resolve = (result) => {
      if (this.#state !== PENDING) return
      this.#state = FULFILLED
      this.#result = result
    }
    const reject = () => {
      if (this.#state !== PENDING) return
      this.#state = REJECTED
      this.#result = result
    }
    try {
       executor(resolve, reject)
    } catch(error) {
       reject(error)
    }
    
  }
}

这里额外提一个细节点,为什么不把resolve、reject函数作为实例方法,这样不就避免每次初始化构造函数都要重复定义,原因是这种写法的this指向的全局对象,如果我们要指定this为Promise实例需要bind:executor(this.resolve.bind(this), this.reject.bind(this))也是返回一个新的函数,本质一样的

接下来便是A+规范中的核心:then的实现,我们一步一步来

image.png

对于上面这段描述其核心点是:onFulfilledonRejected这两个函数什么时候调用,显然这两个函数在调用thenpromise对象兑现/拒绝的时候调用,基于这个思路

我们完成第一版

class MyPromise {
  
  // ....
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      // 需要注意,这里的this指向需要是最开始的promise实例,这里的`this.#state`也是它的状态
      // 换言之这里需要使用箭头函数
      if (this.#state === FULFILLED) {
        onFulfilled?.(this.#result)
      } else if (this.#state === REJECTED) {
        onRejected?.(this.#result)
      }
    })
  }
}

测试一下,没问题😆

const p = new MyPromise((resolve, reject) => {
  resolve(2)
})

p.then((v) => {
  console.log(v); // 2
})

const p2 = new MyPromise((resolve, reject) => {
  reject('errorMsg')
})

p2.then(null, (e) => {
  console.log(e); // errorMsg
})

这样第一步就完成了吗?不,当resolve/reject异步调用的时候就会存在问题,如下

const p = new MyPromise((resolve, reject) => {
  setTimeout(resolve, 0, 1);
})

// then中的逻辑不会执行
p.then((v) => {
  console.log(v);
})

上面这段代码的执行顺序是:MyPromise构造函数的executor -> p.then -> resolve,当 resolve执行的时候,p.then早已执行完成(then执行的时候,promise的实例状态还是pending,因此then中的onFulfilled/onRejected不会执行)

既然知道了原因,那么解决方案也有了,我们在resolve/reject函数内部调用onFulfilled/onRejected就好了,为了实现这点,我们需要用一个属性将它们保存起来用于在某个时机调用

class MyPromise {
  // ..
  #promiseHandler
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('is not a function')
    }
    const resolve = (result) => {
      // ...
      this.#run()
    }
    const reject = (result) => {
      // ...
      this.#run()
    }
    // ...
  }
  #run() {
    if (this.#state === PENDING) return
    if (!this.#promiseHandler) return
    const {
      onFulfilled,
      onRejected
    } = this.#promiseHandler
    if (this.#state === FULFILLED) {
      onFulfilled?.(this.#result)
    } else if (this.#state === REJECTED) {
      onRejected?.(this.#result)
    }
  }
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this.#promiseHandler = {
        onFulfilled,
        onRejected
      }
      this.#run()
    })
  }
}

再测试之前的例子就可以了😄。好耶,我们接着往下走

image.png

const p = new MyPromise((resolve, reject) => {
    setTimeout(resolve, 0)
})
p.then(() => {
    console.log(1)
})
p.then(() => {
    console.log(2)
})
p.then(() => {
    console.log(3)
})

按照规范,上面的代码执行结果依次是:1、2、3。但是目前我们实现的版本输出的只有3,这是因为每次调用then方法,就会覆盖之前的#promiseHandler,于是onFulfilled/onRejected也被覆盖,执行的也就是最后一个。调整也很简单,我们只需要把之前的#promiseHandler改成数组结构,维护一个onFulfilled/onRejected队列即可

class MyPromise {
  // ...
  // 修改成数组结构
  #promiseHandler = []
  constructor(executor) {
    // ...
  }
  #run() {
    if (this.#state === PENDING) return
    while(this.#promiseHandler.length) {
      const {
        onFulfilled,
        onRejected
      } = this.#promiseHandler.shift()
  
      if (this.#state === FULFILLED) {
        onFulfilled?.(this.#result)
      } else if (this.#state === REJECTED) {
        onRejected?.(this.#result)
      }
    }
  }
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this.#promiseHandler.push({
        onFulfilled,
        onRejected
      })
      this.#run()
    })
  }
}

再测试之前的例子,发现已经可以按照then的调用顺序输出结果了,😄又前进了一步

在前面实现的代码中,then方法返回的是一个新的Promise对象,接下来便是A+的规范的最后一个核心难点-then的链式调用

image.png

首先我们需要确认一个点:then方法返回的新的Promise对象的then何时会被调用。答案显而易见,当返回的新的 Promise对象状态更新的时候

我们可以把新Promise对象的resolve/reject也维护在#promiseHandler数组(如果不用数组维护,也会存在上面描述的问题)

then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this.#promiseHandler.push({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
      this.#run()
    })
}

我们先对Promise.thenonFulfilled/onRejected做下梳理:

onFulfilled/onRejected不是函数,则将当前Promise对象的结果/原因穿透到下一层

Promise.resolve(1)
.then(null)
.then(v => {
  console.log(v); // 1
})

Promise.reject('error')
.then(null, 1)
.then(null, (v) => {
  console.log(v); // error
})

这里需要额外提的一点,如果onFulfilled/onRejected不是函数,则会替换成一个函数,具体如下

image.png

即会存在以下转化

let _onFulfilled
if (typeof onFulfilled !== 'function') {
  // this.#result为之前兑现的值
  _onFulfilled = () => this.#result
} else {
  _onFulfilled = onFulfilled
}

let _onRejected
if (typeof onRejected !== 'function') {
  _onRejected = () => {
    // this.#result为之前拒绝的原因
    throw this.#result
  }
}

onFulfilled/onRejected是函数, 这里依据函数执行的返回值又有以下情况

返回值不是Promise对象, 则新Promise对象状态为fulfilled,并将返回值传递到下一个then方法中的onFulfilled回调中

Promise.resolve(1)
.then(v => 2)
.then(v => {
  console.log(v); // 2  如果没有返回值则为undefined
})

Promise.reject(1)
.then(null, () => {
  return 22
}).then((v2) => {
  console.log(v2) // 22 如果没有返回值则为undefined
})

返回值是Promise对象,则新Promise对象状态取决于返回的Promise对象的状态,根据状态为fulfilled/rejected调用第二个then方法的onFulfilled/onRejected函数

const p1 = Promise.resolve(1)
const p2 = Promise.reject('error')

Promise.resolve(1)
.then(() => p1)
.then((v) => {
  console.log(v); // 1
})

Promise.resolve(1)
.then(() => p2)
.then(null, (v) => {
  console.log(v); // error
})

针对以上三种情况,我们继续完善自己Promise

function isPromiseLike(obj) {
  return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

class MyPromise {
  // ...
  constructor(executor) {
    // ...
  }
  #run() {
    if (this.#state === PENDING) return
    while(this.#promiseHandler.length) {
      const {
        onFulfilled,
        onRejected,
        resolve,
        reject
      } = this.#promiseHandler.shift()
      if (this.#state === FULFILLED) {
        let _onFulfilled = onFulfilled
        if (typeof onFulfilled !== 'function') {
          _onFulfilled = () => this.#result
        }
        try {
          const result = _onFulfilled(this.#result)
          if (isPromiseLike(result)) {
            result.then(resolve, reject)
          } else {
            resolve(result)
          }
        } catch(e) {
           reject(e)
        }
      } else if (this.#state === REJECTED) {
        let _onRejected = onRejected
        if (typeof onRejected !== 'function') {
           _onRejected = () => {
              throw(this.#result)
           }
        }
        try {
          const reason = _onRejected(this.#result)
          if (isPromiseLike(reason)) {
            reason.then(resolve, reject)
          } else {
            resolve(reason)
          }
        } catch(e) {
          reject(e)
        }
      }
    }
  }
}

这里需要注意判断一个对象是否是Promise, 不能使用obj instanceof Promise这种方式,promise是具有符合A+规范的then方法的对象或函数

到这里,我们基本已经实现了一个满足A+规范promise,另外还有一个细节点:

image.png

这里我们可以借鉴vue源码中nextTick的实现

const runByMicroTask = (cb) => {
  // node环境
  if (typeof process === 'object' && typeof process.nextTick === 'function') {
    process.nextTick(cb)
  } else if (typeof MutationObserver === 'function') {
    const textNode = document.createTextNode('')
    const observer = new MutationObserver(cb)
    observer.observe(textNode, {
      characterData: true
    })
    textNode.data = 1
  } else {
    setTimeout(cb, 0);
  }
}

我们可以测试下,下面代码输出结果为:1、3、2

console.log(1);
runByMicroTask(() => {
  console.log(2);
})
console.log(3);

setimmediate理论上也可以,但是这个API不建议生产环境使用,详情可见 setImmediate

最后我们把这个功能点加到promise 逻辑中,并且整体优化下代码,把重复逻辑抽离出来。完整代码如下:

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function isPromiseLike(obj) {
  return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

class MyPromise {
  #state = PENDING
  #result = undefined
  #promiseHandler = []
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new Error('is not a function')
    }
    const resolve = (result) => {
      this.#statusChangeHandler(FULFILLED, result)
    }
    const reject = (result) => {
      this.#statusChangeHandler(REJECTED, result)
    }
    try {
      executor(resolve, reject)
    } catch(e) {
      reject(e)
    }
  }
  #statusChangeHandler(status, result) {
    if (this.#state !== PENDING) return
    this.#state = status
    this.#result = result
    this.#run()
  }
  #microTaskRun(cb) {
   if (typeof process === 'object' && typeof process.nextTick === 'function') {
     process.nextTick(cb)
   } else if (typeof MutationObserver === 'function') {
     const textNode = document.createTextNode('')
     const observer = new MutationObserver(cb)
     observer.observe(textNode, {
       characterData: true
     })
     textNode.data = 1
   } else {
    setTimeout(cb, 0);
   }
  }
  #run() {
    if (this.#state === PENDING) return
    while(this.#promiseHandler.length) {
      this.#thenableHandler(this.#promiseHandler.shift())
    }
  }
  #thenableHandler({onFulfilled, onRejected, resolve, reject}) {
    this.#microTaskRun(() => {
      let cb = this.#state === FULFILLED ? onFulfilled : onRejected
      if (typeof cb !== 'function') {
        cb = this.#state === FULFILLED ? () => this.#result : () => { throw this.#result }
      }
      try {
        const r = cb(this.#result)
        if (isPromiseLike(r)) {
          r.then(resolve, reject)
        } else {
          resolve(r)
        }
      } catch(e) {
        reject(e)
      }
    })
  }
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this.#promiseHandler.push({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
      this.#run()
    })
  }
}

最后

这里我们只是实现了一个满足Promise A+规范核心功能的promise,如果你用promises-aplus-tests测试库跑用例,会发现有不少边界情况的测例不会通过,主要在于这块我们并没有严格按照规范编写

image.png

我个人感觉不是很有必要,了解其核心思路即可。如果小伙伴们想看标准A+规范的实现可以自行查阅资料文章,另外这也并不是ES6中的promise。ES6在满足A+规范的前提下,还扩展丰富了其能力(Promise.resolve/reject/all/race等),这些将后续用一篇文章来手写

到这里,就是本篇文章的全部内容了

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

如果你有疑问或者出入,评论区告诉我,我们一起讨论