你了解JavaScript的异步发展史吗?

为什么需要异步?

因为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 = 1console.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实例对象的,构造函数中接受一个回调函数作为参数,该回调函数中也有两个参数resolvereject注意:这两个参数也是两个函数!

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 关键字等待 foobar 函数的返回结果。由于 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

目前看来async/await是消灭异步回调的终极武器!!!

谢谢各位小伙伴愿意花宝贵的时间阅读这篇文章,如果有帮到你的话,请点点赞吧!

相关推荐
一路向前的月光2 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   2 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web2 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
Jiaberrr3 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
安冬的码畜日常5 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ5 小时前
html+css+js实现step进度条效果
javascript·css·html
john_hjy6 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd6 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
yanlele6 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范