JavaScript中的异步编程的发展

308 阅读4分钟

JavaScirpt本身是单线程编程。单线程编程意味着,一次只能完成一个任务。如果有多个任务,必须等待前一个任务完成,才会继续执行下一个任务。

但是,有的任务并不能同步立即知晓结果,比如Ajax请求,所以我们希望开启任务A后,可以先去干其他的事情,等任务A的结果出来,我们再根据A的结果进行处理,这种不阻塞其他任务,异步处理结果的方式就是异步编程,最初的异步编程使用回调函数实现,比如XMLHttpRequest。

尽管可以发送同步Ajax请求。尽管技术上说是这样,但是,在任何情况下都不应该使用这种方式,因为它会锁定浏览器UI(按钮、菜单、滚动条等),并阻塞所有的用户交互。这是一个可怕的想法,一定要避免。

换个角度说,异步编程的程序中,会同时包含现在运行部分将来运行部分

当程序中有很多回调时,代码会变得非常复杂,而且调试也会很麻烦。因此在ES2015标准中新增了Promise特性,用来帮助处理异步代码而不涉及使用回调。在ES2017标准中,又新增了async/await语法,使得异步编程更加简单。

阶段一:回调函数

XHLHttpRequest请求的流程如下,在状态改变的时候调用回调函数,实现了逻辑处理。

const xmlhttp = new XMLHttpRequest();// 创建对象
xmlhttp.open('GET',server_url,true); // 规定请求的类型
xmlhttp.send();// 发送到服务器
xmlhttp.onreadystatechange = function(){ // 监听状态改变的事件
    if(xmlhttp.readyState == 4 && xmlhttp.status == 200){
        console.log("请求返回的结果",xmlhttp.responseText)
    }
}

阶段二:优雅的Promise

Promise是ES2015标准中提供的一种处理异步代码的方式。Promise本质上是一个对象,使用new Promise()构造函数可以创建该对象。在构造中需要传入一个函数,该函数的参数是resolve函数reject函数,会分别在成功时和失败时调用该函数。

从这里可以看出,Promise内部封装了状态的监听逻辑,在变成功的时候会自动调用resolve函数,状态变失败的时候会调用reject函数。


const p = new Promise((resolve, reject) => {
  try {
    resolve('成功');
  } catch (error) {
    reject(error);
  }
});

p.then(
  (resolve) => {
    console.log('成功回调', resolve);
  },
  (reject) => {
    console.log('失败回调', reject);
  }
);

Promise的内部实现如下图所示:

1.png

Promise会有三种状态:

  1. pending(进行中,默认状态),表示异步操作还未完成
  2. fulfilled(已完成),表示异步操作已完成,且是成功的
  3. rejected(已拒绝),表示异步操作失败,返回了一个错误信息。

在实际的业务场景中,可能会出现需要依次请求a、b、c接口的情况, 代码可能就变成这样:

const p = new Promise((resolve, reject) => {
  try {
    resolve('成功');
  } catch (error) {
    reject(error);
  }
});

p.then(
  (res) => {
    console.log('成功回调1', res);
    a().then(res1 => {
      console.log('a接口的成功回调', res1);
      b().then(res2 => {
        console.log('b接口的成功回调', res2);
        c().then(res3 => {
          console.log('c接口的成功回调', res3);
        }, reason3 => {
          console.log('c接口的失败回调', reason3);
        })
      }, reason2 => {
        console.log('b接口的失败回调', reason2);
      })
    }, reason1 => {
      console.log('a接口的失败回调', reason1);
    })
  },
  (reason) => {
    console.log('失败回调1', reason);
  }
)

这样的代码风格分析起来还是蛮费劲。

阶段三:更优雅的async/await

因为Promise的链式调用本身还是蛮复杂,所以ES2017又增加了async/await的设计,使得异步的代码看起来是同步的,更加简洁。 同样顺序请求a、b、c接口, 代码可以这么写

async function test () {
  try {
    await a();
    await b();
    await c();
    
  } catch (error) {
    console.log('报错',error)
  }
}

async函数内部也是利用了Promise,所以在任何函数前面加上async关键字,就意味着函数会返回Promise,即使代码中没有显式返回Promise。

Snipaste_2024-04-28_13-57-19.png

总结

JavaScript的异步编程发展的主体思路是往更加优雅,方便理解代码逻辑的语法糖发展。

本质上都是通过回调函数去实现,但是Promise内部把正常\报错调用不同状态的处理逻辑做了封装,所以我们才可以不同情况的回调是什么。

async/await又提供了同步的代码语法实现异步的操作,降低了心智负担。

这整个变化的过程,没有脱离回调函数的处理本质,但是更好理解了。