捋一下异步发展史

281 阅读12分钟

大家好我是阳阳羊,在最近的面试中,不少面试官叫我聊聊异步,异步到底是个啥,它的发展史到底是什么样的,这篇文章,咱来好好总结一下!

其实早在四个月前就已经写过异步相关文章了(面向小白编程:Promise 的浅入深出 - 掘金 (juejin.cn)),经过四个月的学习进步,或许可能有了更深更广的理解,所以这篇文章,我想再来巩固一下。本文旨在记录学习,如果对你有帮助那再好不过啦~

为什么要有异步

因为JS是单线程的编程语言,同一时间只能执行一个任务,如果有多个任务,需要“排队”,每个操作都是按照顺序一个接一个地执行的,如果此时遇到一个耗时的任务时,整个程序就需要等待它执行完毕,这样就会导致阻塞。

为了解决这一问题,JS设计师打造了同步(Synchronous)和异步(Asynchronous)这两个概念。与上面这种思路不同的是,异步编程中,当遇到耗时的任务时,不再需要等待,而是立即执行后面的同步代码,将这个耗时任务暂且挂载起来,等到同步代码执行完毕,再回过头来执行这个耗时任务。

那为什么要解决异步

有些时候我们又希望同步任务在异步任务执行完毕后再执行。比如当我们向后端拿数据然后渲染到页面上,如果我们没有良好的手段,将渲染页面的操作放在请求之后,就会导致页面渲染失败。

举个例子🌰:

const foo = () =>{
    console.log('2');
}
const bar = () =>{
    setTimeout(()=>{
        console.log('1');
    },1000)
}
bar();
foo();

现在我就想让函数foo()bar()执行完毕后再执行,有什么办法呢?

异步的发展史

1. 回调函数

最早解决异步的方式就是使用回调函数,回调函数大家都应该清楚,简单理解就是一个函数被作为参数传递给另一个函数。回调函数跟异步没有必然联系,只能说回调函数是解决异步的方法之一。

回调解决:

const foo = () =>{
    console.log('2');
}
const bar = (callback) =>{
    setTimeout(()=>{
        console.log('1');
        callback();
    },1000)
}
bar(foo);

函数bar()接受一个回调作为参数,并在setTimeout中先打印再执行该回调。

需要注意的是,回调虽简单,但当有多个异步操作需要串行执行或者有多个异步操作之间存在依赖关系时,嵌套使用回调函数会导致回调地狱,使得代码难以维护。

a(() => {
  b(() => {
    c(() => {
      d(() => {
        e(() => {
          f()
        })
      })
    })
  })
})

回调的优缺点

  • 优点:简单粗暴
  • 缺点:不利于维护,引发回调地狱

2. Promise

promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

详见阮一峰老师的ES6入门(Promise 对象 - ECMAScript 6入门 (ruanyifeng.com)

promise有三种状态:

  • 进行中 (Pending)
  • 已完成 (Fulfilled)
  • 已失败 (Rejected)

状态一经改变无法再次修改,也就保证了thencatch不可能同时触发

  • resolve 函数在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
  • reject 函数在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去

即成功的回调和失败回调。

Promise.prototype.then()

  1. 接收两个回调onFulfilledonRejected
  2. 默认返回个新的promise对象(不是原来那个),原来的promise实例的返回值将作为参数传入这个新promiseresolve函数
  3. then前面的promise状态为 fulfilled,回调onFulfilled直接执行,参数为promise resolve的结果
  4. then前面的promise状态为 rejected,回调onRejected直接执行,参数为promise reject的原因
  5. then前面的promise状态为pendingthen中的回调会缓存起来,等待promise状态改变后执行

因为then()方法返回的还是promise对象,所有支持链式调用。

根据 Promise/A+ 规范(Promises/A+ (promisesaplus.com)),then() 的链式调用的特点:

  • 首先then方法必须返回一个 promise 对象
  • 如果then方法中返回的是一个简单值(如Number等)就使用此值包装成一个新的Promise对象返回
  • 如果then方法中没有return语句,就返回一个用Undefined包装的Promise对象
  • 如果then方法中出现异常,不会在当前第二次回调中被捕获,而是流转到下一个then中被捕获
  • 如果then方法没有传入任何回调,则继续向下传递(即值穿透)
  • 如果then方法中返回了一个Promise对象,那就以这个对象为准,返回它的结果

Promise.prototype.catch()

  1. 接收一个回调 onRejected
  2. 默认返回一个新的promise对象,其状态为 fulfilled
  3. catch前面的promise状态为 rejected,则执行onRejected回调,参数为reject的原因
  4. catch前面的promise状态为 pending,catch 中的回调函数会被缓存起来,直到promise的状态变为rejected,然后执行回调

Promise.prototype.finally()

  1. 接收一个回调函数 onFinally,该回调没有参数,无论promise是 fulfilled 还是 rejected,都会执行该回调函数
  2. 默认返回一个新的promise对象,其状态和值与之前的promise对象一致
  3. finally() 方法适用于需要在promise结束时执行清理操作的情况,无论promise是成功还是失败,都会执行这个清理操作

Promise.resolve()

接收一个任意值,将现有对象转为状态为 fulfilled 的promise对象

const p = Promise.resolve(1);
Promise.resolve(p).then((val)=>{
  console.log(val); // 1
});

Promise.reject()

接收一个任意值,将现有对象转为状态为 rejected 的promise对象

const p = Promise.reject(1);
Promise.resolve(p).catch((res)=>{
  console.log(res); // 1
});

Promise.all()

接收的参数为一组promise实例组成的数组,当所有数组中所有promise对象状态都为 fulfilled 时,当前的promise对象的状态才为 fulfilled ,并以数组形式收集每一项的结果并传递;只要数组中有一个状态为 rejected ,当前promise对象的状态就为 rejected,并返回数组中第一个状态变更为 rejected 的原因。

简单来说,就是全部成功则成功,有一个失败就失败。

let p1 = Promise.resolve(1);
let p2 = Promise.resolve(2);
let p3 = Promise.resolve(3);

Promise.all([p1,p2,p3])
.then(val=>{
  console.log(val) // [1,2,3]
})

每一项状态都为 fulfilled ,才为 fulfilled ,结果为数组,包含了每一项的结果

let p1 = Promise.resolve(1);
let p2 = Promise.reject(2);
let p3 = Promise.reject(3);

Promise.all([p1,p2,p3])
.then(
    (val)=>{
        console.log(val)
    },
    (res)=>{
        console.log(res) // 2
    }
)

只要有一项状态都为 rejected ,则为 rejected ,结果为第一个 rejected 的原因

Promise.race()

all类似,也是接收的参数为一组promise实例组成的数组,但是不同的是all需要所有promise对象状态发生改变才执行,race则是:只要有一个实例的状态发生改变,当前promise的状态就跟着改变。

简单来说,race就是竞赛,比谁快,谁快就选谁,管你是牛还是马。

const p1 = new Promise((resolve, reject) => {
  setTimeout(()=>{
      resolve('1')
  },1000)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(()=>{
      reject('2')
  },500)
})
Promise.race([p1, p2])
.then(
    (val)=>{
        console.log(val)
    },
    (res) => {
        console.log(res) // 2
    }
)

只认准第一个状态改变的实例,不论状态变为 fulfilled 还是 rejected,都会跟着改变。

Promise.any()

race类似,接收的参数为一组promise实例组成的数组,但是不同的是race只需要一个实例的状态发生改变就改变,而any则是:只要有一个实例的状态发生改变,且为 fulfilled ,当前promise的状态就跟着变为 fulfilled;如果状态变为 rejected ,继续判断直到找到状态为 fulfilled 的实例。如果全部实例状态都为 rejected ,则为 rejected ,返回字符串AggregateError: All promises were rejected

简单来说,就是any就是任意的意思,只要任意一个为成功,则成功;全部失败则失败;特别一点的就是失败时候的原因是一串字符串。

let p1 = Promise.reject(1);
let p2 = Promise.reject(2);
Promise.any([p1, p2])
.then(
    (val) => {
        console.log(val)
    },
    (res) => {
        console.log(res) //AggregateError: All promises were rejected
    }
)

Promise 的优缺点

  • 优点:解决了回调地狱;catch()方法处理错误;支持链式调用then()
  • 缺点:一旦创建无法取消,浪费资源;错误处理不够灵活

3. Generator

Generator 函数是 ES6 提供的一种异步编程解决方案。

Generator 函数与普通函数不同之处

  1. function与函数名之间有个*
  2. Generator 函数调用得到一个生成器对象,具有可迭代属性,调用next()方法继续往后执行,碰到yield就暂停

Generator 函数执行过程

function* foo(){
    yield 'a'
    yield 'b'
    yield 'c'
    return 'ending'
}
let gen = foo()
console.log(gen);//Object [Generator] {}
console.log(gen.next());//{ value: 'a', done: false }
console.log(gen.next());//{ value: 'b', done: false }
console.log(gen.next());//{ value: 'c', done: false }
console.log(gen.next());//{ value: 'ending', done: true }
console.log(gen.next());//{ value: undefined, done: true }

value 表示该暂停点处返回的值;done 表示生成器函数是否已经执行完毕;当已经执行完毕再继续执行,value 值为undefined

Generator 函数next()传参

function* foo(a){
    const b = 2 * (yield(a-1))
    const c = yield(b / 4)
    return (a - b + c)
}
const gen = foo(2) //生成对象,将 a 赋为 2
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next(6)); // yield被赋值为6 // {value: 3, done: false}
console.log(gen.next(3)); // yield被赋值为3 // {value: -7, done: true}

需要注意的是next()会将携带的参数赋值给上一个yield表达式

Generator 函数解决异步

function* g() {
    yield a();
    yield b();
}
function a(){
    return new Promise((resolve, reject) =>{
        setTimeout(()=>{
            console.log('a完成');
            resolve()
        },1000)
    })
}
function b(){
    return new Promise((resolve, reject) =>{
        setTimeout(()=>{
            console.log('b完成');
            resolve()
        },500)
    })
}
const gen = g();
const p1 = gen.next().value;
p1.then(res => {
    const p2 = gen.next(res).value;
    p2.then(res => {
        gen.next(res);
    });
});

我们利用 Generator 的暂停特性以及 Promise 的then()方法解决异步

😅😅😅😅😅 ????????????????????

不是!我用 Generator 函数是来给自己找麻烦来的?用了 Generator 结果你说还要用 Promise ,那我为什么不一开始就用 Promise 呢?而且还要我手动next(),这里还好只有两个任务,那要是复杂起来阁下我又该如何应对?

co 模块

co 模块是一个用于处理生成器函数与异步操作结合的库。它的核心思想是利用生成器函数的暂停和恢复执行的特性,以及 yield 关键字的能力,简化异步编程。

co模块允许我们传入一个生成器函数,它会自动执行该生成器函数,并处理其中的异步操作,直到生成器函数执行完毕为止。

const co = require('co');
function a(){
    return new Promise((resolve, reject) =>{
        setTimeout(()=>{
            console.log('a完成');
            resolve()
        },1000)
    })
}
function b(){
    return new Promise((resolve, reject) =>{
        setTimeout(()=>{
            console.log('b完成');
            resolve()
        },500)
    })
}
function* g(){
    yield a()
    yield b()
}
co(g)

co 模块通过递归调用生成器函数的 next 方法,并将生成器函数返回的 Promise 对象进行处理,以实现异步操作的执行和结果的处理。

co模块源码(github.com)

Generator 函数的优缺点

  • 优点:可以分段执行,可以暂停;可以控制每个阶段的返回值;可以知道是否执行完毕;借助 co 模块处理异步
  • 缺点:语法复杂,学习成本高;使用迭代器执行,调试困难

4. async/await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数是什么?一句话,它就是 Generator 函数的语法糖。

async 用于声明一个 function 是异步的,await 用于等待一个异步方法执行完成

async

async function test() {
  return "this is async"
}
const res = test()
console.log(res) // Promise {<resolved>: "this is async"}

async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 PromIse.resolve() 封装成Promise对象返回

await

  • await 只能在 async 函数中使用
  • await 后面不是Promise对象,直接执行
  • await 后面是Promise对象会阻塞后面的代码,Promise对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果

async/await 优缺点

  • 优点:代码结构清晰,十分优雅
  • 缺点:没有错误捕获机制,只能使用 try/catch;滥用await会导致性能问题

将三者放一起比较

回到开头那个demo,看看三者分别如何实现:

const foo = () =>{
    console.log('2');
}
const bar = () =>{
    setTimeout(()=>{
        console.log('1');
    },1000)
}
bar();
foo();

Promise 实现

const foo = () => {
    console.log('2');
}
const bar = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('1');
            resolve();
        }, 1000);
    });
}
bar().then(() => {
    foo();
});

Generator 函数实现

const foo = () =>{
    console.log('2');
}
const bar = () =>{
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('1');
            resolve();
        }, 1000);
    });
}
function* g() {
    yield bar()
    yield foo()
}
const gen = g();
gen.next().value.then(() => {
    gen.next();
});

async/await 实现

const foo = () => {
    console.log('2');
}
const bar = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('1');
            resolve();
        }, 1000);
    });
}
async function baz(){
    await bar();
    foo();
}
baz();

小结

  • promiseasync/await是专门用于处理异步操作的
  • Generator并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator接口...)
  • promise编写代码相比Generatorasync更为复杂化,且可读性也稍差
  • Generatorasync需要与promise对象搭配处理异步情况
  • async实质是Generator的语法糖,相当于会自动执行Generator函数
  • async使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方案

参考

ES6 入门教程 - ECMAScript 6入门 (ruanyifeng.com)

Promises/A+ (promisesaplus.com)

「硬核JS」深入了解异步解决方案 - 掘金 (juejin.cn)

面试官:你是怎么理解ES6中 Generator的?使用场景? | web前端面试 - 面试官系列 (vue3js.cn)

最后

水平有限,欢迎指错,春招加油!码字不易,三连鼓励~

已将学习代码上传至 github,欢迎大家学习指正!

技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 “点赞 收藏+关注” ,感谢支持!!