发布于 2026-01-06 0 阅读
0

JavaScript 中的异步编程

JavaScript 中的异步编程


你是否在 JavaScript 代码中使用过回调函数、Promise 或最新的 async/await?你是否觉得它们难以理解?你是否曾经好奇过它们底层的工作原理……?那么,让我们一起来掌握它们吧。

引言

对于初学 JavaScript 的人来说,JavaScript 中的异步编程可能会令人困惑,有时甚至经验丰富的程序员也会感到吃力。至少我以前也不了解它的底层原理。我们知道 JavaScript 是单线程的,这意味着它一次只能执行一个任务,这与其他多线程编程语言(例如 Java 和 C#)不同。那么,当我们想从 API 获取数据或在后端执行一些异步数据库操作时该怎么办呢?这时,回调、Promise 或 async/await 就派上用场了。我们不想阻塞 JavaScript 的主线程,但又希望在异步操作完成后收到通知,这就是异步编程概念的用武之地。让我们来看看它们以及它们的演变历程……

异步 JavaScript 的演变

*回调函数
* Promise
* Async/await

回调函数

回调函数就是作为参数传递的函数,你希望在某些操作完成后调用它们。

      function add(x,y,callback){
            const sum = x+y;
            callback(sum);
       };

       add(2,3,function(sum){
       console.log('sum',sum); //sum 5
       });

这很简单,我们只需要传入一个要在异步操作完成后执行的函数即可。但是,这种方法的主要问题是,当我们想要进行多个异步调用并且必须一个接一个地执行时……它就会引入所谓的“回调地狱”。类似于下面的代码:

getData(function(a){
    getMoreData(a, function(b){
        getMoreData(b, function(c){ 
            getMoreData(c, function(d){ 
                getMoreData(d, function(e){ 
                    ...
                });
            });
        });
    });
});

由于每个异步调用都依赖于前一次调用获取的数据,因此它必须等待前一次调用完成。这种方法虽然可行,但调试和维护起来非常困难。让我们来看看 Promise 是如何解决这个问题的。

承诺

替代文字

Promise 是在 ES6 中引入的,它解决了回调函数的一些问题。每个 Promise 构造函数都接受一个带有两个参数的函数resolvereject如果resolvePromise 成功解析,则调用该函数;如果 Promise 被拒绝或发生任何错误,则调用该函数。

      const promise = new Promise(function(resolve, reject) { 
             // an API call or any async operation 
      }); 

这里,函数参数 `a`resolvereject`b` 本身也是函数,并且被正确地调用。我们来看一个例子:

const promise = new Promise(function(resolve, reject)  {
     setTimeout(() => {
      resolve("Time is out");
     }, 4000);
   });

promise
.then(function(data){console.log(data)})
.catch(function(error){console.log('Something bad happened: ',error)})

Promise 就是一个对象,它执行任何异步操作,并根据传递给其回调函数的参数调用 resolve 或 reject 函数。
在上面的setTimeout例子中,我们创建了一个新的 Promise 对象,并将其赋值给一个变量,该变量包含一个带有 resolve 和 reject 参数的回调函数。其内部执行过程如下:

1.第一个Promise尝试执行回调函数内部的代码,即:
setTimeout

2. 4 秒后,当setTimeout操作完成后,它会尝试解析,
即调用 resolve 函数。

3.resolve我们作为回调函数参数传递的值将绑定
Promise类中的另一个函数,我们称之为 `function`
onResolved。因此,当在resolve`function` 内部调用 `function`时,它会使用传递给 `function` 的值来调用类中的` setTimeoutfunction` 。这里它是一个字符串。onResolvedPromiseresolveTime is out

4.onResolved函数内部会调用你传递的回调函数,.then()
并将接收到的值传递给它resolve;同样,它也会处理拒绝的情况

5. 这是 Promise 内部运作的简化版本,如果您
要链接多个 Promise,情况就会变得稍微
复杂一些……Promise类维护一个回调数组,这些回调会
按照语句的顺序依次调用.then()
。如果您想深入了解,请参阅这篇文章。

因此,使用 Promise 链,你不需要将一个调用嵌套在另一个调用中,你可以将它们一个接一个地链接起来。

假设你想执行两个异步操作,并且想使用一个 Promise 返回的数据来执行另一个异步调用,我们可以使用 Promise 做类似这样的事情:

  const promise1 =new Promise(function(resolve,reject){
     // async work
  })

  const promise2 = function(datafromFirst){
    return new Promise(function(resolve,reject){
     // async work
   })
  }

  promise1
  .then(function(data1){console.log(data1); return promise2(data1) })
  .then(function(data2){console.log(data2); })
  .catch(function(error){console.log(error);//error caught from any of 
  the promises})

这使得代码更易读、更易理解……但 Promise 链式调用却让代码变得复杂。由于前一个 Promise 必须返回另一个 Promise 才能进行链式调用,调试也变得异常困难……诚然,Promise 让编写异步代码变得更加容易,避免了回调地狱,但我们还能做得更好吗?当然可以!使用 async 和 await 绝对没问题……

异步等待

ES8 的新特性底层async-await使用了相同的promises机制,但它消除了传递回调函数和处理 Promise 链式调用的需要。它提供了更高的抽象级别,代码也变得更加简洁。

 async function func(){
    try{
    const result = await someasynccall();
    console.log(result);
    }
   catch(error){
     console.log(error); 
   } 
}

我们需要使用 `await` 关键字async将函数声明为异步函数,只有这样才能await在函数内部使用 `await` 关键字。我们可以将try-catch`await` 代码包裹起来,以便在抛出错误时能够捕获它。

让我们来看前面两个异步调用的例子,我们需要从第一个调用中获取数据,以便使用 async/await 语法进行另一个异步调用。

  async function func(){
      try{
       const data1 = await someasyncall();
       const data2 = await anotherasynccall(data1);
       console.log(data2);
      }
     catch(error){
     console.log(error); 
   }
  }

这样看起来更简洁,至少写起来更容易……

假设我们想从异步函数返回一些内容,并且之后想使用它,那么我们需要使用立即执行函数表达式(IIFE)模式。

你认为以下代码会console.log(message)记录什么内容?

async function func(){
    try{
    const result = await someasynccall();
    console.log('result',result);
    return 'successful';
    }
   catch(error){
     console.log(error); 
     return 'failed';
   } 
}

const message = func();
console.log(message) 

将会console.log(message)打印出结果Promise{<pending>},但不会打印出实际的“成功”或“失败”,因为我们的console.log函数在内部的 Promiseawait someasynccall()执行完成之前运行。所以,如果我们想要实际使用message值,则需要使用 IIFE(立即调用函数表达式),如下所示:

async function func(){
    try{
    const result = await someasynccall();
    console.log('result',result);
    return 'successful';
    }
   catch(error){
     console.log(error);
     return 'failed'; 
   } 
}

(async function(){
 const message = await func();
 console.log(message);
})();

因此,我们使用另一个立即调用的异步函数,await让该函数返回消息字符串,然后使用它。

这就是我们处理异步代码的方式,经过多年的发展async-await,现在最新的代码看起来更简洁、更易读。

文章来源:https://dev.to/reddyaravind178/asynchronous-programming-in-javascript-5gpf