JavaScript高级程序设计记录-Promise

300 阅读9分钟

ES6-Promise-期约

1. 期约

1.1 期约规范及基础

新增的引用类型 Promise,可以通过 new 操作符来实例化。Promise 内部包含异步操作

let p = new Promise(() => {})
setTimeout(console.log, 0, p) // Promise <pending>

  • Promise 有三种状态:待定(Pending)、兑现(Fulfilled)和拒绝(Rejected) ,且 Promise 必须为三种状态之一,只有异步操作的结果可以决定当前状态,任何其他操作都无法改变状态。
  • Promise 状态只能由 Pending 转为其他两种状态,改变之后不会再发生变化。
  • Pending 变为 Fulfilled 后会得到一个私有 value,变为 Rejected 会得到一个私有 reason,后面的异步代码会接收这个值。

Promise 的执行过程:

  1. 初始化 Promise 状态(Pending)
  2. 立即执行 Promise 中传入的 fn 函数,将 Promise 内部 resolve、reject 函数作为参数传给 fn,按事件机制时机处理
  3. 执行 then(...)注册回调处理数组
  4. Promise 的关键是要保证 then 方法传入的参数 onFulfilled 和 onRejected,必须在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

真正的链式 Promise 是指在当前 promise 达到 fulfilled 状态后,即开始进行下一个 promise。

2.2 执行函数控制期约状态

Promise 的状态是私有的,状态变化只能在期约的执行器函数中完成。Promise 提供了两个函数来调用改变状态:resolve()reject()。resolve 会把状态切换为 Fulfilled,reject 会把状态切位 Rejected,且会抛出错误。

let p1 = new Promise((resolve,reject) => resolve())
setTimeout(console.log, 0, p1) // Promise <resolved>

let p2 = new Promise(reject => reject())
setTimeout(console.log, 0 ,p2) // Promise <rejected>


2.3 期约方法

then 链式操作的用法

let p = new Promise(resolve => resolve('开始了'))
p.then(data => {
  console.log(data)
  return '到你了'
}).then(data => {
  console.log(data)
})

reject 的用法

Promise 的状态置位 rejected,这样就可以在 then 中捕获,执行失败的回调。then 方法接收两个参数,第一个对应 resolve 回调,第二个对应 reject 回调。

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    let num = Math.ceil(Math.random() * 10)
    num <= 5 ? resolve(num) : reject('太大了')
  }, 0)
})

p.then(
  res => {
    console.log('resolved', res)
  },
  err => {
    console.log('rejected', err)
  }
)

catch 的用法

同 then 中第二个参数一样,指定 jeject 的回调,还可以抛出 resolve 毁掉中的异常。

p.then(data => {
  console.log(happy) // happy未定义
}).catch(err => {
  console.log('catch',err)
})

all 的用法

all 提供了并行执行异步操作的能力,接收一个数组参数,里面的值最终都算返回 Promise 对象,在所有异步操作执行完后才执行回调。只有所有方法都成功是才成功,只要有一个失败就变为 Rejected 状态。

const p1 = Promise.resolve(1)
const p2 = new Promise(resolve => {
  setTimeout(resolve,100, 'foo')
})
const p3 = 3

Promise.all([p1,p2,p3]).then(values => {
  console.log(values) // Array [1, 'foo', 3]
})

race 的用法

race 接收一个数组参数,谁先执行返回谁

function requestFast() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('成功了')
    },3000)
  })
}

function requestSlow(){
  return new Promise(reject=> {
    setTimeout(() => {
      reject('失败了')
    },2000)
  })
}

Promise.race([requestFast(),requestSlow()]).then(data => {
  console.log(data)
}).catch(err => {
  console.log(err)
})

2. 手写 Promise

1. 实现 resolve 与 reject

Promise 的初始状态是pendingresolve和reject要绑定this,保证 resolve 和 reject 的 this 指向永远指向当前的MyPromise实例,防止随着函数执行环境的改变而改变

class MyPromise {
  // 构造方法
  constructor(executor) {
    // 初始化值
    this.initValue()
    // 初始化this指向
    this.initBind()
    // 执行传进来的函数
    // 立即执行函数 将this.resolve和this.reject作为变量传递给resolve和reject
    executor(this.resolve, this.reject)
  }

  initBind() {
    // 初始化this
    this.resolve = this.resolve.bind(this)
    this.reject = this.reject.bind(this)
  }

  initValue() {
    // 初始化值
    this.PromiseResult = null
    this.PromiseState = 'pending'
  }

  resolve(value) {
    // 如果执行resolve方法且状态为pending,则改变状态为fulfilled
    if(this.PromiseState !== 'pending') return
    this.PromiseState = 'fulfilled'
    this.PromiseResult = value
  }

  reject(reason) {
    // 如果执行reject方法且状态为pending,则改变状态为rejected
    if(this.PromiseState !== 'pending') return
    this.PromiseState = 'rejected'
    this.PromiseResult = reason
  }
}

测试代码

const test1 = new MyPromise((resolve,reject) => {
  resolve('成功')
})
console.log(test1) // MyPromise {PromiseResult: '成功', PromiseState: 'fulfilled', resolve: ƒ, reject: ƒ}

const test2 = new MyPromise((resolve,reject) =>{
  reject('失败')
})
console.log(test2) // MyPromise {PromiseResult: '失败', PromiseState: 'rejected', resolve: ƒ, reject: ƒ}

注意,Promise 中有throw的时候会执行 reject,所以在执行传进来的函数时要使用try catch

+   try {
      // 执行传进来的函数
      executor(this.resolve, this.reject)
+   } catch (e) {
      //捕捉到错误执行reject
+     this.reject(e)
+   }

2. then 的实现

Promise 的 then 有以下功能:

  • then 接收两个回调,成功和失败
  • 当 Promise 状态为 fulfilled 是执行成功回调,为 rejected 时执行失败回调
  • 如 resolve 或 rejected 在定时器里,则定时器结束后再执行 then
  • then 支持链式调用,下一次 then 可以接收上一次 then 的返回值
// 类中新增then方法
then(onFulfilled, onRejected) {
  // 接收两个回调 onFulfilled onRejected

  //校验参数是否为函数
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
  onRejected= typeof onRejected=== 'function' ? onFulfilled : reason => { throw reason }

  if(this.PromiseState === 'fulfilled') {
    onFulFilled(this.PromiseResult)
  }else {
    onRejected(this.PromiseResult)
  }
}

then 的功能已经实现了,但定时器还不行,如果在定时器里执行 resolve 或 reject,then 调用的时候状态还没变更,所以会返回 null,所以我们需要判断状态为 pending 的时候将 then 里的回调保存起来,等定时器里的 resolve 或 reject 执行后再去判断状态,并且判断要执行刚才保存的哪一个回调。所以我们需要一个数组来保存着两个回调。

  initValue() {
    // 初始化值
    this.PromiseResult = null
    this.PromiseState = 'pending'
+   this.onFulfilledCallbacks = [] // 保存成功回调
+   this.onRejectedCallbacks = [] // 保存失败回调
  }

  resolve(value) {
    // 如果执行resolve方法且状态为pending,则改变状态为fulfilled
    if(this.PromiseState !== 'pending') return
    this.PromiseState = 'fulfilled'
    this.PromiseResult = value
+   // 执行保存的成功回调
    while(this.onFulfilledCallbacks.length) {
      this.onFulfilledCallbacks.shift()(this.PromiseResult)
    }
  }

  reject(reason) {
    // 如果执行reject方法且状态为pending,则改变状态为rejected
    if(this.PromiseState !== 'pending') return
    this.PromiseState = 'rejected'
    this.PromiseResult = reason
+   // 执行保存的失败回调
    while(this.onRejectedCallbacks.length) {
      this.onRejectedCallbacks.shift()(this.PromiseResult)
    }
  }

  then(onFulfilled, onRejected) {
    // 接收两个回调 onFulfilled onRejected
    //校验参数是否为函数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
    onRejected= typeof onRejected === 'function' ? onRejected : reason => { throw reason }
    if(this.PromiseState === 'fulfilled') {
      onFulFilled(this.PromiseResult)
    }else if (this.PromiseState === 'rejected') {
      onRejected(this.PromiseResult)
+   }else if(this.PromiseState === 'pending') {
+     // 如果状态为pending,则保存回调函数
+     this.onFulfilledCallbacks.push(onFulfilled)
+     this.onRejectedCallbacks.push(onRejected)
    }
  }

现在测试一下定时器的效果:

const testA = new MyPromise((resolve,reject) => {
  setTimeout(() => {resolve('成功')},1000)
}).then(res => console.log(res))

结果显示成功,接下来实现 then 链式调用的功能。

Promise 的链式调用有以下几个特点:

  • then 方法本身会返回一个 Promise 对象
  • 如果返回值是 promise 对象且返回值为成功,新的 promise 就是成功,反之是失败,新的 promise 就是失败
  • 如果返回值不是 promise 对象,新的 promise 对象就是成功,值为此返回值

所以我们需要在 then 执行后再返回一个 Promise 对象:

  then(onFulfilled, onRejected) {
    // 接收两个回调 onFulfilled, onRejected
    // 参数校验,确保一定是函数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : reason => {
            throw reason
          }

    var thenPromise = new MyPromise((resolve, reject) => {
      const resolvePromise = cb => {
        console.log(cb)
        try {
          const x = cb(this.PromiseResult)
          if (x === thenPromise) {
            // 不能返回自身哦
            reject('不能返回自身。。。')
          }
          if (x instanceof MyPromise) {
            // 如果返回值是Promise
            // 如果返回值是promise对象,返回值为成功,新promise就是成功
            // 如果返回值是promise对象,返回值为失败,新promise就是失败
            // 谁知道返回的promise是失败成功?只有then知道
            x.then(resolve, reject)
          } else {
            // 非Promise就直接成功
            resolve(x)
          }
        } catch (err) {
          // 处理报错
          reject(err)
        }
      }

      if (this.PromiseState === 'fulfilled') {
        // 如果当前为成功状态,执行第一个回调
        resolvePromise(onFulfilled)
      } else if (this.PromiseState === 'rejected') {
        // 如果当前为失败状态,执行第二个回调
        resolvePromise(onRejected)
      } else if (this.PromiseState === 'pending') {
        // 如果状态为待定状态,暂时保存两个回调
        // 如果状态为待定状态,暂时保存两个回调
        this.onFulfilledCallbacks.push(resolvePromise.bind(this, onFulfilled))
        this.onRejectedCallbacks.push(resolvePromise.bind(this, onRejected))
      }
    })

    // 返回这个包装的Promise
    return thenPromise
  }

3. async 和 await

1.async

async 关键字用于声明异步函数,可以用在函数声明、函数表达式、箭头函数和方法上。

async function () {}
let test = async function () {}
let test2 = async () => {}
class Qux {
  async qux() {}
}

使用 async 关键字可以让函数具有异步特征,但总体上仍然是同步求值的。

async function foo() {
  console.log(1)
}
foo()
console.log(2)
// 会按照顺序输出 1  2

但是,如果使用 return 关键字返回了值,就会被 Promise.resolve()包装成一个 promise 对象,在外部调用这个函数可以得到返回值。

async function foo() {
  console.log(1)
  return 3
}
// 返回的promise调用then方法
foo().then(console.log)

console.log(2)
// 1  =>  2  =>  3
// 先执行主线程任务输出1  then里是异步任务 进入微任务队列,继续执行主线程任务输出2 ,然后清空微任务队列输出3

2. await

异步函数针对不会马上完成的任务,所以需要一种暂停和恢复执行的能力。使用await 可以暂停异步函数代码的执行,等待期约解决。

let p = new Promise(resolve => setTimeout(resolve, 1000 ,3))

p.then(x => console.log(3))

// 使用async await
async function foo() {
  let p = new Promise(resolve => setTimeout(resolve, 1000 ,3))
  console.log(await p)
}
foo()

使用 await 后悔暂停执行异步函数后面的代码,染出 JavaScript 运行时的执行线程。

3. await 的限制

await 必须在异步函数中使用,不能再顶级上下文如<script>标签或模块中使用,但是可以定义并立即调用异步函数。

await 会记录在哪里暂停执行,等到 await 右边的值可用了,JavaScript 会向消息队列推送给一个任务,这个任务会恢复异步函数的执行。因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。

async function foo() {
  console.log(2)
  await null
  console.log(4)
}

console.log(1)
foo()
console.log(3)

上面代码的执行顺序是:

  1. 打印 1
  2. 调用异步函数 foo
  3. 在 foo()中打印 2
  4. 在 foo 中遇到 await 关键字暂停执行,为立即可用的值 null 向消息队列推送一个任务
  5. foo()退出
  6. 打印 3
  7. 同步线程的代码执行完毕
  8. 从消息队列中取出任务,恢复异步函数执行
  9. 在 foo()中恢复执行,await 取得 null 值(并没有使用)
  10. 继续在 foo 中打印 4
  11. foo 返回

如果在 await 后是一个期约,为了执行异步函数,则会推送两个任务到消息队列并被异步求值

async function foo() {
  console.log(2)
  console.log(await Promise.resolve(8))
  console.log(9)
}

async function bar() {
  console.log(4)
  console.log(await 6)
  console.log(7)
}

console.log(1)
foo()
console.log(3)
bar()
console.log(5)

运行顺序为:

  1. 打印 1
  2. 调用异步函数 foo
  3. 在 foo 中打印 2
  4. 在 foo 中遇到 await 暂停,想消息队列添加一个期约在落定之后执行的任务
  5. 期约立即落定,把给 await 提供值得任务添加到消息队列
  6. foo 退出
  7. 打印 3
  8. 调用异步函数 bar
  9. 在 bar 中打印 4
  10. 在 bar 中遇到 await 暂停,为立即可用的值 6 想消息队列中添加一个任务
  11. bar 退出
  12. 打印 5
  13. 顶级线程执行完毕
  14. JavaScript 运行时从消息队列中取出解决 await 期约的处理程序,并将解决的值 8 提供给他
  15. 向消息队列添加一个恢复执行 foo 的任务
  16. 从消息队列中取出恢复执行 bar 的任务及值 6
  17. bar 恢复执行,await 取得值 6
  18. bar 中打印 6
  19. bar 中打印 7
  20. bar 返回
  21. 异步任务完成,从消息队列取出恢复执行 foo 的任务及值 8
  22. foo 中打印 8
  23. foo 中打印 9
  24. foo 返回

注:以上步骤为早起规范,await 后的 promise 会生成两个任务,现在新版本浏览器只会生成一个异步任务,所以输出结果为 1 2 3 4 5 8 9 6 7。