图解Promise

452

本文将以图解和源码相结合的方式介绍Promise的原理,主要以Promises/A+规范为主,帮助大家更好地理解Promise。

下文中提到的源码非官方源码,而是以这份模拟源码为主,原因是相比官方源码,它更容易理解。该源码能够跑通Promises/A+所有测试用例,所以功能是一样的。

Promise的本质

Promise代表的是一个承诺,表示某个异步操作暂时还没有结果,但承诺在将来的某个时候会告知用户结果。

let promise1 = new Promise((resolve, reject) => {
    try {
        // 一些异步操作,比如setTimeout,或者异步请求
        setTimeout(() => {
            resolve(1)
        }, 1000)
    } (err) {
        // 出错时告知错误信息
        reject(err)
    }
})

用图形来表示,每个promise实例内部都有两个接口,resolve和reject,通过回调函数参数的形式暴露给外面,把调用权交个外面,相当于告诉用户:“嘿哥们,请放心执行你的任务,我一直都在,有结果后告诉我一声,成功请call resolve,失败请call reject”。

image.png

promise对象有三种状态:pending、fulfilled和rejected。

pending - The initial state of a promise.

fulfilled - The state of a promise representing a successful operation.

rejected - The state of a promise representing a failed operation.

只能由pending到fulfilled或者从pending到rejected,而且状态不可逆,我们规定pending为“未完结”状态,fulfilled和rejected为“完结”状态。

image.png

一起来看下Promise构造函数的源码:

function Promise(f) {
  this.result = null
  this.state = PENDING
  ...
  let ignore = false

  let resolve = value => {
    if (ignore) return
    ignore = true
    resolvePromise(this, value, onFulfilled, onRejected)
  }

  let reject = reason => {
    if (ignore) return
    ignore = true
    onRejected(reason)
  }

  try {
    f(resolve, reject)
  } catch (error) {
    reject(error)
  }
}

resolve和reject函数是构造函数内的局部函数,作为两个参数传递给 f 执行函数。另外有一个ignore局部变量,保证resolve和reject内的逻辑只会执行一次。另外state属性代表promise当前的状态,而result代表promise完结的结果。当fulfilled时,代表成功结果,而rejected时代表失败原因。

then方法

then是promise最关键的方法,用户通过它来获取异步结果,它拥有两个函数参数,分别获取成功结果和失败原因。

promise1.then((data) => {
    console.log('成功:', data)
}, (err) => {
    console.error('失败:', err)
})

按照Promise的规范,then方法会返回一个新的Promise实例,我们假设为promise2。promise1中对promise2的resolve和reject进行了引用,这是promise链式调用的关键,我们后面讲。

先看then方法的源码:

Promise.prototype.then = function(onFulfilled, onRejected) {
  return new Promise((resolve, reject) => {
    let callback = { onFulfilled, onRejected, resolve, reject }

    if (this.state === PENDING) {
      this.callbacks.push(callback)
    } else {
      setTimeout(() => handleCallback(callback, this.state, this.result), 0)
    }
  })
}

代码很简单,通过一个callback对象保存当前promise的onFulfilled、onRejected函数和下个promise的resolve、reject函数。如果promise的状态还未完结,就暂存到callbacks数组;如果状态已完结,则立即用完结状态的结果处理callback。

图解如下:

image.png

链式调用

上文提到then方法会返回一个新的promise对象,所以后面的then方法就是第二个promise的方法,以此类推,就形成了一条promise链,图解如下:

image.png

有点像单向数据列表有没有:

image.png

单向数据链表

当promise1的resolve被调用时,先判断和调用onFulfilled函数,而当promise1的reject被调用时,则会判断和调用onRejected函数。

处理完onFulfilled或onRejected逻辑后,将处理结果传递给promise2的resolve或者reject,如此,promise2的状态也进行了联动改变,这就是promise链式调用的本质。

promise同步任务

promise一般用于异步任务,但是也支持同步任务,可以猜猜下面代码的运行结果:

new Promise((resolve, reject) => {
    console.log(1)
    resolve(1)
})
.then((result) => {
    console.log(2)
})

console.log(3)

// 输出结果
1
3
2

可以看到,即使是同步resolve,onFulfilled函数也未能同步执行,原因是使用了setTimeout,保证了onFulfilled的异步执行。上源码:

Promise.prototype.then = function(onFulfilled, onRejected) {
  return new Promise((resolve, reject) => {
    let callback = { onFulfilled, onRejected, resolve, reject }

    if (this.state === PENDING) {
      this.callbacks.push(callback)
    } else {
      // 使用setTimeout保证异步
      setTimeout(() => handleCallback(callback, this.state, this.result), 0)
    }
  })
}

const transition = (promise, state, result) => {
  if (promise.state !== PENDING) return
  promise.state = state
  promise.result = result

  // 状态改变时也用了setTimeout保证异步
  setTimeout(() => handleCallbacks(promise.callbacks, state, result), 0)
}

resolve promise实例

熟悉promise的同学应该都知道,onFulfilled函数可以返回任何有效的js值,包括: undefined、promise实例和thenable对象,这里重点讲下promise实例的情况,thenable的原理一样,读者可自行理解。测试代码如下:

let promise1 = new Promise((resolve, reject) => {
    try {
        // 一些异步操作,比如setTimeout,或者异步请求
        setTimeout(() => {
            resolve(1)
        }, 1000)
    } (err) {
        // 出错时告知错误信息
        reject(err)
    }
})

let externalPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('task result')
    }, 1000)
})

promise1.then((data) => {
    return externalPromise   
}).then((data) => {
    console.log('成功:', data)
})

image.png

其中promise1.then生成了promise2, 而第二个then生成了promise3。promise1的onFulfilled返回了externalPromise对象,当externalPromise fulfill时,就会调用promise2的resolve函数,从而触发promise2的resolve流程,从而调用promise2的onFulfilled函数,由此,又回到了原来的resolve链路。

有点像婚姻状况出现了点小插曲,最后还是回到了正轨^_^。

resolve自己

在Promises/A+的规范里,是不允许resolve promise自己的。为什么?

先看源码:

function Promise(f) {
  ...

  let ignore = false
  let resolve = value => {
    if (ignore) return

    ignore = true
    resolvePromise(this, value, onFulfilled, onRejected)
  }
  ...

  try {
    f(resolve, reject)
  } catch (error) {
    reject(error)
  }
}

const resolvePromise = (promise, result, resolve, reject) => {
  ...

  if (isPromise(result)) {
    return result.then(resolve, reject)
  }
  ...

  resolve(result)
}

当resolve的value为本身时,resolvePromise函数的 promise === result,promise的onFulfilled和onRejected传给result的then方法,等待result的结果,这就导致了自己等自己,进入了死循环。测试一下:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(promise)
    }, 0)
})

promise.then((result) => {
    console.log('resolve:', result)
})

// 输出结果

VM783:3 Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
    at <anonymous>
    at <anonymous>:3:9

“奇怪”的reject流程

在promise链式调用里,reject跟resolve的流程不太一样,当我刚接触promise时还不太理解。reject流程的表现为,当头部promise rejected时,后续的promise都会reject,直到遇到第一个拥有onRejected的promise,用onRejected的执行结果,resolve后面的promise,说直白点就是状态“反转”了。

new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(1)
  }, 10)
})
.then((result) => {
  console.log('onResolved_1', result)
  return 'onResolved_1'
}, (reason) => {
  console.log('onRejected_1', reason)
  return 'onRejected_1'
})
.then((result) => {
  console.log('onResolved_2', result)
  return 'onResolved_2'
}, (reason) => {
  console.log('onRejected_2', reason)
  return 'onRejected_2'
})
.then((result) => {
  console.log('onResolved_3', result)
  return 'onResolved_3'
}, (reason) => {
  console.log('onRejected_3', reason)
  return 'onRejected_3'
})
.catch(() => {
  console.log('catch', reason)
})

// 输出结果
onRejected_1 1
onResolved_2 onRejected_1
onResolved_3 onResolved_2

我们可以看到经过第一个then的onRejected后,触发了第二个then的onFulfilled函数,但是收到的是第一个then的onRejected返回的结果。还是看源码比较形象:

const handleCallback = (callback, state, result) => {
  let { onFulfilled, onRejected, resolve, reject } = callback
  try {
    if (state === FULFILLED) {
      isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result)
    } else if (state === REJECTED) {
      isFunction(onRejected) ? resolve(onRejected(result)) : reject(result)
    }
  } catch (error) {
    reject(error)
  }
}

我们可以看到当state === FULFILLED时,如果onRejected为函数时,执行onRejected后,调用了下个promise的resolve接口。

猜测这么设计的目的是给用户一个错误处理的机会,但又不影响后面的链式调用。但需要特别注意的是第二个then中的onResolved中需要判断参数result的类型,否则代码容易出错。

这里,应该也有小伙伴会有疑问,为什么我最后的catch回调函数没有执行?

catch的本质就是onFulfilled函数为null的then函数,参考官方实现

// https://github.com/then/promise/blob/master/src/es6-extensions.js
Promise.prototype['catch'] = function (onRejected) {
  return this.then(null, onRejected);
};

之前也提到了,中间promise的状态“反转”了,后面的promise变成了resolve状态,所以最后的onRejected也理所当然不会被调用。

链式调用中的异常处理

经常会看到这类面试题,请说出以下代码的运行结果:

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 10)
})
.then((result) => {
  console.log('onResolved_1', result)
  throw new Error('custom error')
}, (reason) => {
  console.log('onRejected_1', reason)
  return 'onRejected_1'
})
.then((result) => {
  console.log('onResolved_2', result)
  return 'onResolved_2'
}, (reason) => {
  console.log('onRejected_2', reason)
  return 'onRejected_2'
})

// 运行结果
onResolved_1 1
VM713:17 onRejected_2 Error: custom error

大部分同学应该猜对了结果,但原因是什么?一起来看源码可能更能理解:

const handleCallback = (callback, state, result) => {
  let { onFulfilled, onRejected, resolve, reject } = callback
  try {
    if (state === FULFILLED) {
      isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result)
    } else if (state === REJECTED) {
      isFunction(onRejected) ? resolve(onRejected(result)) : reject(result)
    }
  } catch (error) {
    reject(error)
  }
}

这里的onFulfilled和onRejected就是第一个then的两个参数,而resolve和reject就是第一个then函数新生成promise对象的resolve和reject接口,我们可以看到,外侧包了一层try...catch,一旦内部代码报错,就会调用下一个promise的reject接口,并不会调用当前promise的onRejected函数。

搞清楚catch的本质以及链式调用中的异常处理,结合自己多年的经验,给出个人的最佳实践:

  • then方法的onRejected函数留空,由最后的catch方法统一处理异常
  • catch后不再添加then方法
let syncTask = (resolve, reject) => {}
let handle1 = (res) => {...}
let handle2 = (res) => {...}
let handle3 = (res) => {...}
...

let onError = (err) => { console.log('onError:', err) }

let p = new Promsie(syncTask)
p
.then(handle1)
.then(handle2)
.then(handle3)
.catch(onError)

Promise.resolve的实现

我们经常会看到以下代码:

Promise.resolve(1)
.then((result) => {
    console.log('result:', result)
})

Promise.resolve并不在Promises/A+的规范里,但通过上面内容的学习,懂得promise的原理后,自己实现一个也就非常简单了,其实就是返回一个promise实例:

Promise.resolve = function (value) {
    if (value instanceof Promise) return value;
    return new Promise((resolve, reject) => {
        resolve(value)
    })
};

详细实现请看官方实现:github.com/then/promis…

最后

以上就是我理解的Promise,希望能帮助正在学习Promise的同学更好地理解。最后,附上一套Promise问答题,结合上述内容,看看自己是否都能答对。

Promise 必知必会(十道题)

参考文档

  1. Promises/A+规范
  2. 100 行代码实现 Promises/A+ 规范
  3. Promise 必知必会(十道题)
  4. Promise不会??看这里!!!史上最通俗易懂的Promise!!!
  5. 官方源码