Daily-Interview-Question
Daily-Interview-Question copied to clipboard
第 12 题:JS 异步解决方案的发展历程以及优缺点。
参考:阮一峰异步编程
JS 异步已经告一段落了,这里来一波小总结
1. 回调函数(callback)
setTimeout(() => {
// callback 函数体
}, 1000)
缺点:回调地狱,不能用 try catch 捕获错误,不能 return
回调地狱的根本问题在于:
- 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
- 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
- 嵌套函数过多的多话,很难处理错误
ajax('XXX1', () => {
// callback 函数体
ajax('XXX2', () => {
// callback 函数体
ajax('XXX3', () => {
// callback 函数体
})
})
})
优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
2. Promise
Promise就是为了解决callback的问题而产生的。
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
优点:解决了回调地狱的问题
ajax('XXX1')
.then(res => {
// 操作逻辑
return ajax('XXX2')
}).then(res => {
// 操作逻辑
return ajax('XXX3')
}).then(res => {
// 操作逻辑
})
缺点:无法取消 Promise ,错误需要通过回调函数来捕获
3. Generator
特点:可以控制函数的执行,可以配合 co 函数库使用
function *fetch() {
yield ajax('XXX1', () => {})
yield ajax('XXX2', () => {})
yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
4. Async/await
async、await 是异步的终极解决方案
优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch('XXX1')
await fetch('XXX2')
await fetch('XXX3')
}
下面来看一个使用 await
的例子:
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1
对于以上代码你可能会有疑惑,让我来解释下原因
- 首先函数
b
先执行,在执行到await 10
之前变量a
还是 0,因为await
内部实现了generator
,generator
会保留堆栈中东西,所以这时候a = 0
被保存了下来 - 因为
await
是异步操作,后来的表达式不返回Promise
的话,就会包装成Promise.reslove(返回值)
,然后会去执行函数外的同步代码 - 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候
a = 0 + 10
上述解释中提到了 await
内部实现了 generator
,其实 await
就是 generator
加上 Promise
的语法糖,且内部实现了自动执行 generator
。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。
本文首发于我的博客:JS异步解决方案的发展历程以及优缺点
@sisterAn 没有依赖关系的异步操作不使用 await
就没有你所说的性能问题了。 比如:
async function test() {
// 以下代码没有依赖性的话,不使用 await 便不会阻塞运行
fetch('XXX1')
fetch('XXX2')
fetch('XXX3')
}
@sisterAn 规范里好像没提到 await 内部实现了 generator, 如果从 polyfill 中的实现去断定 await 内部就是 generator……这样好像有点不严谨……
另外 await
的例子其实可以转换为
var a = 0
var b = () => {
var temp = a;
Promise.resolve(10)
.then((r) => {
a = temp + r;
})
.then(() => {
console.log('2', a)
})
}
b()
a++
console.log('1', a)
@sisterAn 规范里好像没提到 await 内部实现了 generator, 如果从 polyfill 中的实现去断定 await 内部就是 generator……这样好像有点不严谨……
另外
await
的例子其实可以转换为var a = 0 var b = () => { var temp = a; Promise.resolve(10) .then((r) => { a = temp + r; }) .then(() => { console.log('2', a) }) } b() a++ console.log('1', a)
请教一下,为何 b 里不是直接访问 a?
@duanzheng
若 b 里直接访问 a ,即写为 a = a + r
,则最后then打印'a' 11
,因为会先执行 a++
。我猜他应该是为了保持最后的打印结果是 'a' 10
,所以才使用了一个变量缓存最初a的值吧。个人见解
@sisterAn 没有依赖关系的异步操作不使用
await
就没有你所说的性能问题了。 比如:async function test() { // 以下代码没有依赖性的话,不使用 await 便不会阻塞运行 fetch('XXX1') fetch('XXX2') fetch('XXX3') }
其实提到了 Promise.al 就说明这里有个隐含前提,尽管三个 fetch 之前没有依赖,但是需要等待三个 fetch 都已经完成了再执行下一句。
callback->Promise->generator->async/await
需要好好理解的是async, await VS Generator改进了什么
- async内置执行器
- 更好的语义化
Promise 如果在这样写
Promise.resolve().then(function() { return new Promise(function() {}) })
这样写的话Promise就可以中止Promise执行链,相当于取消Promise了
@sisterAn 规范里好像没提到 await 内部实现了 generator, 如果从 polyfill 中的实现去断定 await 内部就是 generator……这样好像有点不严谨……
另外
await
的例子其实可以转换为var a = 0 var b = () => { var temp = a; Promise.resolve(10) .then((r) => { a = temp + r; }) .then(() => { console.log('2', a) }) } b() a++ console.log('1', a)
我尝试以下代码片段,按照您这样模拟是行不通的,不太理解为什么我这样写async里面输出就是 11:
let a = 0
let b = async () => {
let c = await 10
a = a + c
console.log('2', a) // 2 11
}
b()
a++
console.log('1', a) // 1 1
@sisterAn 规范里好像没提到 await 内部实现了 generator, 如果从 polyfill 中的实现去断定 await 内部就是 generator……这样好像有点不严谨…… 另外
await
的例子其实可以转换为var a = 0 var b = () => { var temp = a; Promise.resolve(10) .then((r) => { a = temp + r; }) .then(() => { console.log('2', a) }) } b() a++ console.log('1', a)
我尝试以下代码片段,按照您这样模拟是行不通的,不太理解为什么我这样写async里面输出就是 11:
let a = 0 let b = async () => { let c = await 10 a = a + c console.log('2', a) // 2 11 } b() a++ console.log('1', a) // 1 1
let a = 0
let b = async () => {
let tmp = a
let c = await 10
a = tmp + c
console.log('2', a) // 2 10
}
b()
a++
console.log('1', a) // 1 1
@sisterAn 规范里好像没提到 await 内部实现了 generator, 如果从 polyfill 中的实现去断定 await 内部就是 generator……这样好像有点不严谨…… 另外
await
的例子其实可以转换为var a = 0 var b = () => { var temp = a; Promise.resolve(10) .then((r) => { a = temp + r; }) .then(() => { console.log('2', a) }) } b() a++ console.log('1', a)
我尝试以下代码片段,按照您这样模拟是行不通的,不太理解为什么我这样写async里面输出就是 11:
let a = 0 let b = async () => { let c = await 10 a = a + c console.log('2', a) // 2 11 } b() a++ console.log('1', a) // 1 1
上面说了执行到 await 的时候会保留 堆栈中的东西,这个时候变量a并没有使用,所以并没有保留 a = 0;当 await 结束后,再使用变量a,此时a的值经过 a++ 已经变成了 1 了。所以最后输出的是11。
最早的异步的实现应该: 1.回调函数 缺点: 回调地狱,不利于维护 2. promise 可以链式调用了 解决了回调地狱, 但是无法取消promise 一旦开启只有pending resolve reject 不能取消 3.generator yield next 4.async await 不是所有场景都使用 注意性能问题 可以用try catch 捕获异常,将异步代码改成同步代码,如果多个操作没有依赖性 会造成性能问题
@sisterAn 其实最后async await代码可以翻译成下面这样就清爽了:
let a = 0
let b = async () => {
const c = await 10;
a = a + c;
console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1
async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行
@sisterAn 规范里好像没提到 await 内部实现了 generator, 如果从 polyfill 中的实现去断定 await 内部就是 generator……这样好像有点不严谨…… 另外
await
的例子其实可以转换为var a = 0 var b = () => { var temp = a; Promise.resolve(10) .then((r) => { a = temp + r; }) .then(() => { console.log('2', a) }) } b() a++ console.log('1', a)
我尝试以下代码片段,按照您这样模拟是行不通的,不太理解为什么我这样写async里面输出就是 11:
let a = 0 let b = async () => { let c = await 10 a = a + c console.log('2', a) // 2 11 } b() a++ console.log('1', a) // 1 1
let a = 0 let b = async () => { let tmp = a let c = await 10 a = tmp + c console.log('2', a) // 2 10 } b() a++ console.log('1', a) // 1 1
上一个console,a++之后的,变成1了;下一个console,tmp存的是a++之前的,值为0
async ƒ test() {
// 这样不会阻塞性能把
let a = fetch('XXX1')
let b = fetch('XXX2')
let c = fetch('XXX3')
let aa = await a
let bb = await b
let cc = await c
console.log(aa,bb,cc)
}
@sisterAn 规范里好像没提到 await 内部实现了 generator, 如果从 polyfill 中的实现去断定 await 内部就是 generator……这样好像有点不严谨…… 另外
await
的例子其实可以转换为var a = 0 var b = () => { var temp = a; Promise.resolve(10) .then((r) => { a = temp + r; }) .then(() => { console.log('2', a) }) } b() a++ console.log('1', a)
我尝试以下代码片段,按照您这样模拟是行不通的,不太理解为什么我这样写async里面输出就是 11:
let a = 0 let b = async () => { let c = await 10 a = a + c console.log('2', a) // 2 11 } b() a++ console.log('1', a) // 1 1
你这样的写法a = a + c已经在一个微任务队列中,这时候由于同步任务a++先执行,导致a被读取时为1,你的写法等价于
let a = 0;
let b = async () => {
let c;
Promise(10).then(r => {
c = r;
})
.then(_ => {
a = a + c; // 此时a++,已经在第一次宏任务中执行过
console.log('2', a) // '2' ,11
});
}
b()
a++
promise的缺点有三个: 1、promise一旦新建,就会立即执行,无法取消 2、promise如果不设置回调函数,promise内部抛出的错误,不会反应到外部 3、promise处于pending状态时,无法得知目前进展到哪一阶段,刚开始执行还是即将完成
async ƒ test() { // 这样不会阻塞性能把 let a = fetch('XXX1') let b = fetch('XXX2') let c = fetch('XXX3') let aa = await a let bb = await b let cc = await c console.log(aa,bb,cc) }
还是会有点影响,这样才不会阻塞 async function test() { let a = fetch('XXX1',) let b = fetch('XXX2',) let c = fetch('XXX3',)
let [a1,b1,c1]=await Promise.all([a,b,c])
console.log(a1,b1,c1)
}
@sisterAn 其实最后async await代码可以翻译成下面这样就清爽了:
let a = 0 let b = async () => { const c = await 10; a = a + c; console.log('2', a) // -> '2' 10 } b() a++ console.log('1', a) // -> '1' 1
async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行
我试了一下打印出的还是2,11啊
@inJs 虽然这三个异步操作没有依赖关系,那要是函数内下面的某个操作需要他们三个全部返回才能处理怎么办呢?所以还是得用await,否则连async函数都没必要写。
async function test(){
const cbs = await Promise.all([fetch('XXX1'), fetch('XXX2'),fetch('XXX3')])
// ...其他需要使用cbs的操作
}
async ƒ test() { // 这样不会阻塞性能把 let a = fetch('XXX1') let b = fetch('XXX2') let c = fetch('XXX3') let aa = await a let bb = await b let cc = await c console.log(aa,bb,cc) }
还是会有点影响,这样才不会阻塞 async function test() { let a = fetch('XXX1',) let b = fetch('XXX2',) let c = fetch('XXX3',)
let [a1,b1,c1]=await Promise.all([a,b,c]) console.log(a1,b1,c1)
}
@wfymalong 如果把异步函数提前赋值给变量,就不需要用Promise.all了吧,人家给的例子是正确的。
async ƒ test() {
// 这样不会阻塞性能把
let a = fetch('XXX1') // 我认为赋值的同时已经在执行异步函数了
let b = fetch('XXX2') // 这样就是并发执行三个异步函数
let c = fetch('XXX3') // 就不存在阻塞了
let aa = await a
let bb = await b
let cc = await c
console.log(aa,bb,cc)
}
没人提rxjs吗。 ReactiveX是一种针对异步数据流的编程。简单来说,它将一切数据,包括HTTP请求,DOM事件或者普通数据等包装成流的形式,然后用强大丰富的操作符对流进行处理,使你能以同步编程的方式处理异步数据,并组合不同的操作符来轻松优雅的实现你所需要的功能 。rxjs是ReactiveX的javascript实现。 对rxjs简单的理解,就是把一系列的异步行为用时间维度的数组表示出来,将行为视为同步数据一般进行处理,编排与更新。 promise只有成功和失败两个结果,且一旦开始不能中止。rxjs则可以有多个成功结果,可以随时中止或进行其他分叉处理。 promise通常只能对串行、并发的多个异步行为进行简单的异步控制,一旦叠加其他维度条件控制则力不从心。 比如promise在实际应用中会因接口响应慢而导致竞态条件问题。rxjs可以轻松的通过switchMap操作符解决。
@sisterAn 规范里好像没提到 await 内部实现了 generator, 如果从 polyfill 中的实现去断定 await 内部就是 generator……这样好像有点不严谨…… 另外
await
的例子其实可以转换为var a = 0 var b = () => { var temp = a; Promise.resolve(10) .then((r) => { a = temp + r; }) .then(() => { console.log('2', a) }) } b() a++ console.log('1', a)
请教一下,为何 b 里不是直接访问 a?
AsyncFunctionBody 中遇到 await 关键字的时候,会解析成 AwaitExpression 并等待其返回结果。上面代码中,对表达式 a + await 10
进行求职的时候,第一个 await 出现之前的代码是同步代码,此时 a 通过 RHS 得到的值是 0
相关流程可以感受下:
;(async () => {
console.log("sync 0")
var a =
(console.log("sync 1"), await "", console.log("async 3"), await "resolved")
console.log("a: ", a)
})()
console.log("sync 2")
// 打印顺序:
// sync 0
// sync 1
// sync 2
// async 3
// a: resolved
另外,书写时推荐 await 放在表达式的开头。ESLint 规则:https://cn.eslint.org/docs/rules/require-atomic-updates
Rxjs
回调 -》 回调地域 发布订阅/事件监听-》执行流程不清晰 Promise -》获取错误还是得回调函数处理,也存在一定的类似 ”回调地域“ Generator 生成器、Async/await-》完美解决 异步代码 ”同步“ 化 ,书写方便,代码流程阅读清晰
有个疑问: 不能说回调函数是异步的解决方案吧, 不是用在setTimeout等这些异步方法内的回调函数才算得上是异步吗
有个疑问: 不能说回调函数是异步的解决方案吧, 不是用在setTimeout等这些异步方法内的回调函数才算得上是异步吗
对,准确来说,应该叫 “异步回调” ,不过 回调函数经常被用于在一个异步操作完成后执行代码,这里这么说也合理 https://developer.mozilla.org/zh-CN/docs/Glossary/Callback_function