第三课:异步编程核心:Callback、Promise与Async/Await

Node.js 是一个基于事件驱动的非阻塞 I/O 模型,这使得它非常适合处理高并发的网络请求。在 Node.js 中,异步编程是一项非常重要的技能。理解和掌握异步编程的不同方式不仅能提高代码的效率,还能让你更好地应对复杂的开发任务。本文将深入探讨 Node.js 中常见的三种异步编程方式:回调函数(Callback)、Promise 和 async/await。通过比较它们的用法和特点,我们能够选择最适合的方式来处理异步任务,并解决其中可能遇到的问题。

1. 单线程与事件循环机制解析

1.1 单线程模型

Node.js 采用单线程模型,这意味着它一次只能执行一个任务。这种设计简化了多线程环境下的复杂问题,如资源竞争和死锁等。然而,单线程也意味着 Node.js 无法利用多核 CPU 的优势。为了解决这个问题,Node.js 使用了事件驱动和非阻塞 I/O 模型。

1.2 事件循环机制

事件循环是 Node.js 处理异步操作的关键机制。它允许 Node.js 在等待 I/O 操作完成的同时继续执行其他任务。事件循环的工作流程大致如下:

  1.  执行同步代码:执行栈中的同步代码。
  2. 处理异步任务:将异步任务(如 I/O 操作、定时器等)放入相应的队列(宏任务队列或微任务队列)。
  3. 执行微任务:在当前执行栈为空时,执行微任务队列中的所有任务。
  4. 执行宏任务:执行一个宏任务,然后再次清空微任务队列,如此循环。
console.log('Start');
setTimeout(() => {
    console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
    console.log('Promise');
});
process.nextTick(() => {
    console.log('nextTick');
});
console.log('End');
 
// 输出顺序: Start End nextTick Promise setTimeout

2. 回调地狱问题与解决方案

2.1 回调地狱问题

回调函数是最早在 JavaScript 中用于处理异步操作的一种方式。然而,当需要处理多个异步操作时,回调函数可能会导致“回调地狱”(callback hell)。多个嵌套的回调函数会使代码层级深、逻辑混乱,难以维护。

const fs = require('fs')
fs.readFile('file1.txt', 'utf8', (err, data1) => {
    if (err) throw err;
    fs.readFile('file2.txt', 'utf8', (err, data2) => {
        if (err) throw err;
        fs.readFile('file3.txt', 'utf8', (err, data3) => {
            if (err) throw err;
            console.log(data1, data2, data3);
        });
    });
});
2.2 解决方案
2.2.1 使用Promise

Promise 是为了解决回调地狱问题而引入的。它表示一个异步操作的最终完成(或失败)及其结果值。通过链式调用 .then() 方法,可以清晰地组织异步代码。

const fs = require('fs').promises;
 
fs.readFile('file1.txt', 'utf8')
    .then((data1) => {
        return fs.readFile('file2.txt', 'utf8');
    })
    .then((data2) => {
        return fs.readFile('file3.txt', 'utf8');
    })
    .then((data3) => {
        console.log(data1, data2, data3);
    })
    .catch((err) => {
        console.error('Error:', err);
    });
2.2.2 使用async/await

async/await 是基于 Promise 的语法糖,它让异步代码看起来更像同步代码,从而提高可读性。

const fs = require('fs').promises;
 
async function readFiles() {
    try {
        const data1 = await fs.readFile('file1.txt', 'utf8');
        const data2 = await fs.readFile('file2.txt', 'utf8');
        const data3 = await fs.readFile('file3.txt', 'utf8');
        console.log(data1, data2, data3);
    } catch (err) {
        console.error('Error:', err);
    }
}

readFiles();

3. Promise链式调用与错误处理

3.1 Promise链式调用

Promise 允许我们进行链式调用,每一个 .then() 方法都会返回一个新的 Promise,这样就可以继续处理异步操作。

function getDataFromAPI() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
        const data = { id: 1, name: 'Alice' };
        resolve(data);
    }, 500);
  });
}
 
function processData(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
        const processedData = { ...data, role: 'Admin' };
        resolve(processedData);
    }, 500);
  });
}
 
function saveData(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('Data saved:', data);
        resolve();
    }, 500);
  });
}
 
getDataFromAPI()
    .then(data => processData(data))
    .then(processedData => saveData(processedData))
    .catch(error => console.error('Error:', error));
3.2 Promise错误处理

Promise 使用 .catch() 来捕获任何在 .then() 中发生的错误。

Promise.resolve()
    .then(() => {
        throw new Error('Something went wrong');
    })
    .then(() => {
        console.log('This will not be executed');
    })
    .catch((err) => {
        console.error('Error caught:', err);
    });

4. Async/Await语法糖实战

4.1 async/await基本用法

async/await 是基于 Promise 的语法糖,它让异步代码看起来更像同步代码。async 修饰函数,表示该函数会返回一个 Promise,而 await 用于等待一个 Promise 完成。

const fs = require('fs').promises;
 
async function readFiles() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    const data2 = await fs.readFile('file2.txt', 'utf8');
    console.log('All files content:', data1, data2);
  } catch (err) {
    console.error('Error:', err);
  }
}
 
readFiles();
4.2 并行与串行执行

使用 Promise.all() 可以并行执行多个异步操作,而使用 await 则会串行执行。

const fs = require('fs').promises;
 
async function readFiles() {
  try {
    const [data1, data2] = await Promise.all([
      fs.readFile('file1.txt', 'utf8'),
      fs.readFile('file2.txt', 'utf8')
    ]);
    console.log('All files content:', data1, data2);
  } catch (err) {
    console.error('Error:', err);
  }
}
 
readFiles();

串行执行示例:

const fs = require('fs').promises;
 
async function readFiles() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    const data2 = await fs.readFile('file2.txt', 'utf8');
    console.log('All files content:', data1, data2);
  } catch (err) {
    console.error('Error:', err);
  }
}
 
readFiles();

总结

Node.js 提供了多种处理异步操作的方式,包括回调函数、Promise 和 async/await。每种方式都有其适用场景和优缺点:

  • 回调函数:简单直接,但容易导致回调地狱,难以维护。
  • Promise:提供了链式调用和错误处理机制,解决了回调地狱的问题,但仍有一定的复杂性。
  • async/await:基于 Promise,简洁、易读,是现代 JavaScript 的推荐写法。

根据不同的需求和代码结构,你可以选择最适合的异步编程方式来提高代码的可读性、可维护性和性能。

关注我!! 持续为你带来Python爬虫相关内容。

你可能感兴趣的:(Nodejs,node.js,javascript)