手动实现Promise

前言

众所周知,ES6中的Promise对象是异步编程的一种解决方案,比传统的解决方案——回调函数和事件,更合理和强大,目前也得到了前端领域的广泛使用。不仅是项目实践中常用,面试中也经常出现。
Promise的使用想必大家都很熟练,可究其内部原理,很多人都是一知半解。这导致面试中出现Promise原理甚至要求手动封装时,很多人都会挂掉。本着知其然,也要知其所以然的目的,开始对Promise进行了探索。


一、为什么要使用Promise

大家都知道JavaScript一大特点就是单线程,为了不阻塞主线程,有些耗时操作(比如ajax)必须放在任务队列中异步执行。传统的异步编程解决方案之一回调,很容易产生臭名昭著的回调地狱问题。

setTimeout(function () {
    console.log('延时触发');
}, 2000);

fs.readFile('./sample.txt', 'utf-8', function (err, res) {
    console.log(res);
});

上面就是典型的回调函数,不论是在浏览器中,还是在node中,JavaScript本身是单线程,因此,为了应对一些单线程带来的问题,异步编程成为了JavaScript中非常重要的一部分。
不论是浏览器中最为常见的ajax、事件监听,还是node中文件读取、网络编程、数据库等操作,都离不开异步编程。在异步编程中,许多操作都会放在回调函数(callback)中。同步与异步的混杂、过多的回调嵌套都会使得代码变得难以理解与维护,这也是常受人诟病的地方。
回调嵌套过多后,你的代码会变成这样:

asyncFunc1(opt, (...args1) => {
    asyncFunc2(opt, (...args2) => {
        asyncFunc3(opt, (...args3) => {
            asyncFunc4(opt, (...args4) => {
                // some operation
            });
        });
    });
});

左侧明显出现了一个三角形的缩进区域,过多的回调也就让我们陷入“回调地狱”。
虽然回调地狱可以通过减少嵌套、模块化等方式来解决,但我们有更好的方案可以采取,那就是Promise

二、含义与规范

Promise 是一个对象,保存着异步操作的结果,在异步操作结束后,会变更 Promise 的状态,然后调用注册在 then 方法上回调函数。
实际上,Promise 是对 Promises/A+ 规范的一种实现。 ES6 原生提供了 Promise 对象,统一了用法。

三、封装Promise

下面将依据规范和ES6来进行Promise的封装

1.promise构造函数

规范没有指明如何书写构造函数,那就参考ES6的构造方式:

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject

resolve 函数的作用是将 Promise 对象的状态从 pending 变为 fulfilled ,在异步操作成功时调用,并将异步操作的结果,作为参数传递给注册在 then 方法上的回调函数(then方法的第一个参数); reject 函数的作用是将 Promise 对象的状态从 pending 变为 rejected ,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递给注册在 then 方法上的回调函数(then方法的第二个参数)

所以我们要实现的 promise (小写以便区分ES6的 Promise )构造函数大体如下:

// promise 构造函数
function promise (fn) {
  let that = this
  that.status = 'pending' // 存储promise的state
  that.value = '' // 存储promise的value
  that.reason = '' // 存储promise的reason
  that.onFulfilledCb = [] // 存储then方法中注册的回调函数(第一个参数)
  that.onRejectedCb = [] // 存储then方法中注册的回调函数(第二个参数)

  // 2.1
  function resolve (value) {
    // 将promise的状态从pending更改为fulfilled,并且以value为参数依次调用then方法中注册的回调
    setTimeout (() => {
      if (that.status === 'pending') {
        that.status = 'fulfilled'
        that.value = value
        // 2.2.2、2.2.6
        that.onFulfilledCb.map( item => {
          item(that.value)
        })
      }
    }, 0)
  }

  function reject (reason) {
    // 将promise的状态从pending更改为rejected,并且以reason为参数依次调用then方法中注册的回调
    setTimeout(() => {
      if (that.status === 'pending') {
        that.status = 'rejected'
        that.reason = reason
        // 2.2.3、2.2.6
        that.onRejectedCb.map( item => {
          item(that.reason)
        })
      }
    }, 0)
  }

  fn(resolve, reject)
}

规范2.2.6中明确指明 then 方法可以被同一个 promise 对象调用,所以这里需要用一个数组 onFulfilledCb 来存储then方法中注册的回调

这里我们执行 resolve reject 内部代码使用setTimeout,是为了确保 then 方法上注册的回调能异步执行(规范3.1)

2.then方法

promise 实例具有 then 方法,也就是说,then方法是定义在原型对象 promise.prototype 上的。它的作用是为 promise 实例添加状态改变时的回调函数。

规范2.2 promise 必须提供一个 then 方法 promise.then(onFulfilled,onRejected)
规范2.2.7 then 方法必须返回一个新的promise

阅读理解规范2.1和2.2,我们也很容易对then方法进行实现:

promise.prototype.then = function (onFulfilled, onRejected) {
  let that = this
  let promise2  

  // 2.2.1、2.2.5
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
  onRejected = typeof onRejected === 'function' ? onRejected : r => r

  if (that.status === 'pending') {
    // 2.2.7
    return promise2 = new promise((resolve, reject) => {
      that.onFulfilledCb.push(value => {
        try {
          let x = onFulfilled(value)
        } catch(e) {
          // 2.2.7.2
          reject(e)
        }
      })

      that.onRejectedCb.push(reason => {
        try {
          let x = onRejected(reason)
        } catch(e) {
          // 2.2.7.2
          reject(e)
        }
      })
    })
  }
}

重点在于对 onFulfilled 、 onRejected 函数的返回值x如何处理,规范中提到一个概念叫 PromiseResolutionProcedure ,这里我们就叫做Promise解决过程

Promise 解决过程是一个抽象的操作,需要输入一个 promise 和一个值,我们表示为 [[Resolve]] (promise,x),如果 x 有 then 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;否则用 x的值来执行promise。

3.promise解决过程

对照规范2.3,我们再来实现 promise resolution , promise resolution 针对x的类型做了各种处理:如果 promise 和 x 指向同一对象,以 TypeError 为 reason 拒绝执行 promise、如果 x 为 promise ,则使 promise 接受 x 的状态、如果 x 为对象或者函数,判断 x.then 是否是函数、 如果 x 不为对象或者函数,以 x 为参数执行 promise(resolve和reject参数携带promise2的作用域,方便在x状态变更后去更改promise2的状态)

// promise resolution
function promiseResolution (promise2, x, resolve, reject) {
  let then
  let thenCalled = false
  // 2.3.1
  if (promise2 === x) {
    return reject(new TypeError('promise2 === x is not allowed'))
  }
  // 2.3.2
  if (x instanceof promise) {
    x.then(resolve, reject)
  }
  // 2.3.3
  if (typeof x === 'object' || typeof x === 'function') {
    try {
      // 2.3.3.1
      then = x.then
      if (typeof then === 'function') {
        // 2.3.3.2
        then.call(x, function resolvePromise(y) {
          // 2.3.3.3.3
          if (thenCalled) return
          thenCalled = true
          // 2.3.3.3.1
          return promiseResolution(promise2, y, resolve, reject)
        }, function rejectPromise(r) {
          // 2.3.3.3.3
          if (thenCalled) return
          thenCalled = true
          // 2.3.3.3.2
          return reject(r)
        })
      } else {
        // 2.3.3.4
        resolve(x)
      }
    } catch(e) {
      // 2.3.3.3.4.1
      if (thenCalled) return
      thenCalled = true
      // 2.3.3.2
      reject(e)
    }
  } else {
    // 2.3.4
    resolve(x)
  }
}
4.思考

以上,基本实现了一个简易版的 promise ,说白了,就是对 Promises/A+ 规范的一个翻译,将规范翻译成代码。因为大家的实现都是基于这个规范,所以不同的 promise 实现之间能够共存(不得不说制定规范的人才是最厉害的)

function doSomething () {
  return new promise((resolve, reject) => {
    setTimeout(() => {
      resolve('promise done')
    }, 2000)
  })
}

function doSomethingElse () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('ES6 promise')
    }, 1000)
  })
}

this.promise2 = doSomething().then(doSomethingElse)
console.log(this.promise2)

ES7 的 Async/Await 也是基于 promise 来实现的,可以理解成 async 函数会隐式地返回一个 Promise , await 后面的执行代码放到 then 方法中

更深层次的思考,你需要理解规范中每一条制定的意义,比如为什么then方法不像jQuery那样返回this而是要重新返回一个新的promise对象(如果then返回了this,那么promise2就和promise1的状态同步,promise1状态变更后,promise2就没办法接受后面异步操作进行的状态变更)、 promise解决过程 中为什么要规定 promise2 和 x 不能指向同一对象(防止循环引用)。

5.极简版promise

考虑到上述实现方法还是太全面,完全依照规范来写的,不直观且难懂。下面写一个极简版的实现方式,以便大家理解和在面试时使用:

function promise () {
  this.status = 'pending' // 2.1
  this.msg = '' // 存储value与reason
  let process = arguments[0],
       that = this
  process (function () {
    that.status = 'resolve'
    that.msg = argument[0]
  }, function () {
    that.status = 'reject'
    that.msg = argument[0]
  })
  return this
}

promise.prototype.then = function () {
  if (this.status === 'resolve') {
    arguments[0](this.msg)
  } else if (this.status === 'reject' && arguments[1]) {
    arguments[1](this.msg)
  }
}

四、promise的弊端

promise彻底解决了callback hell,但也存在以下一些问题

1.延时问题(涉及到evnet loop)
2.promise一旦创建,无法取消
3.pending状态的时候,无法得知进展到哪一步(比如接口超时,可以借助race方法)
4.promise会吞掉内部抛出的错误,不会反映到外部。如果最后一个then方法里出现错误,无法发现。(可以采取hack形式,在promise构造函数中判断onRejectedCb的数组长度,如果为0,就是没有注册回调,这个时候就抛出错误,某些库实现done方法,它不会返回一个promise对象,且在done()中未经处理的异常不会被promise实例所捕获)
5.then方法每次调用都会创建一个新的promise对象,一定程度上造成了内存的浪费

五、总结

支持 promise 的库有很多,现在主流的浏览器也都原生支持 promise 了,而且还有更好用的 Async/Await 。希望大家都有所收获!

你可能感兴趣的:(手动实现Promise)