ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

JS 异步 ( 二、Promise 的用法、手写模拟 Promise )

2021-09-07 20:32:18  阅读:38  来源: 互联网

标签:异步 resolve console log JS Promise reject 处理程序



文章目录

一、Promise 基础


Promise 作用


1. 回调地狱
想知道 Promise 的作用, 需要先了解一个概念叫回调地狱。回调地狱是指,回调函数间多层嵌套调用后,
引发的可读性问题。

这样解释可能太抽象,下面举例演示一下。
需求:输出 a b c d e 五个字母,每个字母输出间隔为 500ms

(1) 使用内置函数引发的回调地狱

<!doctype html>
<script>
  // 回调地狱
  setTimeout(function () {
    console.log('a')
    setTimeout(function () {
      console.log('b')
      setTimeout(function () {
        console.log('c')
        setTimeout(function () {
          console.log('d')
          setTimeout(function () {
            console.log('e')
            console.log('输出结束')
          }, 500)
        }, 500)
      }, 500)
    }, 500)
  }, 500)
</script>

(2) 使用自定义函数引发的回调地狱

<script>
  // 定义函数,参数是回调函数
  let fn = function (word, callback) {
    setTimeout(function () {
      console.log(word)
      callback()
    }, 500)
  }
  // 回调地狱
  fn('a', function () {
    fn('b', function () {
      fn('c', function () {
        fn('d', function () {
          fn('e', function () {
            console.log('输出结束')
          })
        })
      })
    })
  })
</script>

2. Promise

通过上面的代码可以了解到,发生回调地狱后,代码的可读性和可维护性有多么差,Promise 用
链式调用的方式很好的解决了回调地狱的问题,从而增强了代码的可读性。

上面输出 a b c d e 的需求,用 Promise 来实现后,明显清爽了许多:

<script>
  // 定义函数
  let promise = function (word) {
    return new Promise((resolve => {
      setTimeout(function () {
        console.log(word)
        resolve()
      }, 500)
    }))
  }

  // 链式调用
  promise('a').then(() => {
    return promise('b')
  }).then(() => {
    return promise('c')
  }).then(() => {
    return promise('d')
  }).then(() => {
    return promise('e')
  }).then(() => {
    console.log('输出结束')
  })
</script>

Promise 语法


常见的 Promise 语法结构如下:

<script>
  // 用一个随机数大于 5 就成功,否则就失败的例子, 来
  // 展示一下较完整且常见的 Promise 语法结构
  new Promise((resolve, reject) => {
    const random = Math.ceil(Math.random() * 10)
    if(random > 5){
      resolve(random)
    }else{
      reject(random)
    }
  }).then((data) => {
    console.log('随机数为:' + data + ',大于5,执行成功!')
  }).catch((data) => {
    console.log('随机数为:' + data + ',小于等于5,执行失败!')
  }).finally(() => {
    console.log('执行结束!')
  })
</script>

语法结构拆解:

1. 构造函数

Promise 的构造器需要一个回调函数作为参数(该参数后面简称 executor),executor 被调用时会被传入两个函数
类型的参数,一般习惯称为 resolve 和 reject,当 executor 中调用这两个函数时,会异步执行收集回来的处理程序

<script>
  // executor 执行时不需要使用 resolve 和 reject 函数
  new Promise(function () {

  })
  // executor 执行时只需要使用 resolve 函数
  new Promise(function (resolve) {
    resolve()
  })
  // executor 执行时需要使用 resolve 和 reject 函数
  new Promise(function (resolve, reject) {
    resolve()
    reject()
  })
</script>

2. then 方法

then 方法用来收集处理程序,其参数为两个可选的回调函数 (处理程序),当 executor 中调用了 resolve 时会执行第
一个处理程序,当 executor 中出现异常 (仅限同步代码块异常) 或调用了 reject 时会执行第二个处理程序。

<script>
  // 声明一个 Promise 对象
  let promise = new Promise(function (resolve, reject) {
    resolve('调用处理程序1')
    reject('调用处理程序2')
  })
  promise.then(() => {}) // 传入 resolve 处理程序的实现 - 不需要参数
  promise.then(data => {}) // 传入 resolve 处理程序的实现 - 需要参数
  promise.then(undefined, () => {}) // 不需要 resolve 处理程序的实现,需要传入 reject 处理程序的实现 - 不需要参数
  promise.then(data => {}, message => {}) // 传入 resolve 和 reject 处理程序的实现 - 需要参数
</script>

3. catch

catch 方法与 then 方法第二个参数的作用及原理是一样的,一般习惯将 resolve 处理程序写在 then 方法的第一
个参数中,把 reject 处理程序写在 catch方法中,这样代码可读性更高,结构更清晰

<script>
  new Promise((resolve, reject) => {
    reject()
  }).then(() => console.log('resolve 处理程序执行'))
    .catch(() => console.log('reject 处理程序执行'))
</script>

4. finally

finally 方法的参数也是回调函数,并且该回调函数没有参数,当 executor 中调用了 resolve、 reject 或同步执行的代
码发生异常时都会执行该回调。

<script>
  let promise = new Promise((resolve, reject) => {
    resolve('数据')
    reject('错误信息')
  })
  promise.then((data) => console.log("resolve 执行: " + data), (message) => console.log("reject 执行: " + message))
  promise.finally(() => {
    console.log('finally 执行')
  })
</script>

上面代码等同于:

<script>
  let promise = new Promise((resolve, reject) => {
    resolve('数据')
    reject('错误信息')
  })
  promise.then((data) => {
    console.log("resolve 执行: " + data)
    console.log('finally 执行')
  }, (message) => {
    console.log("reject 执行: " + message)
    console.log('finally 执行')
    throw message
  })
</script>

Promise 内部状态值 和 链式调用

请添加图片描述
Promise 拥有三种状态:
1. pending:对象刚创建时的状态
2. fulfilled:executor 中调用 resolve 后的状态。
3. rejected:executor 中出现异常或者调用 reject 后的状态

如上图所示,状态只能由 pending ➡ fulfilled 或 pending ➡ rejected,这也就表示 executor 中 resolve 和 reject
只能同时执行一个,不能 fulfilled ⬅➡ rejected 这样变化


链式调用:

先认识一下链式调用的代码,
注意下面这种不属于链式调用,这只是给同一个 Promise 对象添加多个 resolve 处理程序而已:

<script>
  let promise = new Promise(((resolve) => {
    resolve()
  }))
  promise.then(() => {
    console.log('执行成功,第一次');
  })
  promise.then(() => {
    console.log('执行成功,第二次');
  })
  promise.then(() => {
    console.log('执行成功,第三次');
  })
</script>

这才是真正意义上的链式调用:

<script>
  new Promise(((resolve) => {
    resolve()
  })).then(() => {
    console.log('执行成功,第一次');
  }).then(() => {
    console.log('执行成功,第二次');
  }).then(() => {
    console.log('执行成功,第三次');
  })
</script>

之所以能够实现链式调用,是因为 then 方法的返回值永远是一个 Promise 对象,该 Promise 的内部状态,与被执
行的处理程序 ( resolve 处理程序或 reject 处理程序 ) 的返回值有关。

情况1:处理程序显式返回 Promise 对象时,then 方法返回的 Promise 对象的内部状态应与其一致:

<script>
  // resolve 处理程序显式返回状态为 fulfilled 的 Promise 对象, 所以 then 方法最终返回的 
  // Promise 对象的内部状态也是 fulfilled, 然后程序会链式的执行下个 then 方法的 resovle 处理程序
  Promise.resolve().then(() => {
    return Promise.resolve('可选参数')
  }).then((data) => console.log(data))

  // resolve 处理程序显式返回状态为 rejected 的 Promise 对象, 所以 then 方法最终返回的 
  // Promise 对象的内部状态也是 rejected, 然后程序会链式的执行下个 then 方法的 reject 处理程序
  Promise.resolve().then(() => {
    return Promise.reject('可选参数')
  }).catch((data) => console.log(data))
</script>

情况2:处理程序未显式返回 Promise 对象时,then 方法会返回一个状态为 fulfilled 的 Promise 对象:

<script>
  // 未显式返回任何数据
  Promise.resolve().then(() => {
    console.log('未显式返回任何数据')
  }).then(() => console.log('处理程序未显式返回 Promise 对象时,then 方法会返回一个状态为 fulfilled 的 Promise 对象'))

  // 显式返回非 Promise 对象时, 会把返回的数据当作参数传递给下一个处理程序
  Promise.resolve().then(() => {
    return '这个字符串会当作参数传给下一个处理程序'
  }).then((data) => console.log(data))
</script>

Promise 是否为异步执行

executor 中的代码是同步执行的,只有调用 resolve 和 reject 时,它们对应的处理程序才会异步执行,但是并不影
响 executor 中其他代码的同步执行

<script>
  console.log(1)
  let promise = new Promise(resolve => {
    console.log(2)
    resolve(3) // 此处 resolve 的处理程序会等待主线程的内容执行完后,异步执行
    console.log(4)
  })
  console.log(5)
  promise.then(data => {
    console.log(data)
  })
  console.log(6)
  
  // 输出:
  // 1、2、4、5、6、3
</script>

Promise 常用函数或属性


1. Promise.all

判断任务是否全部为 fulfilled 状态,当全部为 fulfilled 时,会返回一个状态为 fulfilled 的 Promise 新对象, 并会收集
所有任务的 resolve 处理程序的参数,将其封装成一个数组,传递给新的 Promise 的 resolve 处理程序,当有任意一个任务的状
态为 rejected 时,会立即中断并返回一个状态为 rejected 的 Promise 新对象,并把失败任务的 reject 处理程序的参数,传递给
新的 Promise 的 reject 处理程序。

任务全部为 fulfilled 状态:

<script>
  // 状态为 fulfilled 的 Promise 对象
  let promise1 = new Promise(resolve => {
    resolve('第一个 Promise');
  })
  // 状态为 fulfilled 的 Promise 对象
  let promise2 = new Promise(resolve => {
    resolve('第二个 Promise');
  })
  // 任务全部为 fulfilled 状态,会执行 Promise.all 返回的新 Promise 对象的 resolve 处理程序
  Promise.all([promise1, promise2]).then(data => {
    console.log(data)
  })
</script>

有任务为 rejected 状态:

<script>
  // 状态为 fulfilled 的 Promise 对象
  let promise1 = new Promise(resolve => {
    resolve('第一个 Promise');
  })
  // 状态为 rejected 的 Promise 对象
  let promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('第二个 Promise');
    })
  })
  // 状态为 rejected 的 Promise 对象
  let promise3 = new Promise((resolve, reject) => {
    reject('第三个 Promise');
  })
  // 有任务为 rejected 状态,会执行 Promise.all 返回的新 Promise 对象的 rejected 处理程序
  // promise2 被 setTimeout 挂起,所以 promise3 先执行,因为 promise3 为 rejected 状态,
  // 所以 Promise.all 会直接返回,而 promise2的 reject 处理程序的参数不会被记录
  Promise.all([promise1, promise2, promise3]).then(data => {
    console.log(data)
  }).catch(error => {
    console.log(error)
  })
</script>

2. Promise.race

以第一个执行的任务结果来决定最终返回的结果,第一个任务的结果状态为 fulfilled 时,Promise.race 会返回一个
状态为 fulfilled 的新 Promise 对象,并把第一个任务的 resolve 处理程序的参数,传递给新 Promise 对象的 resolve 处理程序,第
一个任务的结果状态为 rejected 时,Promise.race 会返回一个状态为 rejected 的新 Promise 对象,并把第一个任务的 rejected
处理程序的参数,传递给新 Promise 对象的 rejected 处理程序

第一个执行的任务的结果状态为 fulfilled:

<script>
  // 状态为 fulfilled 的 Promise 对象
  let promise1 = new Promise(resolve => {
    resolve('成功!');
  })
  // 状态为 rejected 的 Promise 对象
  let promise2 = new Promise((resolve, reject) => {
    reject('失败!');
  })
  // promise1 会先执行,其状态是 fulfilled,所以返回的新 Promise 对象的状态也是 fulfilled
  Promise.race([promise1, promise2]).then(data => {
    console.log(data)
  }).catch(error => {
    console.log(error)
  })
</script>

第一个执行的任务的结果状态为 rejected:

<script>
  // 状态为 fulfilled 的 Promise 对象
  let promise1 = new Promise(resolve => {
    setTimeout(()=>{
      resolve('成功!');
    })
  })
  // 状态为 rejected 的 Promise 对象
  let promise2 = new Promise((resolve, reject) => {
    reject('失败!');
  })
  // promise1 因为被 setTimeout 挂起了,所以 promise2 会先执行,其状态是 rejected,
  // 所以返回的新 Promise 对象的状态也是 rejected
  Promise.race([promise1, promise2]).then(data => {
    console.log(data)
  }).catch(error => {
    console.log(error)
  })
</script>

二、模拟 Promise,加深理解


then 方法的实现,真的是超级超级难理解,只能尽量记录当时的理解思路,希望以后复习时,能马上想回忆起来

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MyPromise</title>
</head>
<body>
<script>
  // Promise 的三种状态
  const PENDING = 'pending'
  const FULFILLED = 'fulfilled'
  const REJECTED = 'rejected'

  class MyPromise {

    // 构造器的参数 executor 为回调函数
    constructor(executor) {
      this._status = PENDING     // 创建 Promise 对象后的初始状态
      this._value = undefined    // executor 中传给 resolve/reject 函数的参数,或 then 方法显式 return 的值
      this._resolveQueue = []    // then 方法收集的 resolve 处理程序队列
      this._rejectQueue = []     // then 方法收集的 reject 处理程序队列 

      // 定义函数变量,当作 executor 的第一个参数
      let _resolve = (val) => {
      
        // 函数体封装成闭包,供下面 setTimeout 调用,使其变成异步执行,给 then 方法让步,让其先行收集 resolve/reject 处理程序
        // 此处模拟了原版 Promise 调用 resolve 后会变成异步执行的特性
        const run = () => {
          if (this._status !== PENDING) return   // Promise 规定状态只能由 pending 到 fulfilled 或 rejected
          this._status = FULFILLED               // 将状态变更为 fulfilled
          this._value = val                      // 存储 executor 中传给 resolve 函数的参数

          // 这里之所以使用一个数组来存储, 是为了实现,可以给同一个 Promise 对象添加多个 resolve 处理程序的功能
          while (this._resolveQueue.length) {
            const callback = this._resolveQueue.shift() // 从 then 方法收集的 resolve 处理程序队列中取出回调函数
            callback(val) // 执行取出的回调函数,并把之前保存的参数传进去
          }
        }
        // executor 中调用 resolve 后,resolve 异步执行,给 then 方法让步
        setTimeout(run)
      }

      // 定义函数变量,当作 executor 的第二个参数
      let _reject = (val) => {
      
        // 函数体封装成闭包,供下面 setTimeout 调用,使其变成异步执行,给 then 方法让步,让其先行收集 resolve/reject 处理程序
        // 此处模拟了原版 Promise 调用 reject 后会变成异步执行的特性
        const run = () => {
          if (this._status !== PENDING) return   // 对应规范中的 "状态只能由 pending 到 fulfilled 或 rejected "
          this._status = REJECTED                // 将状态变更为 rejected
          this._value = val                      // 存储 executor 中传给 reject 函数的参数

          // 这里之所以使用一个数组来存储, 是为了实现,可以给同一个 Promise 对象添加多个 resolve 处理程序的功能
          while (this._rejectQueue.length) {
            const callback = this._rejectQueue.shift()  // 从 then 方法收集的 reject 处理程序队列中取出回调函数
            callback(val)  // 执行取出的回调函数,并把之前保存的参数传进去
          }
        }
        // executor 中调用 reject 后,reject 异步执行,给 then 方法让步
        setTimeout(run)
      }
      
      // 构造中直接执行 executor 回调函数,并传入前面定义的两个函数变量
      executor(_resolve, _reject)
    }

    // then 方法
    // 将 resolveFn/rejectFn  函数进一步封装后,收集到 resolve/reject 处理程序队列中
    then(resolveFn, rejectFn) {

      // 参数不是 function 类型时,把参数重定义为一个函数,函数会返回上一个 Promise 的 resolve 参数
      // 或 上一个 then 的返回值
      typeof resolveFn !== 'function' ? resolveFn = value => value : null
      typeof rejectFn !== 'function' ? rejectFn = error => error : null

      // 为了满足链式调用, then 方法必须返回 Promise 对象,这样语法上才能继续的写 .then()
      return new MyPromise((resolve, reject) => {

        // 定义函数变量,目的就是包装一下 resolveFn 函数,其内部会调用 resolveFn 函数
        const fulfilledFn = value => {
          try {
            // 执行 resolveFn 函数,并获得返回值
            let x = resolveFn(value)
            
            // 1. resolveFn 方法的返回值是 Promise 类型时,按照规定 then 方法需要返回一个和 resolveFn 返回值状态相同的        
            // Promise 对象, 目的是为了链式调用时,可以正确的调用后续的 resolve/reject 处理程序,因为 resolveFn 返回的 
            // Promise 的状态可能是 fulfilled,也可能是 rejected,而当前代码被包含在新创建的 MyPromise 内部,其默认状态
            // 是 pending,不满足返回规定,无法正确调用后续链式的 resolve/reject 处理程序, 所以这个地方需要调用
            // x.then(resolve, reject), 其目的是,如果后续为链式调用 .then(resolveFn, rejectFn) 的时候,那么会根据
            // resolveFn 返回的 Promise 对象的状态,决定调用 resolveFn 还是 rejectFn,从而实现链式调用的效果

            // 2. resolveFn 方法的返回值不是 Promise 类型时, 按照规定需要返回一个状态为 fulfilled 的 Promise
            // 因为当前代码被包含在新创建的 MyPromise 内部,而新创建的 MyPromise 默认状态是 pending,所以此处要调用
            // 该 MyPromise 的 resolve 方法,原因有两个,一个是调用 resolve 方法后状态会从 pending 变成 fulfilled,
            // 另一个是如果后续为链式调用 .then(resolveFn, rejectFn) 的时候,那么调用 resolve 后就会执行后续 then 方
            // 法的 resolveFn, 从而实现链式调用效果
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
          } catch (error) {
            reject(error)
          }
        }

        // 原理同上
        const rejectedFn = error => {
          try {
            let x = rejectFn(error)
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
          } catch (error) {
            reject(error)
          }
        }
        
        // 将处理程序加入 resolve/reject 处理程序队列
        // 此处让我简化了, 只留下了主线逻辑
        this._resolveQueue.push(fulfilledFn)
        this._rejectQueue.push(rejectedFn)
      })
    }
  }
</script>
</body>
</html>

模拟 Promise 的代码部分,参考文章链接

标签:异步,resolve,console,log,JS,Promise,reject,处理程序
来源: https://blog.csdn.net/Ares5kong/article/details/119890130

专注分享技术,共同学习,共同进步。侵权联系[admin#icode9.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有