背景
CodeReview中发现存在误用Promise, 对Promise相关callback的回调时机控制存在问题的场景
因此梳理了这份文档, 主要收集了常见的误用场景, 比较复杂的场景如何处理, 如何通过 Promise优化网页体验, 以及 async await 实现原理
export async function getStaticConf() { let rollbackToTaskCenterModuleList; await axios.post('/rest/wd/kconf/get', { key: 'frontend.browserConfig.lowActiveConfig', type: 'json', }).then( res => { rollbackToTaskCenterModuleList = res.data.rollbackToTaskCenterModuleList; }, () => { rollbackToTaskCenterModuleList = false; }); return rollbackToTaskCenterModuleList;}
上面的例子中,axios.post 执行后发出了异步请求,其链式调用 then 的回调函数是异步执行的, 因此
rollbackToTaskCenterModuleList 总是返回 undefined
可以对then的返回值使用await,就能实现我们想要的结果
export async function getStaticConf() { let rollbackToTaskCenterModuleList; await axios.post('/rest/wd/kconf/get', { key: 'frontend.browserConfig.lowActiveConfig', type: 'json', }).then( res => { rollbackToTaskCenterModuleList = res.data.rollbackToTaskCenterModuleList; }, () => { rollbackToTaskCenterModuleList = false; }); return rollbackToTaskCenterModuleList;}
更进一步: Promise 的各部分回调函数是异步执行的吗
new Promise 里的代码是同步的,then、catch、finally 都是异步执行的
代码地址 https://codesandbox.io/s/11-q2ub7
无意识地充分串行执行
开发中可能会出现 把可以并行执行的代码 串行执行
const start = new Date();const a = await getValue();const b = await getValue();const c = await getValue();console.log(new Date() - start); // 300
假设 每个getValue 所需的时间都是100ms, 那么总共所需的时间为 300ms,时间轴如下
解决方法
1)为了将代码并行执行,可以的使用Promise.all的方式
const start = new Date();const [a, b, c] = await Promise.all([ getValue(), getValue(), getValue()]);console.log(new Date() - start); // 100
2)还可以 先初始化Promise,在需要获取值得地方 再await
const start = new Date(); const a1 = getValue();const b1 = getValue();const c1 = getValue();const a = await a1;const b = await b1;const c = await c1;console.log(new Date() - start); // 100
可以看到3个方法都是并行执行的,代码链接: https://codesandbox.io/s/22-dn0le?file=/src/index.js
为了测试每个值获取到的时间,https://codesandbox.io/s/23-d5ou1
可以看到,Promise的执行时间虽然是固定的,但在变量赋值的时机上,前面的await会影响后面的await,整体结束的时间取决于时间最长的那个方法
b方法的执行时间虽然只有200,但是 受限于c的await,需要300ms后才能赋值给b
异步调用常规习惯是
- 不要着急await
- 延迟到要用 Promise 中保存的值时, 再 await
- 将 Promise 想象成魔法师的帽子, 未来某个时刻可以掏出兔子, 但不一定要现在掏
可选await的场景(缓存 及其 初始化)
需求:某些功能的接口请求,期望能立即返回缓存数据,并显示出来,随后立即对数据进行更新
方法:对于已缓存数据,优先返回缓存数据,并对数据更新;对于未缓存的数据,先表现loading,请求接口并更新数据成功后,loading结束,并替换内容
在vue中我们可以使用如下伪代码来实现这个功能
foo: function(...args) { const params = 12222 const pms = ajax(params) const update = (v) => this.xx = v if (cache == null) { cache = await pms } else { pms.then(res => update(res)) } update(cache);}
async await 原理
generator简单介绍
function *get() { yield fetch('/'); yield fetch('/a'); return 1;}
这是一个典型的generator函数,使用方法
var g = get();g.next(); // {value: Promise, done: false}g.next(); // {value: Promise, done: false}g.next(); // {value: 1, done: true}
将generator函数编译成es5代码
var __generator = () { ...}function get() { return __generator(function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, fetch('/')]; case 1: return [4 /*yield*/, fetch('/a')]; case 2: return [2 /*return*/, 1]; } });}
尝试对async函数转换成es5代码
async function get() { await fetch('/'); await fetch('/a'); return 1;}
转换后
var __awaiter = () {...}var __generator = () { ...}function get() { return __awaiter(function () { return __generator(function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, fetch('/')]; case 1: return [4 /*yield*/, fetch('/a')]; case 2: return [2 /*return*/, 1]; } }); });}
可以看到 主要是两个函数,awaiter 还有generator,__awaiter 函数的大概内容如下
var __awaiter = function(_g) { var generator = _g(); return new Promise(function(resolve) { function fulfilled() { step(generator.next()); } function step(result) { // {value: 1, done: true} result.done ? resolve(result.value) : Promise.resolve(result.value).then(fulfilled); } step(generator.next()); // {value: Promise, done: fales} });};
它的核心是 订阅Promise的执行完成时机,自动调用generator的next方法,这样就不用每次手动再调用next方法,使用的体验更好也是 async 和 generator 的区别
结论:async 函数是 generator的语法糖,优点是能够自动调用next
generator函数的简单实现
var __generator = function(body) { var _ = { label: 0, } return { next: function() { var op = body(_); switch (op[0]) { case 4: _.label++; return { value: op[1], done: false }; case 2: return { value: op[1], done: true }; } } }};
如果是并行执行
async function get() { const f1 = fetch('/'); const f2 = fetch('/a'); await f1; await f2; return 1;}
转换后
var __awaiter = () {...}var __generator = () { ...}function get() { return __awaiter(function () { var f1, f2; return __generator(function (_a) { switch (_a.label) { case 0: f1 = fetch('/'); f2 = fetch('/a'); return [4 /*yield*/, f1]; case 1: return [4 /*yield*/, f2]; case 2: return [2 /*return*/, 1]; } }); });}
比较转换后的代码差异
可以看到串行时,f1先执行,并且订阅了f1的执行完成事件,完成时 ,再调用f2,订阅f2的执行完成事件,再return 1;
与此相对并行时,f1和f2先后一起执行,订阅f1的执行完成事件,完成时,再调用f2的执行完成事件,在return 1。