最近开始不断学习实践 JavaScript ,出于性能测试的敏感,首先研究了 JavaScript 的异步编程实践,目前看跟之前学过的 Java 和 Go 都不太一样。使用的语法上相对更加复杂,也可能因为我实践得还太少。

异步编程

异步编程是一种在程序执行过程中,不阻塞主线程的任务处理方式。相较于同步编程,异步编程允许程序在等待某个任务(例如网络请求或文件读写)完成的同时,继续执行其他操作。这种方式极大提高了程序的效率,尤其在处理大量 I/O 操作时,表现尤为突出。

在 JavaScript 中,异步编程尤为重要,原因在于它的单线程特性。JavaScript 在浏览器环境下运行时,只有一个主线程负责执行代码。如果程序中某个任务需要较长时间才能完成(比如网络请求、数据库查询等),而没有采用异步处理方式,主线程将会被阻塞,导致整个页面的交互变得迟缓甚至无响应。

为了避免这种情况,JavaScript 提供了多种异步处理方式,如回调函数Promiseasync/await等。通过这些机制,JavaScript 能够在处理耗时任务时,不阻塞主线程,保持页面的流畅性和响应性。

回调函数(Callback)

回调函数是指作为参数传递给另一个函数,并在该函数执行完毕后调用的函数。在 JavaScript 的异步编程中,回调函数是最早且最基础的实现方式之一。当某个异步操作(如网络请求或定时器)完成时,JavaScript 运行时环境会调用提供的回调函数,继续执行后续的逻辑。这种模式允许我们在异步任务完成后进行处理,而不阻塞主线程。

在 JavaScript 中,回调函数通过结合浏览器或 Node.js 的事件循环机制来实现异步行为。单单使用 JavaScript 本身无法实现异步,而是通过将任务交给浏览器或 Node.js 的运行时(如定时器、I/O 操作、网络请求等)来处理,等这些任务完成后,再通过回调函数把结果传回给 JavaScript 主线程继续执行。这整个过程就是实现异步的关键。

示例

function fetchData(callback) {  
    console.log("Fetching data...");  
    setTimeout(() => {// 模拟耗时操作  
        // 模拟耗时操作  
        const data = "Hello FunTester!";  
        callback(data); // 调用回调函数,传递数据  
    }, 1000);  
}  

function processData(data) {  
    console.log("Data received:", data);// 处理数据的逻辑  
}  

console.info("start --------");  
fetchData(processData);// 调用 fetchData 函数,传入回调函数  
console.info("end --------");

在这个例子中,fetchData 模拟了一个异步数据获取的操作,执行完成后调用回调函数 processData 来处理获取到的数据。控制台打印:

start --------
Fetching data...
end --------
Data received: Hello FunTester!

Promise

Promise 是一种用于处理异步操作的对象,它代表了一个异步操作的最终完成(或失败)及其结果值。Promise 提供了更清晰、更可读的方式来管理多个异步操作,特别是当它们相互依赖时。相比回调函数,Promise 能避免回调地狱,使代码结构更加扁平化和易于维护。

Promise 的三种状态

  1. Pending(待定):初始状态,表示异步操作尚未完成,也没有结果。
  2. Fulfilled(已完成):异步操作成功完成,并返回一个值。
  3. Rejected(已拒绝):异步操作失败,并返回一个原因。

在 Promise 的生命周期中,它只能从 pending 状态转移到 fulfilledrejected 状态,并且一旦状态发生变化,结果就不会再改变。

Promise 的基本用法

一个 Promise 对象会接受一个函数作为参数,该函数本身又有两个参数:resolvereject。当异步操作成功时,调用 resolve 来将 Promise 状态改为 fulfilled;当操作失败时,调用 reject 将状态改为 rejected

.then().catch() 的用法

Promise 示例

const fetchData = async () => {  
    return new Promise((resolve, reject) => {  
        setTimeout(() => {  
            const success = true; // 模拟成功或失败  
            if (success) {// 成功, 调用 resolve, 并传递数据  
                resolve("hello, FunTester");  
            } else {// 失败, 调用 reject, 并传递错误信息  
                reject("sorry, error occurred");  
            }  
        });  
    });  
};  

console.info("start--------------");  
// 使用 Promise 来处理异步操作  
fetchData()  
    .then((data) => {// 成功时的处理  
        console.log("received:", data);  
        return data.length;  
    })  
    .then((age) => {// 成功时的处理  
        console.log("length:", age);  
    })  
    .catch((error) => {// 失败时的处理  
        console.error("Error:", error);  
    });  

console.info("end--------------");

控制台打印:

start--------------
end--------------
received: hello, FunTester
length: 16

下面是一些小小的 tips:

async/await

async/await 是 ES2017(ES8)引入的语法,提供了一种更加简洁的方式来处理异步代码,使其看起来更像同步代码。这种方式可以减少 Promise 链的复杂性,提升代码的可读性和维护性。async/await 实际上是基于 Promises 之上的语法糖。

通过使用 asyncawait,我们可以编写看起来像同步执行的代码,但它在内部仍然是异步的。这种写法可以避免 Promise 链式调用的繁琐结构,将异步任务的执行流顺序化,逻辑更加清晰。

async/await 的基本用法

  1. async 函数:声明一个 async 函数,它会自动返回一个 Promise。即使没有显式返回 Promise,async 函数默认会把返回值封装成一个 Promise。
  2. await 表达式:用于等待一个 Promise 的结果。它暂停 async 函数的执行,直到 Promise 解决(resolve)或拒绝(reject),然后继续执行后续代码。

示例:使用 async/await

const fetchData = () => {  
    return new Promise((resolve, reject) => {  
        setTimeout(() => {// 模拟异步操作  
            const success = true;  
            if (success) {  
                resolve("hello funtester");  
            } else {  
                reject("sorry, error");  
            }  
        }, 1000);  
    });  
};  

async function getData() {  
    try {  
        const data = await fetchData(); // 等待 Promise 解决  
        console.log("Data received:", data);  
    } catch (error) {  
        console.error("Error:", error); // 处理错误  
    }  
}  
console.info("start----------------");  
let data = getData();  
console.log("data:", data);  
console.info("end----------------");

控制台打印:

start----------------
data: Promise { <pending> }
end----------------
Data received: hello funtester

在使用 async/await 时,错误处理的方式类似于同步代码中捕获异常。我们可以使用 try/catch 块来处理异步操作中的异常,这种方式比在每个 .then() 末尾添加 .catch() 更加直观。

async/await 基于 Promise

需要注意的是,async/await 并不是替代 Promise 的机制,而是基于 Promise 进行的更高级抽象。它们简化了 Promise 的链式调用,消除了嵌套结构。可以把 await 视作一个暂停点,等待 Promise 完成(无论是成功还是失败),使得异步代码的处理更符合程序的执行逻辑。

异步编程的重要性

在 JavaScript 中,异步编程至关重要,因为 JavaScript 运行在单线程环境中,尤其是在浏览器和 Node.js 等平台中。单线程意味着同一时间只能执行一个任务。如果代码中的某些操作(如网络请求、文件读取或定时任务)需要较长的执行时间,而 JavaScript 只支持同步编程的话,整个线程将被阻塞,用户界面会变得卡顿或无响应。因此,异步编程通过不阻塞主线程来解决这个问题,使得 JavaScript 能够高效处理 I/O 密集型任务,保持应用的流畅运行。

当然 JavaScript 异步编程的内容还有其他高级用法,比如学习 Promise.all、Promise.race 等,将来等我实践充足再来分享。

FunTester 原创精华


↙↙↙阅读原文可查看相关链接,并与作者交流