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

发布于 2022-11-28 18:09:57 字数 143 浏览 120 评论 25

代码是自上而下同步执行的,既后面的代码必须等待前面的代码执行完才会执行,而异步执行则是将主线程中的某段代码交由子线程去执行,当交给子线程后,主线程就会继续执行后面代码,而不用等待子线程执行完成,异步是程序语言并行执行的一种手段,通常将耗时的任务交由子线程同时处理,从而提升整体任务耗时。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(25

煞人兵器 2022-05-04 13:57:40
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)

}

最偏执的依靠 2022-05-04 13:57:40

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

迷爱· 2022-05-04 13:57:40

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

async function test(){
  const cbs = await Promise.all([fetch('XXX1'), fetch('XXX2'),fetch('XXX3')])
  // ...其他需要使用cbs的操作
}
哆兒滾 2022-05-04 13:57:40
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)
}
苏大泽ㄣ 2022-05-04 13:57:40

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

╭ゆ眷念 2022-05-04 13:57:40

@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

拒绝两难 2022-05-04 13:57:40

Rxjs

眼泪淡了忧伤 2022-05-04 13:57:32
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)
}
孤君无依 2022-05-04 13:57:32

@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++
等风来 2022-05-04 13:57:32

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

西瓜 2022-05-04 13:57:22

@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

寂寞陪衬 2022-05-04 13:57:09

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

千柳 2022-05-04 13:52:18

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

撩发小公举 2022-05-04 13:50:48

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

南城追梦i 2022-05-04 13:50:27

@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
我早已燃尽 2022-05-04 13:48:57

@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 
め可乐爱微﹏笑 2022-05-04 13:38:50

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

浊酒尽余欢 2022-05-04 13:35:31

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

  1. async内置执行器
  2. 更好的语义化
朦胧时间 2022-05-04 13:33:01

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

别闹i 2022-05-04 13:32:22

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

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

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

霞映澄塘 2022-05-04 13:29:44

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

一抹微笑 2022-05-04 13:22:51

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

埋情葬爱 2022-05-04 09:58:17

@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)
醉城メ夜风 2022-05-03 18:22:04

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

async function test() {
  // 以下代码没有依赖性的话,不使用 await 便不会阻塞运行
  fetch('XXX1')
  fetch('XXX2')
  fetch('XXX3')
}
仅冇旳回忆 2022-05-03 15:14:27

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异步解决方案的发展历程以及优缺点

~没有更多了~

关于作者

陌伤浅笑

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

巷子口的你

文章 0 评论 0

微信用户

文章 0 评论 0

神妖

文章 0 评论 0

7460852697

文章 0 评论 0

ligengkai

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文