九成开发者都误解的链式调用!Promise 执行过程的更详细解释!

177 阅读7分钟

Promise 历史

Promise 是一套专门处理异步场景的规范,它能有效的避免回调地狱的产生。这套规范最早出现于社区,规范名称为 Promise A+

该规范出现后立刻得到了很多开发者的响应,关于 Promise A+ 的规定可自行点击链接前往阅读

Promise API

ES6 提供了一套 API,实现了 Promise A+ 规范

每个 new Promise 会返回一个 Promise 对象,我们可以将该对象视为一个委托任务,该委托承诺无论任务执行成功或失败都会通知我们,具体在代码中便是:

  • 如果任务成功,那么便会调用 resolve()
  • 如果任务失败,那么便会调用 reject()

每个 Promise 对象都会初始化为 pending 挂起状态,形象点的描述就是该 Promise 对象要执行的任务尚未得到结果

Promise 得出结果,即调用了 resolve()reject() 时,该 Promise 便立即变成完成状态 fulfilled 或失败状态 rejected,然后根据结果调用 then()catch() 里的函数

// 创建一个任务对象,该任务立即进入 pending 状态
const task = new Promise((resolve, reject) => {
  // 任务的具体执行流程,该函数会立即被执行
  // 调用 resolve(data),可将任务变为 fulfilled 状态,data 为需要传递的相关数据
  // 调用 reject(reason),可将任务变为 rejected 状态,reason 为需要传递的失败原因
});

task.then(
  (data) => {
    // onFulfilled 函数,当任务完成后,会自动运行该函数,data 为任务完成的相关数据
  },
  (reason) => {
    // onRejected 函数,当任务失败后,会自动运行该函数,reason 为任务失败的相关原因
  }
)

task.catch(
  (reason) => {
    // onRejected 函数,当任务失败后,会自动运行该函数,reason 为任务失败的相关原因
  }
)

传入 new Promise 里的函数会被立刻执行,也即:该函数并不是异步的,而是同步的

then() & catch()

很多人会默认 then() 便是用来处理成功的,catch() 便是用来处理失败的,实际上并不是。

then() 实际上有两个参数,可以传入两个函数 task.then(handler1, handler2) ,第一个函数执行成功之后的要执行的代码,第二个函数执行失败之后要执行的代码

catch() 只能传入一个函数,其作用等价于 then() 只传入第二个参数

链式调用

我们都习惯在业务代码中这样使用 Promise

const task = new Promise(...)

task.then(handler1).then(handler2).catch(handler3)

在学习中,多数课程为了便于初学者理解,可能会这样解释:task 得出结果后,如果成功便会执行 then() 中的函数;如果出现失败或报错,便会立即跳到 catch() 然后执行里面的函数,或者将这一过程描述为捕获错误

这种描述对于上面的代码并不会得出错误的结果,是符合预期的,但在实际执行过程中这种说法并不严谨,实际上并不存在 “跳” 或 “捕获” 这一逻辑,只是通过另一种方式实现了类似的效果

以上面的代码为例给出解释:

跳?捕获?

到目前我们可以知道 Promise 最终都会成功或失败,变为对应的状态,并执行对应的代码。以上面的代码为例,如果 task 的状态最终为 fulfilled ,即成功,那么便会调用第一个 then() 里面的函数

为什么会调用 then() 里面的函数呢?

因为 then() 的第一个参数需要传入的便是处理成功的函数,因为该函数存在,我们传给了它,所以会被调用

那如果 task 失败了呢?为什么就会 “跳” 到 catch()因为第一个 then() 并没有传入处理失败的函数,所以会被跳过;同理第二个 then() 也没有传入处理失败的函数,所以会被跳过,接着便是 catch()catch() 只能传一个函数,该函数就是用来处理失败的,所以会执行,该分析过程就解释了为什么会出现 “跳” 或 “捕获” 这一逻辑

catch(rejectionHandler) 相当于 then(undefined, rejectionHandler)

新的 Promise 是什么状态?

每个 then()catch() 都会返回新的 Promise ,新的 Promise 与之前的 Promise 的状态实际上并没有直接的关系,新的 Promise 都会被初始化为 pending ,如果有对应的处理函数,那就进行处理,如果没有对应的处理函数,那么新的 Promise 对象就会继承之前 Promise 对象的状态,并继续进行传递

以该代码为例:(篇幅太长了再贴一次代码)

const task = new Promise(...)

task.then(handler1).then(handler2).catch(handler3)

如果 task 成功了,状态为 fulfilled ,第一个 then() 有对应的处理函数,那么返回新的 Promise 对象的状态就为 pending 并执行对应的处理函数,然后继续传递

如果 task 失败了,状态为 rejected ,第一个 then() 没有对应的处理函数,那么返回新的 Promise 对象的状态就会继承为 rejected ,继续传递,第二个 then() 同理,返回的新的 Promise 对象也为 rejected ,所以传到 catch() 时,有对应的执行函数,那么新的 Promise 的状态便为 pending ,随后执行 catch() 里面的函数

解决回调地狱

Promise 并不是什么新的东西,我们完全可以自己进行实现,只是改变了代码的执行顺序,让我们能更容易地编写出符合思考逻辑、贴合业务需求的代码

在 Promise 出现之前,我们要实现类似的效果,只能不断地写回调函数,每写一次回调函数都会进行嵌套,嵌套层次越来越深,代码可读性也就越来越趋于无,这种现象被我们称之为回调地狱

// 传统回调地狱
getUser(id, user => {
  getOrder(user, orders => {
    calcAmount(orders, total => {
      sendBill(total);
    });
  });
});

// Promise链式天堂
获取用户(id)
  .then(获取订单)
  .then(计算金额)
  .then(发送账单);

在上面的知识理解完后,我们可以说解决了回调地狱,但我们还没有解决回调,我们依然需要编写回调函数,我们仍然无法在代码中避免嵌套

解决回调 async & await

ES8 出现了两个新的关键词:asyncawait

  • async 用于标记函数,表示该函数一定返回一个 Promise 对象

    function task() {
        return new Promise(...)
    }
    
    // 等同于
    
    async function task() {
        ...
    }
    

    async 标记的函数里面的内容也是同步代码,不要误以为标记了 async 之后里面的代码就是异步了

  • await 用于等待某个 Promise 对象完成,它必须用于 async 函数中(新的 ES 标准允许顶层的 await,但在日常开发中通常会这样默认)

    async function task() {
      const n = await Promise.resolve(1);
      console.log(n); // 1
    }
    
    // 等同于
    
    function task() {
        return new Promise((resolve, reject) => {
            Promise.resolve(1).then(n => {
              console.log(n);
                resolve(1)
            })
        })
    }
    

需要注意的是:asyncawait 并没有带来新功能,它们只是新的语法糖。

一个被 async 标记的函数在函数体里出现 await 之前的所有代码都是同步代码,会在函数调用时立刻执行

之后的每个 await 实际上就是套了一层 then() ,这个写法可能对初学者来说有些理解难度,但这个写法帮我们解决了回调嵌套,借助 asyncawait ,我们便可以写出不带回调的异步代码了!

但还有最后一个问题:如果需要针对失败的任务进行处理,该怎么办?

可以使用 try-catch 语法:(看代码吧不多解释了,跟正常的 try-catch 没啥区别,只不过执行在异步中)

async function method() {
    try {
        const n = await Promise.reject(123); // 这句代码将抛出异常
        console.log('成功', n)
    } catch (err) {
        console.log('失败', err)
    }
}

method(); // 输出:失败 123

对 Promise 的解释到此便告一段落,相信在你阅读完这篇文章后能有新的体会