前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Promise + async/await 推荐实践

Promise + async/await 推荐实践

作者头像
贤羽
发布2022-06-09 15:13:34
4850
发布2022-06-09 15:13:34
举报

异步任务是我们日常开发中离不开的一环,例如用户操作后的网络请求、动画延时回调、node.js 中各种异步 IO/进程操作等等。

过去通常是通过传递回调函数的形式使用,如今我们通常使用 Promise,配合 async/await,让日常这些异步处理方便了很多。

不过对于刚接触 Promise 的新同学来说,日常可能只接触和使用过其中比较基础的使用形式,又没有花时间去了解其中的实现原理,这就可能会导致一些错误理解和反模式实践。

这里将平时遇见过的问题列举出来,结合自己的理解,希望能帮新同学们绕开一些可以避免的坑。

1. 简要介绍

(1) 什么是 Promise

个人认为,Promise 是一种 可链式触发的单向异步任务单元

  • 它基于 异步任务 进行封装,内部维护一个任务进行状态:进行中已完成已拒绝
  • 初始状态为 进行中,可 单向流转进行中已完成/已拒绝;不可以逆向流转。
  • 一个 Promise 实例在 进行中 状态下,可以通过它的 then(onResolved?, onRejected?) 函数指定 完成/拒绝状态回调函数
  • 异步任务执行完毕时,可以执行以下 A/B 操作之一:
    • (A) 给 完成状态回调函数 传递一个 结果值,进入 已完成 状态;
    • (B) 给 拒绝状态回调函数 传递一个 理由,进入 已拒绝 状态。

上面是 Promise 基本概念,看起来似乎“平平无奇”。然而它又通过以下机制实现了链式触发的效果:

  • then 函数中,将自动创建另一个临时 Promise 实例:
    • 它将在 完成/拒绝状态回调函数 执行完毕时变为 已完成 状态。
    • 状态回调函数的同步返回值将被作为其 结果值
  • 若一个 Promise 完成时的 结果值 也是一个 Promise 时:
    • 结果值的 Promise 将被当作 后续任务 处理。
    • 直到后续任务被 完成/拒绝 后,当前任务才会真正被 完成/拒绝

而其中 then 函数的状态回调函数还存在特殊情况:

  • then 的两个回调函数参数中,不存在对应当前 Promise 状态的回调函数时:
    • 当前 Promise 被完成,却没有 完成状态回调函数 时,临时 Promise 将被以相同的 结果值 完成。
    • 当前 Promise 被拒绝,却没有 拒绝状态回调函数 时,临时 Promise 将被以相同的 理由 拒绝。

这样,我们就可以在日常开发中通过 then 不断地链式创建临时 Promise,让我们的多个异步任务按照预期地逐个触发了。

(2) 什么是 async/await

async/await 被我们日常作为 Promise 状态回调函数函数的语法糖使用。

代码语言:javascript
复制
function createTask(factor) {
    return new Promise((rs) => {
        const start = new Date();
        setTimeout(() => {
            const end = new Date();
            rs(end - start);
        }, factor);
    });
}

const work = () => {
    console.log('Task start...');
    const task = createTask(500);
    task.then((cost) => {
        console.log(`Task end. (${cost}ms)`);
    }).catch((err) => {
        console.error(err);
    });
};

work();

上面的 work 可以使用 async/await 改写为:

代码语言:javascript
复制
const work = async () => {
    console.log('Task start...');
    try {
        const task = createTask(500);
        const cost = await task;
        console.log(`Task end. (${cost}ms)`);
    } catch (err) {
        console.error(err);
    }
};

只需要对 Promise 实例使用 await 操作符,就可以将异步任务的后续处理方式从嵌套的回调函数,彻底改变成仿佛是顺序执行的相同层级语句。甚至还可以使用 try/catch 同时捕获异步任务前后的异常。

尤其是对于多个异步任务逐个执行的情况,代码会简单和清晰很多,减轻业务开发中不必要的思维负担。

而对于暂时不支持 async/await 的浏览器环境,可以通过 babel+regeneratorRuntime 对项目代码进行转换,从而在日常开发中放心的使用这项新语法糖。

2. 不良实践与改进

(1) 嵌套的 Promise 回调

对于初次使用 Promise 的新手,可能会因为不知道可以在 then 回调内直接传递新的 Promise 作为 结果值,从而把 Promise 当作过去的回调函数使用,重新陷入回调地狱:

代码语言:javascript
复制
// Bad:
new Promise((rs) => {
    console.log('Step 1 start...');
    doSomething(() => {
        console.log('Step 1 finished.');
        rs();
    });
}).then(() => {
    new Promise((rs) => {
        console.log('Step 2 start...');
        doSomething(() => {
            console.log('Step 2 finished.');
            rs();
        });
    }).then(() => {
        new Promise((rs) => {
            console.log('Step 3 start...');
            doSomething(() => {
                console.log('Step 3 finished.');
                rs();
            });
        }).then(() => {
            console.log('All steps finished');
        });
    });
});

得益于 Promise 递归等待的机制,我们可以直接在最外层的 then 后面链式追加后续任务,并不需要反复嵌套:

代码语言:javascript
复制
new Promise((rs) => {
    console.log('Step 1 start...');
    doSomething(() => {
        console.log('Step 1 finished.');
        rs();
    });
}).then(() => new Promise((rs) => {
    console.log('Step 2 start...');
    doSomething(() => {
        console.log('Step 2 finished.');
        rs();
    });
})).then(() => new Promise((rs) => {
    console.log('Step 3 start...');
    doSomething(() => {
        console.log('Step 3 finished.');
        rs();
    });
})).then(() => {
    console.log('All steps finished');
});

当然,还可以使用 async/await 处理:

代码语言:javascript
复制
(async () => {
    await new Promise((rs) => {
        console.log('Step 1 start...');
        doSomething(() => {
            console.log('Step 1 finished.');
            rs();
        });
    });
    await new Promise((rs) => {
        console.log('Step 2 start...');
        doSomething(() => {
            console.log('Step 2 finished.');
            rs();
        });
    });
    await new Promise((rs) => {
        console.log('Step 3 start...');
        doSomething(() => {
            console.log('Step 3 finished.');
            rs();
        });
    });
    console.log('All steps finished');
})();

(2) 忽视异常处理

新同学使用日常使用 Promise 时,可能并不会留心给每次 Promise 调用的最后加上 catch() 进行异常捕获。

或者直接使用 try/catch 尝试捕获 Promise 异步任务和状态回调内的异常,发现没能如预期地捕获到。

这是由于 Promise 的异步函数执行时,已经脱离创建时的调用栈,其内部发生的错误没法直接被调用时的 try/catch 捕捉到。

可以通过以下例子模拟类似的情形:

代码语言:javascript
复制
function doItLater(fn, delay) {
    setTimeout(fn, delay);
}

try {
    doItLater(() => {
        // 这个异常无法被这里的 try/catch 捕获到
        throw new Error('Out of catch.');
    }, 100);
} catch(ex) {
    console.error(ex);
}

doItLater() 中的 setTimeout(fn, delay) 改为 fn() 同步调用,就能在外层捕获到异常。而 Promise 的状态回调并非同步执行,所以无法在外层直接捕获异常。

对于异步任务,我们需要通过 catch() 进行异常捕获,以便在外层做好任务被拒绝或者其它意外的处理:

代码语言:javascript
复制
new Promise((rs) => {
    console.log('Task start...');
    doSomething(() => {
        console.log('Task finished.');
        rs();
    });
}).then(() => {
    console.log('Done');
}).catch((ex) => {
    console.error(ex);
    reportError(ex);
});

不过 catch() 只能捕获到 Promise 内部的异常,如果需要同时捕获异步任务之前的某些同步处理异常,还得把相同的异常处理再用 try/catch 写一遍:

代码语言:javascript
复制
try {
    doSomePreprocessing();
} catch (ex) {
    // 异常处理
    console.error(ex);
    reportError(ex);
}
new Promise((rs) => {
    try {
        console.log('Task start...');
        doSomething(() => {
            console.log('Task finished.');
            rs();
        });
    } catch (ex) {
        // 异常处理
        console.error(ex);
        reportError(ex);
    }
}).then(() => {
    console.log('Done');
}).catch((ex) => {
    // 异常处理
    console.error(ex);
    reportError(ex);
});

相同的异常处理写了三遍,有些可怕……不过上面的例子有点刻意了,doSomePreprocessing() 其实可以放在 Task start 相同的 try/catch 里。

但有时候也不一定能这样重新组织代码,不如直接使用 async/await 避免这样的冗余情况:

代码语言:javascript
复制
(async () => {
    try {
        doSomePreprocessing();
        await new Promise((rs) => {
            console.log('Task start...');
            doSomething(() => {
                console.log('Task finished.');
                rs();
            });
        });
        console.log('Done');
    } catch (ex) {
        // 异常处理
        console.error(ex);
        reportError(ex);
    }
})();

(3) await 一把梭

日常开发中,如果涉及到多个异步任务的情况,新同学可能没有多想就直接使用 await 让它们逐个执行了:

代码语言:javascript
复制
(async () => {
    // 展示 loading 动画
    setLoading(true);
    try {
        // 加载商品类别信息
        await loadGoodsCatalogs();
        // 加载地区信息
        await loadGeoData();
        // 加载用户信息
        await loadUserInfo();
        // 加载用户绑定的收货地址
        await loadUserAddress();
        // 加载用户绑定的支付方式
        await loadUserPayingMethods();

        // 更新表单
        refreshForm();
    } catch (ex) {
        showErrorInfo(ex);
    }
    // 关闭 loading 动画
    setLoading(false);
})();

然而稍微观察就会发现,上面的请求的数据中可能存在前后依赖关系的情况,但也有不少可以并行处理的数据。

而让所有请求一股脑排队串行处理,既浪费现在日新月异的终端性能,又浪费用户宝贵的等待时间,未免有些暴殄天物。

对于并行处理的任务,我们可以使用 Promise.all() 方法:

  • 它接收一个 Promise 数组参数,返回一个新的 Promise
  • 同时启动其中的异步任务,直到它们全部结束时转为 已完成 状态。

让我们用它重新组织上面的异步任务,提高一下页面效率吧:

代码语言:javascript
复制
(async () => {
    // 展示 loading 动画
    setLoading(true);
    try {
        // 1. 需要逐个串行获取的用户相关数据
        const loadUserData = async () => {
            // 加载用户信息
            await loadUserInfo();
            // 加载用户绑定的收货地址
            await loadUserAddress();
            // 加载用户绑定的支付方式
            await loadUserPayingMethods();
        };
        // 2. 可以并行处理的各类数据
        await Promise.all([
            // 加载商品类别信息
            loadGoodsCatalogs(),
            // 加载地区信息
            loadGeoData(),
            // 加载用户相关数据
            loadUserData(),
        ]);

        // 更新表单
        refreshForm();
    } catch (ex) {
        showErrorInfo(ex);
    }
    // 关闭 loading 动画
    setLoading(false);
})();

(4) race 与 any

除了 Promise.all(),还有两个类似的 Promise.race()Promise.any() 方法。

Promise.race():

  • 参数中的所有 Promise 同时启动,并进行竞赛
  • 任何一个异步任务 发生状态改变时,当前 Promise.race 封装的任务转为其相同的 已完成/已拒绝 状态。

Promise.any():

  • 参数中的所有 Promise 同时启动。
  • 其中任何一个异步任务完成时,当前 Promise.any 转为 已完成
  • 如果所有异步任务最终都未完成,则转为 已拒绝 并返回它们的异常集合,亦即所有 拒绝理由

注意! Promise.any() 方法依然是实验特性,尚未被浏览器完全支持。

3. 更多

(1) 复杂任务

对于类似 IO 任务的情况,可能需要反复确认完成进度的情况。

直接封装为只有开始结束态的 Promise 的话,会让用户长时间等待中无法获得任何感知,用户体验较差。

需要配合传统回调函数,结合具体的业务需求和页面交互进行实现。

(2) 宏任务与微任务

Promise/A+ 的规范中,Promise 的实现可以是微任务,也可以是宏任务。不过普遍的共识一般将 Promise.then 的状态回调作为微任务实现。

相比之下,setTimeout 的宏任务将会在同一批创建的 Promise.then 微任务之后执行。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-07-042,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 简要介绍
    • (1) 什么是 Promise
      • (2) 什么是 async/await
      • 2. 不良实践与改进
        • (1) 嵌套的 Promise 回调
          • (2) 忽视异常处理
            • (3) await 一把梭
              • (4) race 与 any
              • 3. 更多
                • (1) 复杂任务
                  • (2) 宏任务与微任务
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档