包教包会,和你实现一个Promise(一)

1、开始

大约从半年前开始,就想试着写一个符合规范的Promise,可是一直写不出来,期间也看了很多Promise的文章,可是一般看了一点就看不懂了。最近几天,又仔仔细细地研究了一遍并查阅了不少文章,终于完全整明白了Promise了。之因此要写这个小系列文章,是由于我以为网上大部分写Promise实现的文章都有点深,之前我看的时候就看不懂,并非说写的很差,只是有很多还在学习中的小伙伴看不明白,因此,我决定尽我所能,努力写一个能让大多数前端小伙伴都能看懂的Promise实现,只须要你有Promise的使用经验便可。javascript

这三篇文章,会和你从零起步,一点一点完成一个彻底符合Promise A+规范的Promise,而且完美经过官方提供的872个测试用例。我会把写一个Promise所须要的所有知识和注意点掰开揉碎,所有讲清楚。 接下来,咱们就开始吧!前端

2、经过Promise的使用来理解它的大概形式

关于Promise的实现,咱们先无论规范怎样,先看一下它是怎么用的。咱们以chrome里原生支持的Promise为例。vue

let promise1 = new Promise(function(resolve, reject) {
  setTimeout(() => { // 模拟一个异步执行
    resolve(1)
  }, 1000)
})

promise1.then(function(res) {
  console.log(res)
}, function(err){
  console.log(err)
})
复制代码

以上是咱们使用Promise时常常写的代码,从这些代码来看,咱们能够获得如下信息:java

  • Promise是一个能够new的构造函数
  • 它在构造实例,也就是new的时候接收一个函数做为参数,咱们先把这个函数叫作executor
  • Promise有一个名为then的实例方法

从这些条件中,咱们能够对咱们本身的MyPromise做出如下的实现:chrome

function MyPromise(executor) {
  
} 
MyPromise.prototype.then = function() {
  
}
复制代码

接下来,咱们仔细研究一下构造Promise时传递的参数,也就是我上面称之为executor的函数和它的两个参数设计模式

3、实例化时传递的参数executor

从上面使用Promise的常规代码中,咱们能够知道,executor是一个函数,那接下来要明确一件事:executor的具体代码是由使用者写的,并由Promise内部被调用。大概就是这样:数组

function MyPromise(executor) {
  executor()
}
MyPromise.prototype.then = function() {
  
}

// 使用MyPromise
// executor是由使用MyPromise的人来写的
function executor(resolve, reject) {
  setTimeout(() => {
    resolve(1)
  }, 1000)
}
let promise1 = new MyPromise(executor)
复制代码

根据使用经验,使用者在写executor时候,会有两个形参resolve和reject,同时,会在适当的时候调用resolve和reject,因此,resolve和reject都是函数,并且都是在promise内部实现。 因此,咱们要实现的MyPromise应该包含resolve和reject方法的实现,并在调用时做为实参传递给executorpromise

function MyPromise(executor) {
  let resolve = function() {} // resolve和reject名字能够随便起
  let reject = function() {}
  executor(resolve, reject) // 只要调用的时候传递
}
MyPromise.prototype.then = function() {
  
}

// 使用Promise
let promise1 = new MyPromise(function(resolve, reject) {
  setTimeout(() => {
    resolve(1) // resolve或者reject是由使用者调用
  }, 1000)
})
复制代码

其实,在MyPromise内部实现resolve和reject函数的时候不必定叫resolve或者reject,叫a、b甚至阿猫阿狗也行,只要在executor执行的时候传递给它就行。由于只有这样,使用者在写executor具体内容的时候,能够经过executor的形参拿到它并使用。浏览器

因此,resolve和reject函数由咱们,也就是实现这个MyPromise的人实现,而由使用这个MyPromise的人调用的。 厘清这一点很重要。app

如今,咱们实现Promise的代码以下:

function MyPromise(executor) {
  let resolve = function() {}
  let reject = function() {}
  executor(resolve, reject)
}
MyPromise.prototype.then = function() {
  
}
复制代码

接下来,是本节的重点,明确MyPromise里resolve和reject函数的功能和实现

4、实现resolve和reject函数的两项功能

如今的MyPromise只有一个架子,到这里必须完成resolve和reject两个函数。那么,resovle和reject到底是干啥的呢?这里,必需要提一些Promise A+规范的内容了。

根据规范,一个Promise的实例可能有三种状态:

  • pending 未决
  • fulfilled 成功状态
  • rejected 拒绝状态,也能够理解成失败状态

之因此会有这三种状态,是由于咱们一般用Promise来处理异步操做,而异步操做的结果根据状况可能成功可能失败。

一个Promise在实例化的时候默认是pending状态,那么它的状态由谁来改变?答案是由resolve或者reject这两个函数来改变。当resolve或者reject函数调用时,resolve会把Promise实例由pending状态更改成fulfilled成功状态,reject函数会把pending状态更改成rejected状态。到这里,resolve和reject这两兄弟的第一个功能就清楚了。

可是,要实现这个功能,就须要在咱们的MyPromise里先定义一个状态,而后在resolve和reject里更改

function MyPromise(executor) {
  this.status = 'pending' // 默认是pending状态哦
  let resolve = function() {
    // resolve方法会把pending状态改成fulfilled
    this.status = 'fulfilled'
  }
  let reject = function() {
    // reject方法会把状态改成rejected
    this.status = 'rejected'
  }
  executor(resolve, reject)
}
MyPromise.prototype.then = function() {
  
}
复制代码

可是,上面的代码是有问题的,一个是this的指向问题,咱们在MyPromise构造函数里声明的resolve和reject函数,它的内部this默认都是window,而不是MyPromise实例。这个问题有不少解决,能够将this先存一下,也能够直接使用箭头函数,这里咱们就使用箭头函数来解决。

function MyPromise(executor) {
  this.status = 'pending' 
  let resolve = () => {  // this指向和外面保持一致哦
    this.status = 'fulfilled'
  }
  let reject = () => {  // this指向和外面保持一致哦
    this.status = 'rejected'
  }
  executor(resolve, reject)
}
MyPromise.prototype.then = function() {
  
}
复制代码

上面的代码还有一个问题,根据规范,若是一个Promise实例状态改变,就会被固定住,之后它的状态就不再会更改了。 也就是说,若是一个Promise实例由pending状态变成fulfilled状态,就不能再变回pending或者rejected了。可是咱们这个这个不行,你能够把下面的代码粘到浏览器里运行,就会发现这个问题。

function MyPromise(executor) {
  this.status = 'pending' // 默认是pending状态哦
  let resolve = () => {
    this.status = 'fulfilled'
  }
  let reject = () => {
    this.status = 'rejected'
  }
  executor(resolve, reject)
}
MyPromise.prototype.then = function() {
  
}

let promise1 = new MyPromise(function(resolve, reject) {
  setTimeout(() => { // 不要忘了它哦,由于只有在异步下,才能打印promise1实例
    resolve(1)
    console.log(promise1) // 这里是{status: 'fulfilled'} 成功状态
    reject(1)
    console.log(promise1) // 可是到了这里又变成失败状态了
  }, 1000)
})
复制代码

要解决这个问题,也很简单,加上一个if条件判断就能够了,当resolve函数运行时,先看下this.status是否是pending状态,若是是,就更改它,若是不是就啥都不作。reject也是如此,这样,当promise的staus状态变化后,再调用resolve或者reject也会被忽略掉了。

function MyPromise(executor) {
  this.status = 'pending'
  let resolve = () => {
    // 判断是不是pending状态,若是是就改,不是就啥都不干,这样起到状态固定做用
    if (this.status === 'pending') { 
      this.status = 'fulfilled'
    }
  }
  let reject = () => {
    // 这里也要加if判断
    if (this.status === 'pending') {
      this.status = 'rejected' 
    }
  }
  executor(resolve, reject)
}
MyPromise.prototype.then = function() {
  
}

let promise1 = new MyPromise(function(resolve, reject) {
  setTimeout(() => { // 不要忘了它哦,由于只有在异步下,才能打印promise1的实例
    resolve(1)
    console.log(promise1) // 这里是{status: 'fulfilled'} 成功状态
    reject(1) // 虽然reject了,可是被忽略掉了
    console.log(promise1) // 到这里依然是成功状态
  }, 1000)
})
复制代码

这样,resolve、reject这哥俩的第一个功能完成了。

接下来,咱们实现resolve和reject的第二个功能。

有Promise使用经验的小伙伴确定早就知道:咱们在调用resolve或者reject方法时通常会给它传值,而这个值和then方法的实现息息相关。咱们先看一下chrome使用Promise的例子:

let promise1 = new Promise(function(resolve, reject) {
  setTimeout(() => { // 模拟一个异步执行
    let flag = Math.random() > 0.5 ? true: false
    if (flag) {
      resolve('success') // 传递一个值
    } else {
      reject('fail') // 传递一个值
    }
  }, 1000)
})

promise1.then(function(res) { // 调用resolve传过来的值会被这个函数拿到
  console.log(res)
}, function(err) { // 调用reject传过来的值会被这里拿到
  console.log(err)
}) 
复制代码

从这个例子里,咱们能够发现,resolve和reject调用时传递过来的值,会被then方法执行时传递的两个函数分别做为参数拿到。 这里咱们知道,resolve和reject执行时传过来的值必定被存储起来了,当then方法执行时传递的两个函数在某个时机拿到了它们并执行。

因此,resolve和reject函数的第二个功能也呼之欲出:将调用时的值存储起来,后面then方法里传递的两个函数会使用它们。

由于它们分别是成功时和失败时调用的,因此咱们须要分开存放。为此,MyPromise须要在构造函数里加两个属性,并在resolve和reject函数执行时赋值。

MyPromise写成以下:

function MyPromise(executor) {
  this.status = 'pending'
  this.data = undefined // 用来存入resolve传递过来的值
  this.reason = undefined // 用来存储reject传递过来的值
  
  // 添加参数,由于使用者调用时通常会给传参
  let resolve = (value) => { 
    if (this.status === 'pending') { 
      this.status = 'fulfilled'
      this.data = value
    }
  }
  
  // 添加参数,reject表示失败,因此写作失败缘由reason
  let reject = (reason) => { 
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.reason = reason
    }
  }
  executor(resolve, reject)
}
MyPromise.prototype.then = function() {
  
}
复制代码

其实,resolve和reject传递过来的值放在一个属性里也是能够的,由于promise实例状态一旦更改就不会再变了,也就是resolve和reject只可能执行其中一个,后面即便再执行也会被里面的if条件判断忽略掉。不过为了好对应,咱们仍是使用两个属性来分别存放resolve和reject函数传过来的值。

写到这里,resolve和reject分别实现了两个功能。实际上它们兄弟俩每一个人都有三个功能,只是第三个功能和then方法密切相关,因此第三个功能须要和then一块儿写。不过在此以前,咱们要先聊聊promise处理异步代码的执行顺序。

5、Promise在处理异步时的执行顺序

咱们经过console.log()的方式来看chrome里原生支持的Promise处理异步代码时的执行顺序。请仔细看下面的例子:

let promise1 = new Promise(function(resolve, reject) {
  console.log(1)
  setTimeout(() => { // 模拟一个异步执行
    let flag = Math.random() > 0.5 ? true: false
    if (flag) {
      resolve('success')
      console.log(2) // 注意这里和reject都是打印2
    } else {
      reject('fail')
      console.log(2)
    }
  }, 1000)
})

console.log(3)

promise1.then(function(res) {
  console.log(res)
}, function(err) {
  console.log(err)
})

console.log(4)
复制代码

若是你把上面的代码贴到浏览器里执行的话,你会发现打印结果是1 3 4 2 success或者fail,咱们缕一下这个顺序:

  • new Promise的时候,开始构造实例,传递给构造函数的函数执行,因此先打印出1
  • 而后setTimeout了,里面的代码须要等到下一个执行序列,而后构造结束,构造出来的实例赋值给变量promise1
  • console.log(3)执行,打印出3
  • promise1.then()执行,可是,then方法里面传递的两个函数都没有执行,否则这里就会打印出success或者fail,没有打印说明then方法传递的两个函数都没执行
  • console.log(4)执行,打印出4。当前序列结束
  • 下一个执行序列开始,以前构造promise1时setTimeout里的代码开始执行
  • 根据条件resolve或者reject执行,而后console.log(2)执行,打印2
  • 最后,then()方法里传递的两个函数根据条件执行,拿到以前resolve或者reject传递并存储的值,而且执行,打印success或者fail

总结一下:当Promise用resolve和reject方法处理异步的代码的时候,then方法先于resolve或者reject执行,可是then方法传递的两个函数此时并未执行,而是等到resolve或者reject执行以后再执行。这实际上是一种设计模式:分发订阅模式,也叫观察者模式。

这个总结若是看不明白不要紧,由于接下来就要说它。

6、分发-订阅模式和then

分发-订阅模式,也叫观察者模式,它在前端应用是如此的普遍,你几乎在因此的事件机制和异步处理中均可以见到它的身影。咱们先举个例子:

let app = document.getElementById('app')

app.addEventListener('click', function fn1() {
  console.log(1)
})
app.addEventListener('click', function fn2() {
  console.log(2)
})
app.addEventListener('click', function fn3() {
  console.log(3)
})
复制代码

以上代码对于前端的同窗再日常不过,当点击id为app的标签时,fn一、fn2和fn3才会执行。而代码执行到app.addEventListener时,相应的函数并未执行,而是等到点击的时候才执行。因此,你能够猜到,fn一、fn2和fn3一开始必定会被存放在某个地方,当某种条件发生时,它们才会被一次性执行。

若是你使用vue的话,vue里的watch也是同样的道理:先把某个函数或者某些函数注册存放到一个地方,当某个状态发生改变时,就把这些存放起来的函数一次性所有执行掉。

Promise里的then也是这样作的。当Promise处理异步时,then方法先执行,把做为参数的两个函数分别注册存放在实例中。等到resolve或者reject函数调用的时候再把它们执行掉。

此时,then方法和resolve和reject的第三项功能也呼之欲出了。

  • 首先,在构造函数里定义两个数组resolvedCallbacks和rejectedCallbacks,用来存放then方法传递进来的两个函数
  • then方法接收两个参数,一个是成功的回调,一个是失败的回调,分别命名onResolved和onRejected
  • then执行时,将onResolved函数push到定义好的resolvedCallbacks里,onRejected函数push到定义好的rejectedCallbacks里
  • 当resolve执行时,除了以前的功能,还须要把resolvedCallbacks里存放的函数所有执行掉,在执行时把this.data的值传给它们;reject也是如此
function MyPromise(executor) {
  this.status = 'pending'
  this.data = undefined
  this.reason = undefined
  this.resolvedCallbacks = [] // 存储then方法传递进来的第一个参数,成功的回调
  this.rejectedCallbacks = [] // 存储then方法传递进来的第二个参数,失败的回调
  
  let resolve = (value) => {
    if (this.status === 'pending') { 
      this.status = 'fulfilled'
      this.data = value
      // 将成功的回调所有执行,而且将this.data传递过去
      this.resolvedCallbacks.forEach(fn => fn(this.data))
    }
  }
  let reject = (reason) => {
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.reason = reason
      // 将失败的回调所有执行,而且将this.reason传递过去
      this.rejectedCallbacks.forEach(fn => fn(this.reason))
    }
  }
  executor(resolve, reject)
}
// then方法接收到参数,分别命名onResolved和onRejected
MyPromise.prototype.then = function(onResolved, onRejected) { 
  this.resolvedCallbacks.push(onResolved) // 将onResolved存起来
  this.rejectedCallbacks.push(onRejected) // 将onRejected存起来
}
复制代码

请注意,以上的代码都是基于处理异步代码,也就是then方法会早于resolve或者reject执行。 因此then里还须要作一步判断,即当前promise为pending状态时,再把回调push存放到相应的地方。

function MyPromise(executor) {
  this.status = 'pending'
  this.data = undefined
  this.reason = undefined
  this.resolvedCallbacks = []
  this.rejectedCallbacks = []
  
  let resolve = (value) => {
    if (this.status === 'pending') { 
      this.status = 'fulfilled'
      this.data = value
      this.resolvedCallbacks.forEach(fn => fn(this.data))
    }
  }
  let reject = (reason) => {
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.reason = reason
      this.rejectedCallbacks.forEach(fn => fn(this.reason))
    }
  }
  executor(resolve, reject)
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  // 判断状态,只有当pending时才执行
  if (this.status === 'pending') {
    this.resolvedCallbacks.push(onResolved)
    this.rejectedCallbacks.push(onRejected)
  }
}
复制代码

看到这一步,你可能会有点疑问,为啥用来存放then方法传递的函数要用数组?由于Promise能够像下面这样用哦

let promise = new Promise(function(resolve, reject){
  setTimeout(() => {
    resolve(1)
  }, 1000)
})

promise.then(function(res) {
  console.log('处理res')
})

promise.then(function(res) {
  console.log('再来一次')
})
复制代码

这个例子在同一个Promise实例上then了两次,注册了两次函数。当resolve执行的时候,会把then注册的两个函数都执行掉。

还有,你可能问,如今咱们的Promise都是处理异步的状况,若是是同步的状况怎么办呢? 嗯,这个就是接下来要说的。

7、处理同步的状况

咱们一般使用Promise是用来处理异步的状况,咱们的MyPromise写到如今也都是基于处理异步这个前提。实际上,Promise也是能够处理同步情况的,并且很是简单。

若是你还记得前面有关Promise执行序列讲解的话,应该还记得,异步时then方法是先于resolve或者reject执行的,而同步时then方法是在resolve或者reject以后执行的。 请看下面的例子:

let promise = new Promise(function(resolve, reject){
  resolve('success')
})

promise.then(function(res) {
  console.log(res)
})
复制代码

上面的例子中,在构造时resolve就已经调用,状态就已经肯定,此时then晚执行,因此then此时只须要根据已经肯定的状态直接调用成功或者失败的回调就完事了,没必要再注册存放了。

MyPromise进行以下更改:

function MyPromise(executor) {
  this.status = 'pending'
  this.data = undefined
  this.reason = undefined
  this.resolvedCallbacks = []
  this.rejectedCallbacks = []
  
  let resolve = (value) => {
    if (this.status === 'pending') { 
      this.status = 'fulfilled'
      this.data = value
      this.resolvedCallbacks.forEach(fn => fn(this.data))
    }
  }
  let reject = (reason) => {
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.reason = reason
      this.rejectedCallbacks.forEach(fn => fn(this.reason))
    }
  }
  executor(resolve, reject)
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  if (this.status === 'pending') {
    this.resolvedCallbacks.push(onResolved)
    this.rejectedCallbacks.push(onRejected)
  }
  // 若是是成功状态直接成功的回调函数
  if (this.status === 'fulfilled') {
    onResolved(this.data)
  }
  // 若是是失败状态直接调失败的回调函数
  if (this.status === 'rejected') {
    onRejected(this.reason)
  }
}
复制代码

咱们能够测试一下哦~

let promise = new MyPromise(function(resolve, reject) {
  setTimeout(() => {
    let flag = Math.random() > 0.5 ? true : false
    if (flag) {
      resolve('success')
    } else {
      reject('fail')
    }
  }, 1000)
})
promise.then(res => {
  console.log(res)
}, error => {
  console.log(error)
})
复制代码

到这里,MyPromise的雏形完成了!嗯,只是一个雏形,它最核心的then方法咱们几乎还没怎么实现。可是,若是你能彻底看懂这四十几行的代码,那表示你已经离成功不远了!

接下来的一篇,咱们须要完成最最核心的then方法的实现了!进入下一篇包教包会,和你实现一个Promise(二)

相关文章
相关标签/搜索