Daily-Interview-Question icon indicating copy to clipboard operation
Daily-Interview-Question copied to clipboard

第 12 题:JS 异步解决方案的发展历程以及优缺点。

Open xiaofengqqcom123 opened this issue 5 years ago • 29 comments

参考:阮一峰异步编程

xiaofengqqcom123 avatar Feb 13 '19 13:02 xiaofengqqcom123

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 内部实现了 generatorgenerator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
  • 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。

本文首发于我的博客:JS异步解决方案的发展历程以及优缺点

sisterAn avatar Feb 16 '19 09:02 sisterAn

@sisterAn 没有依赖关系的异步操作不使用 await 就没有你所说的性能问题了。 比如:

async function test() {
  // 以下代码没有依赖性的话,不使用 await 便不会阻塞运行
  fetch('XXX1')
  fetch('XXX2')
  fetch('XXX3')
}

inJs avatar Mar 07 '19 08:03 inJs

@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)

Zousdie avatar Mar 07 '19 14:03 Zousdie

@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 avatar Mar 27 '19 15:03 duanzheng

@duanzheng 若 b 里直接访问 a ,即写为 a = a + r,则最后then打印'a' 11,因为会先执行 a++。我猜他应该是为了保持最后的打印结果是 'a' 10,所以才使用了一个变量缓存最初a的值吧。个人见解

Randysheng avatar May 23 '19 15:05 Randysheng

@sisterAn 没有依赖关系的异步操作不使用 await 就没有你所说的性能问题了。 比如:

async function test() {
  // 以下代码没有依赖性的话,不使用 await 便不会阻塞运行
  fetch('XXX1')
  fetch('XXX2')
  fetch('XXX3')
}

其实提到了 Promise.al 就说明这里有个隐含前提,尽管三个 fetch 之前没有依赖,但是需要等待三个 fetch 都已经完成了再执行下一句。

michaelcai avatar Jul 10 '19 07:07 michaelcai

callback->Promise->generator->async/await

arixse avatar Jul 10 '19 08:07 arixse

需要好好理解的是async, await VS Generator改进了什么

  1. async内置执行器
  2. 更好的语义化

NuoHui avatar Jul 22 '19 09:07 NuoHui

Promise 如果在这样写 Promise.resolve().then(function() { return new Promise(function() {}) }) 这样写的话Promise就可以中止Promise执行链,相当于取消Promise了

lk3407105 avatar Jul 23 '19 01:07 lk3407105

@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 

252860883 avatar Aug 21 '19 05:08 252860883

@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

eightHundreds avatar Aug 22 '19 02:08 eightHundreds

@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。

kt3721 avatar Sep 04 '19 03:09 kt3721

最早的异步的实现应该: 1.回调函数 缺点: 回调地狱,不利于维护 2. promise 可以链式调用了 解决了回调地狱, 但是无法取消promise 一旦开启只有pending resolve reject 不能取消 3.generator yield next 4.async await 不是所有场景都使用 注意性能问题 可以用try catch 捕获异常,将异步代码改成同步代码,如果多个操作没有依赖性 会造成性能问题

xiaoxixi6633 avatar Sep 27 '19 02:09 xiaoxixi6633

@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会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行

tywei90 avatar Nov 02 '19 07:11 tywei90

@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

yinzuowen avatar Dec 02 '19 01:12 yinzuowen

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)
}

No3ming avatar Jan 10 '20 16:01 No3ming

@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++

wojiaowanyuxuan avatar Jan 25 '20 13:01 wojiaowanyuxuan

promise的缺点有三个: 1、promise一旦新建,就会立即执行,无法取消 2、promise如果不设置回调函数,promise内部抛出的错误,不会反应到外部 3、promise处于pending状态时,无法得知目前进展到哪一阶段,刚开始执行还是即将完成

womenbudon avatar Feb 25 '20 00:02 womenbudon

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 avatar May 23 '20 14:05 wfymalong

@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啊

jackchang2015 avatar Aug 17 '20 07:08 jackchang2015

@inJs 虽然这三个异步操作没有依赖关系,那要是函数内下面的某个操作需要他们三个全部返回才能处理怎么办呢?所以还是得用await,否则连async函数都没必要写。

async function test(){
  const cbs = await Promise.all([fetch('XXX1'), fetch('XXX2'),fetch('XXX3')])
  // ...其他需要使用cbs的操作
}

xsfxtsxxr avatar Aug 20 '20 02:08 xsfxtsxxr

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)
}

xsfxtsxxr avatar Aug 20 '20 02:08 xsfxtsxxr

没人提rxjs吗。 ReactiveX是一种针对异步数据流的编程。简单来说,它将一切数据,包括HTTP请求,DOM事件或者普通数据等包装成流的形式,然后用强大丰富的操作符对流进行处理,使你能以同步编程的方式处理异步数据,并组合不同的操作符来轻松优雅的实现你所需要的功能 。rxjs是ReactiveX的javascript实现。 对rxjs简单的理解,就是把一系列的异步行为用时间维度的数组表示出来,将行为视为同步数据一般进行处理,编排与更新。 promise只有成功和失败两个结果,且一旦开始不能中止。rxjs则可以有多个成功结果,可以随时中止或进行其他分叉处理。 promise通常只能对串行、并发的多个异步行为进行简单的异步控制,一旦叠加其他维度条件控制则力不从心。 比如promise在实际应用中会因接口响应慢而导致竞态条件问题。rxjs可以轻松的通过switchMap操作符解决。

dunhuang avatar Mar 16 '21 02:03 dunhuang

@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

microJ avatar Apr 26 '21 07:04 microJ

Rxjs

ietwangwei avatar Jul 16 '21 07:07 ietwangwei

回调 -》 回调地域 发布订阅/事件监听-》执行流程不清晰 Promise -》获取错误还是得回调函数处理,也存在一定的类似 ”回调地域“ Generator 生成器、Async/await-》完美解决 异步代码 ”同步“ 化 ,书写方便,代码流程阅读清晰

Yangfan2016 avatar Aug 23 '22 13:08 Yangfan2016

有个疑问: 不能说回调函数是异步的解决方案吧, 不是用在setTimeout等这些异步方法内的回调函数才算得上是异步吗

wever-liang avatar Nov 16 '22 07:11 wever-liang

参考:阮一峰异步编程

进不去了,贴个新的 https://javascript.ruanyifeng.com/advanced/single-thread.html#

Yangfan2016 avatar Feb 13 '23 09:02 Yangfan2016

有个疑问: 不能说回调函数是异步的解决方案吧, 不是用在setTimeout等这些异步方法内的回调函数才算得上是异步吗

对,准确来说,应该叫 “异步回调” ,不过 回调函数经常被用于在一个异步操作完成后执行代码,这里这么说也合理 https://developer.mozilla.org/zh-CN/docs/Glossary/Callback_function

Yangfan2016 avatar Feb 13 '23 09:02 Yangfan2016