为什么需要异步?
因为js是单线程的编程语言,同一时间只执行一个任务,当有一段耗时较长的计算代码或ajax请求出现,会出现用户等待时间过长的情况,此时当前任务还未完成,导致其他的操作也都会滞留,这是低效率的。
那js为什么不设计成多线程?
这就和创造js这门编程语言有关了。JS最初是作为一种用于网页前端交互的脚本语言而设计的,它的主要任务是操作DOM、响应用户交互等。在早期,JS并没有涉及到需要处理大量并发任务的场景,因此为了语言的轻量和简单,单线程设计是合理的选择。
多线程编程很容易导致共享资源的竞争和数据同步问题,这会增加代码的复杂性和出错的可能性,而单线程模型则简化了这些问题。比如JS没有多线程语言中锁、解锁的过程,这样节约上下文切换的时间。
异步是什么?
异步是一种编程模式,它允许程序在执行某个任务的同时,不必等待该任务完成就能继续执行其他任务。比如:
javascript
复制代码
setTimeout(function foo(){ console.log('这就是异步!'); }, 1000); console.log('异步是什么');
这里的setTimeout就是一个异步任务,由于 JS 是单线程执行的,当遇到 setTimeout
这样的异步操作时,它会被放到事件队列中,等待当前执行栈清空后才会执行。因此,console.log('异步是什么');
会立即执行并输出 '异步是什么'
,而 function foo(){...}
则会在 1 秒后执行,输出 '这就是异步'
。所以JS才不会和傻子一样等1000ms执行回调函数,而是先执行之后的代码。
异步的发展史👏
1. 回调函数callback
回调函数是一种常见的处理异步操作。它实质上是一个函数,作为参数传递给另一个函数,并在特定事件发生或异步操作完成后被调用。
比如我有个同步操作(foo)和一个异步操作(bar)
scss
复制代码
let count = 0 function foo() { console.log(count); } function bar () { setTimeout(() => { count = 1 },1000) } bar() foo() // 输出:0
按照JS单线程来说,同步操作一定在异步之前,这样代码只会输出0
,而我们想将count = 1
在console.log(count);
之前执行能怎么样呢?
scss
复制代码
let count = 0 function foo() { console.log(count); } function bar (callBack) { setTimeout(() => { count = 1 callBack() },1000) } bar(foo) // 输出:1
我们可以采用回调的方式处理,在函数 bar()
中,它接受一个回调函数 callBack
作为参数。在1000ms后,会将 count
的值设为 1,然后调用传递进来的回调函数 callBack()
,此时的callBack()
也正是foo()
综合起来,当程序执行到 bar(foo)
时,它会等待1秒,然后将 count
的值修改为 1,并调用 foo()
函数。此时 foo()
函数会输出 count
的值,即 1
。
优点:
- 解决了同步问题
缺点:
- 回调地狱(多层级嵌套)
- 不能捕获错误
- 嵌套多层之后,代码可读性差
2. Promise
在ES6中,官方打造了Promise来解决异步、回调地狱等问题。它能更加优雅地书写复杂的异步任务
Promise对象具有这些特点:
-
对象的状态不受外界影响。
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 -
一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。这两个特点取自阮一峰ES6入门(es6.ruanyifeng.com/#docs/promi…)
场景
javascript
复制代码
function a () { return new Promise((resolve, reject) => { setTimeout(() => { console.log('a'); resolve('ok') },1000) }) } function b () { setTimeout(() => { console.log('b'); },500) } a().then( (res) => { // res = 'ok console.log(res); b() }, (err) => { console.log(err); } ) //输出:a ok b
从场景中出现的new
,不难看出Promise
对象其实就是一个构造函数,是用来生成一个Promise
实例对象的,构造函数中接受一个回调函数作为参数,该回调函数中也有两个参数resolve
和reject
,注意:这两个参数也是两个函数!
Promise实例生成以后,可以用then方法
分别指定resolved状态
和rejected状态
的回调函数。
优点:
- 通过链式调用,避免了深度嵌套的回调函数,使得代码更加清晰易读。
- Promise实例之间可以轻松组合和复用,使得代码更加模块化和灵活。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果
缺点:
- Promise对象一旦创建后,其状态就不可再改变。这意味着无法取消或者终止Promise实例的执行。
- 虽然Promise内置了错误处理机制,但有时错误处理可能不够直观,特别是在处理多个并发Promise时,可能会出现不易定位和调试的问题。
- 当处于 pending 状态时,无法得知目前进展到哪一个阶段
3. Generator
Generator是 ES6 中引入的一种特殊的函数类型,它可以在执行过程中暂停并且可以从暂停的位置恢复执行。Generator 函数通过使用 function*
关键字来定义,其中可以包含零个或多个 yield
关键字,用于暂停函数的执行并向调用者返回一个值。
场景
javascript
复制代码
function* g() { var o = 1 yield o++ yield o++ yield o++ } let gen = g() // 迭代对象 console.log(gen.next()); // { value: 1, done: false } console.log(gen.next()); // { value: 2, done: false } console.log(gen.next()); // { value: 3, done: false } console.log(gen.next()); // { value: undefined, done: true }
Generator 函数在调用时并不立即执行,而是返回一个迭代器对象,该迭代器对象包含了内部状态的指针,用于控制 Generator 函数的执行。 下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。即:每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。
优点
- 可以分段执行,可以暂停
- 可以控制每个阶段的返回值
- 可以知道是否执行完毕
- 帮助打造了
async await
缺点
- Generator函数的执行流程更加复杂,需要手动调用
next()
方法来控制执行流程,会让代码不够直观、显得繁琐。 - 最好借助 Thunk 和 co 模块 处理异步
4. async/await
async/await 是 ES7中引入的异步编程解决方案,它是基于 Promise 的语法糖。
async/await 和 promise的关系
async/await
可以替代Promise链式调用(.then()
)的方式,使得异步操作的代码更加清晰和易读。async/await
结合try-catch语句,代替了 Promise的 catch。与Promise链式调用相比,错误处理更加直观和统一。- 执行 async 函数,返回的是 Promsie 对象
场景
javascript
复制代码
function foo(num){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(num*10) },1000) }) } function bar(num){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(num*100) },2000) }) } async function test(){ let res1 = await foo(1) let res2 = await bar(10) console.log(res1,res2); } test() // 输出:10 1000
这个场景中foo
函数放回一个Promise
对象,在指定的时间后解析Promise,并返回传入的参数 num
乘以 10 的结果,bar
函数同理。test
函数在执行时使用 await
关键字等待 foo
和 bar
函数的返回结果。由于 await
关键字会使异步操作变为同步执行,所以耗时为3000ms。
从这个场景我们可以得出
async
表示这是一个async
函数,await
只能用在async
函数里面,不能单独使用async
返回的是一个Promise
对象await
等待的是一个Promise
对象,后面必须跟一个Promise
对象await Ywis
相当于是Ywis.then
,并且只是成功态的then
优点
async/await
更加直观和易读。它让异步操作的代码看起来更像是同步的,使得代码更加清晰和易于理解。async/await
是由promise
+generator
来实现的,本质是在generator
的基础上通过递归的方式来自动执行一个又一个的next
函数,当done为true时结束递归。async/await
是建立在Promise之上的,它使用Promise来管理异步操作。await
关键字会等待其后的异步操作完成后再继续执行后续的代码。
缺点
- 在使用多个
await
时,异步操作会变为串行执行,这可能导致性能瓶颈。 - 没有错误捕获机制
结尾
所以JS的异步发展史,可以认为是从 callback -> promise -> generator -> async/await