Promise的分层解析及实现

今天写的这篇文章是关于 Promise 的,其实在谷歌一搜索,会出来不少关于Promise的文章,那为何还要写这篇文章呢?git

我相信必定有人用过 Promise ,但总有点似懂非懂的感受,好比咱们知道异步操做的执行是经过 then 来实现的,那后面的操做是如何得知前面异步操做完成的呢? Promise 具体是怎样实现的呢?github

因此我写这篇文章的目的主要是从最基础的点开始剖析,一步一步来理解 Promise 的背后实现原理编程

也是由于最近本身的困惑,后面边看文章,边调试代码,以致于对 Promise 的理解又上升了一个台阶~数组

为何会有 Promise 的产生

咱们能够想象这样一种应用场景,须要连续执行两个或者多个异步操做,每个后来的操做都在前面的操做执行成功以后,带着上一步操做所返回的结果开始执行promise

在过去,咱们会作多重的异步操做,好比缓存

doFirstThing((firstResult) => {
  doSecondThing(firstResult, (secondResult) => {
    console.log(`The secondResult is:` + secondResult)
  })
})
复制代码

这种多层嵌套来解决一个异步操做依赖前一个异步操做的需求,不只层次不够清晰,当异步操做过多时,还会出现经典的回调地狱异步

那正确的打开方式是怎样的呢?Promise 提供了一个解决上述问题的模式,咱们先回到上面那个多层异步嵌套的问题,接下来转变为 Promise 的实现方式:async

function doFirstThing() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('获取第一个数据')
      let firstResult = 3 + 4
      resolve(firstResult)
    },400)
  })
}

function doSecondThing(firstResult) {
  console.log('获取第二个数据')
  let secondResult = firstResult * 5
  return secondResult
}

doFirstThing()
  .then(firstResult => doSecondThing(firstResult))
  .then(secondResult => {
    console.log(`The secondResult Result: ${secondResult}`
  )})
  .catch(err => {
    console.log('err',err)
  })
复制代码

能够看到结果就是咱们预期获得的,须要注意的一点是,若是想要在回调中获取上个 Promise 中的结果,上个 Promise 中必须有返回结果ide

Promise 究竟是什么

相信通过上面的应用场景,已经大体明白 Promise 的做用了,那它的具体定义是什么呢?异步编程

Promise 是对异步编程的一种抽象,是一个代理对象,表明一个必须进行异步处理的函数返回的值或抛出的异常

简单来讲,Promise 主要就是为了解决异步回调的问题,正如上面的例子所示

能够将异步对象和回调函数脱离开来,经过 then 方法在这个异步操做上面绑定回调函数

用 Promise 来处理异步回调使得代码层析清晰,便于理解,且更加容易维护,其主流规范目前主要是 Promises/A+ ,下面介绍具体的API

状态和值

Promise 有3种状态: pending (待解决,这也是初始状态), fulfilled (完成), rejected (拒绝)

状态只能由 pending 变为 fulfilled 或由 pending 变为 rejected ,且状态改变以后不会再发生变化,会一直保持这个状态

Promise 的值是指状态改变时传递给回调函数的值

接口

Promise 惟一接口 then 方法,它须要2个参数,分别是 onResolvedonRejected

而且须要返回一个 promise 对象来支持链式调用

Promise 的构造函数接收一个函数参数,参数形式是固定的异步任务,接收的函数参数又包含 resolvereject 两个函数参数,能够用于改变 Promise 的状态和传入 Promise 的值

  1. resolve:将 Promise 对象的状态从 pending (进行中)变为 fulfilled (已成功)

  2. reject:将 Promise 对象的状态从 pending (进行中)变为 rejected (已失败)

  3. resolve 和 reject 均可以传入任意类型的值做为实参,表示 Promise 对象成功( fulfilled )和失败( rejected )的值

了解了 Promise 的状态和值,接下来,咱们开始讲解 Promise 的实现步骤

Promise 是怎样实现的

咱们已经了解到实现多个相互依赖异步操做的执行是经过 then 来实现的,那从新回到最开始的疑问,后面的操做是怎么得知异步操做完成了呢?

在讲解 Promise 实现以前,咱们仍是先简要提一下Vue的发布/订阅模式:首先有一个事件数组来收集事件,而后订阅经过 on 将事件放入数组, emit 触发数组相应事件

那 Promise 呢? Promise 内部其实也有一个数组队列存放事件, then 里边的回调函数就存放数组队列中。下面咱们能够看下具体的实现步骤

实现 promise 雏形

( demo1 )

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    executor(value => {
      this.status = 'resolve',
      this.value = value
    }, reason => {
      this.status = 'rejected'
      this.value = reason
    })
  }

  then(onResolved) {
    onResolved(this.value)
  }
}

// 测试
var promise = new Promise((resolve, reject) => {
  resolve('promise')
})

promise.then(value => {
  console.log('value',value)
})
promise.then(value => {
  console.log('value',value)
})
复制代码

上述代码很简单,大体的逻辑是:

经过构造器 constructor 定义 Promise 的初始状态和初始值,经过 Promise 的构造函数接收一个函数参数 executor , 接收的函数参数又包含 resolvereject 两个函数参数,能够用于改变 Promise 的状态和传入 Promise 的值。

而后调用 then 方法,将 Promise 操做成功后的值传入回调函数

异步操做

相信有人会好奇,上述 Promise 实例中都是进行的同步操做,可是每每咱们使用 Promise 都是进行的异步操做,那会出现怎样的结果呢?在上述例子上进行修改,咱们用 setTimeout 来模拟异步的实现

// 测试
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise')
  },300)
})
复制代码

会发现后面的回调函数中打印出来的值都是undefined

image

很明显,这种错误的形成是由于 then 里边的回调函数在实例化 Promise 操做 resolve 或 reject 以前就执行完成了,因此咱们应该设定触发回调函数执行的标识,也就是在状态和值发生改变以后再执行回调函数

正确的逻辑是这样的:

  1. 调用 then 方法,将须要在 Promise 异步操做成功时执行的回调函数放入 children 数组队列中,其实也就是注册回调函数,相似于观察者模式

  2. 建立 Promise 实例时传入的函数会被赋予一个函数类型的参数,即 resolve ( reject ),它接收一个参数 value ,当异步操做执行成功后,会调用 resolve ( reject )方法,这时候其实真正执行的操做是将 children 队列中的回调一一执行

在 demo1 的基础上修改以下:

( demo2 )

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    this.children = [] // children为数组队列,存放多个回调函数
    executor(value => {
      this.status = 'resolve',
      this.setValue(value)
    }, reason => {
      this.status = 'rejected'
      this.setValue(reason)
    })
  }

  then (onResolved) {
    this.children.push(onResolved)
  }

  setValue (value) {
    this.value = value
    this.children.forEach(child => {
      child(this.value)
    })
  }
}

// 测试
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise')
  },300)
})
promise.then(value => {
  console.log('value',value)
})
复制代码

首先实例化 Promise 时,传给 promise 的函数发送异步请求,接着调用 promise 对象的 then 函数,注册请求成功的回调函数,而后当异步请求发送成功时,调用 resolve ( rejected )方法,该方法依次执行 then 方法注册的回调数组

实现 promise 开枝散叶

相信仔细的人应该能够看出来,then 方法应该可以支持链式调用,可是上面的初步实现显然没法支持链式调用

那怎样才能作到支持链式调用呢?其实实现也很简单:

then (onResolved) {
  this.children.push(onResolved)
  return this
}
复制代码
// 测试
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise')
  },300)
})
promise.then(value1 => {
  console.log('value1',value1)
}).then(value2 => {
  console.log('value2',value2)
})
复制代码

then 方法中加入 return this 实现了链式调用,但若是须要在 then 回调函数中返回一个值 value 或者 promise ,传给下一个 then 回调函数呢?

先来看返回值 value 的状况,好比:

// 测试
promise.then(value1 => {
  console.log('value1',value1)
  let value = 'promise2'
  return value
}).then(value2 => {
  console.log('value2',value2)
})
复制代码

在 demo2 的基础上进行改造:

( demo3 )

then (onResolved) {
  var child = new Promise(() => {})
  child.onResolved = onResolved
  this.children.push(child)
  return this
}

setValue (value) {
  this.value = value
  this.children.forEach(child => {
    var ret = child.onResolved(this.value)
    this.value = ret
  })
}
复制代码

原理就是在调用 Promise 对象的 then 函数时,注册全部请求成功的回调函数,后续在 setValue 函数中循环全部的回调函数,每次执行完一个回调函数就会更新 this.value 的值,而后将更新后的 this.value 传入下一个回调函数里,这样就解决了传值的问题

但这样也会出现一个问题,咱们只考虑了串行 Promise 的状况下依次更新 this.value 的值,若是串行和并行一块儿呢?好比:

// 测试
// 串行
promise.then(value1 => {
  console.log('value1',value1)
  let value = 'promise2'
  return value
}).then(value2 => {
  console.log('value2',value2)
})

// 并行
promise.then(value1 => {
  console.log('value1',value1)
})
复制代码

打印出来的结果最后一个 value1 为 undefined ,由于咱们一直在改变 this.value 的值,而且在串行最后一个 then 回调函数中也显示设定返回值,默认返回 undefined

image

可见 return this 并行不通,继续在 demo3 的基础上改造 then 和 setValue 函数以下:

( demo4 )

then (onResolved) {
  var child = new Promise(() => {})
  child.onResolved = onResolved
  this.children.push(child)
  return child
}
setValue (value) {
  this.value = value
  this.children.forEach(child => {
    var ret = child.onResolved(this.value)
    child.setValue(ret)
  })
}
复制代码

那若是 then 回调函数中返回一个 promise 呢?好比:

// 测试
promise.then(value1 => {
  console.log('value1',value1)
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('promise2')
    },200)
  })
}).then(value2 => {
  console.log('value2',value2)
})
复制代码

image

很明显,打印出来的结果是个 Promise 。继续在 demo4 的基础上改造 setValue 函数

( demo5 )

setValue (value) {
  if (value && value.then) {
    value.then(realValue => {
      this.setValue(realValue)
    })
  } else {
    this.value = value
    this.children.forEach(child => {
      var ret = child.onResolved(this.value)
      child.setValue(ret)
    })
  }
}
复制代码

在 setValue 方法里面,咱们对 value 进行了判断,若是是一个 promise 对象,就会调用其 then 方法,造成一个嵌套,直到其不是 promise 对象为止

到目前为止,咱们已经实现了 Promise 的主要功能—'开枝散叶',状态和值的有序更新

实现 promise 错误处理

上面全部列举到的 demo 都是在异步操做成功的状况下进行的,但异步操做不可能都成功,在异步操做失败时,状态为标记为 rejected ,并执行注册的失败回调

rejected 失败的错误处理也相似于 resolve 成功状态下的处理,接着在 demo5 的注册回调、处理状态上加入新的逻辑,在 Promise 上加入 resolvereject 静态函数

( demo6 )

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    this.children = []
    executor(value => {
      this.setValue(value, 'resolved')
    }, reason => {
      this.setValue(reason, 'rejected')
    })
  }

  then (onResolved, onRejected) {
    var child = new Promise(() => {})
    this.children.push(child)
    Object.assign(child, {
      onResolved: onResolved || (value => value),
      onRejected: onRejected || (reason => Promise.reject(reason))
    })
    if (this.status !== 'pending') {
      child.triggerHandler(this.value, this.status)
    }
    return child
  }

  catch (onRejected) {
    return this.then(null, onRejected)
  }

  triggerHandler (parentValue, status) {
    var handler
    if (status === 'resolved') {
      handler = this.onResolved
    } else if (status === 'rejected') {
      handler = this.onRejected
    }
    this.setValue(handler(parentValue), 'resolved')
  }

  setValue (value, status) {
    if (value && value.then) {
      value.then(realValue => {
        this.setValue(realValue, 'resolved')
      }, reason => {
        this.setValue(reason, 'rejected')
      })
    } else {
      this.status = status
      this.value = value
      this.children.forEach(child => {
        child.triggerHandler(value, status)
      })
    }
  }

  static resolve (value) {
    return new Promise(resolve => {
      resolve(value)
    })
  }

  static reject (reason) {
    return new Promise((resolve, reject) => {
      reject(reason)
    })
  }
}
复制代码

then 函数中有两个回调 handler, 分别是onResolvedonResolved ,表示成功执行的回调函数和是失败执行的回调函数,并设置默认值,保持链式链接

定义一个 triggerHandler 函数用来判断当前 child 的 status ,并触发本身的 handler ,执行回调函数,再次更新 status 和 value

setValue 函数同时设置 Promise 本身的状态和值,而后在从新设置新的状态以后循环遍历 children

为了更高效率的运行,在 then 函数中注册回调函数时加入状态判断,若是状态改变不为 pending ,说明 setValue 函数已经执行,状态已经发生了更改,就立马执行 triggerHandle r函数;若是状态为 pending ,则在 setValue 函数执行时再触发 triggerHandle `函数

Promise 中的 nextTick

Promise/A+规范要求 handler 执行必须是异步的, 具体能够参见标准 3.1 条

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called

这里用 setTimeout 简单实现一个跨平台的 nextTick

function nextTick(func) {
  setTimeout(func)
}
复制代码

而后使用 nextTick 包裹 triggerHandler

triggerHandler (status, parentValue) {
  nextTick(() => {
    var handler
    if (status === 'resolved') {
      handler = this.onResolved
    } else if (status === 'rejected') {
      handler = this.onRejected
    }
    this.setStatus('resolved', handler(parentValue))
  })
}
复制代码

在 demo6 中咱们实现了不论是异步仍是同步均可以执行 triggerHandler ,那为何要强制异步的要求呢?

主要是为了流程可预测,标准须要强制异步。可类比于经典的 image onload 问题

var image = new Image()
image.onload = funtion
image.src = 'url'
复制代码

src 属性为何须要写在 onload 事件后面?

由于 js 内部是按顺序逐行执行的,能够认为是同步的,给 image 赋值 src 时,去加载图片这个过程是异步的,这个异步过程完成后,若是有 onload ,则执行 onload

若是先赋值 src ,那么这个异步过程可能在你赋值 onload 以前就完成了(好比图片缓存),那么 onload 就不会执行

反之, js 同步执行肯定 onload 赋值完成后才会赋值 src ,能够保证这个异步过程在 onload 赋值完成后才开始进行,也就保证了 onload 必定会被执行到

一样的,在Promise中,咱们但愿代码执行顺序是彻底能够预测的,不容许出现任何问题

总结

上述 Promise 每一个阶段层次的实现代码可见个人 github 分层实现Promise

须要注意的是:

  1. promise 里面的 then 函数仅仅是注册了后续须要执行的回调函数,真正的执行是在 triggerHandler 方法里

  2. then 和 catch 注册完回调函数后,返回的是一个新的 Promise 对象,以延续链式调用

  3. 对于内部 pending 、fulfilled 和 rejected 的状态转变,经过 handler 触发 resolve 和 reject 方法,而后在 setValue 中更改状态和值

参考文献

深刻理解Promise

相关文章
相关标签/搜索