Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
xiaofengqqcom123 opened this issue Feb 13, 2019 · 29 comments
Labels

Comments

@xiaofengqqcom123
Copy link

参考:阮一峰异步编程

@sisterAn
Copy link
Collaborator

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

@inJs
Copy link

inJs commented Mar 7, 2019

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

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

@Zousdie
Copy link

Zousdie commented Mar 7, 2019

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

@duanzheng
Copy link

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

@yygmind yygmind changed the title 第十二题 第 12 题:JS 异步解决方案的发展历程以及优缺点。 Apr 26, 2019
@Randysheng
Copy link

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

@michaelcai
Copy link

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

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

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

@arixse
Copy link

arixse commented Jul 10, 2019

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

@NuoHui
Copy link

NuoHui commented Jul 22, 2019

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

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

@lk3407105
Copy link

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

@252860883
Copy link

@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 

@eightHundreds
Copy link

@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

@kt3721
Copy link

kt3721 commented Sep 4, 2019

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

@xiaoxixi6633
Copy link

xiaoxixi6633 commented Sep 27, 2019

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

@tywei90
Copy link

tywei90 commented Nov 2, 2019

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

@yinzuowen
Copy link

yinzuowen commented Dec 2, 2019

@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

@yygmind yygmind added the 异步 label Dec 16, 2019
@No3ming
Copy link

No3ming commented Jan 10, 2020

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

@wojiaowanyuxuan
Copy link

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

@womenbudon
Copy link

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

@wfymalong
Copy link

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)

}

@jackchang2015
Copy link

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

@xsfxtsxxr
Copy link

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

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

@xsfxtsxxr
Copy link

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

@dunhuang
Copy link

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

@microJ
Copy link

microJ commented Apr 26, 2021

@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

@ietwangwei
Copy link

Rxjs

@Yangfan2016
Copy link

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

@wever-liang
Copy link

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

@Yangfan2016
Copy link

@Yangfan2016
Copy link

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests