同步和异步

5 阅读5分钟

在前端开发里,异步是一种关键的编程模式,用于处理可能耗时较长的操作,避免阻塞主线程,从而提升应用程序的响应能力和性能。下面从异步产生的原因、实现方式、应用场景和注意事项几个方面详细介绍:

异步产生的原因

JavaScript 是单线程的,这意味着同一时间只能执行一个任务。如果执行一个耗时的操作(如网络请求、文件读取)采用同步方式,后续代码会被阻塞,页面会出现卡顿,影响用户体验。而异步操作可以让程序在等待耗时操作完成的同时继续执行其他任务。

异步的实现方式

回调函数

回调函数是最基本的异步处理方式,将一个函数作为参数传递给另一个函数,当异步操作完成后调用该回调函数。

function fetchData(callback) {
    setTimeout(() => {
        const data = { message: '数据已获取' };
        callback(data);
    }, 1000);
}

fetchData((result) => {
    console.log(result.message); 
});

缺点:回调地狱问题,代码嵌套层次过深,难以维护和调试。

Promise

Promise 是一种更优雅的异步处理方式,避免了回调地狱。它有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = true;
            if (success) {
                const data = { message: '数据已获取' };
                resolve(data);
            } else {
                reject(new Error('数据获取失败'));
            }
        }, 1000);
    });
}

fetchData()
  .then((result) => {
        console.log(result.message); 
    })
  .catch((error) => {
        console.error(error); 
    });

优点:链式调用使代码更清晰,便于错误处理。

async/await

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

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = true;
            if (success) {
                const data = { message: '数据已获取' };
                resolve(data);
            } else {
                reject(new Error('数据获取失败'));
            }
        }, 1000);
    });
}

async function main() {
    try {
        const result = await fetchData();
        console.log(result.message); 
    } catch (error) {
        console.error(error); 
    }
}

main();

优点:代码结构更简洁,易于理解和调试。

异步的应用场景

  • 网络请求:如使用 fetchaxios 进行 API 调用,避免页面在等待响应时卡顿。
  • 定时器setTimeoutsetInterval 用于实现延迟执行或周期性执行任务。
  • 文件读取:在浏览器或 Node.js 中读取文件时,使用异步方式避免阻塞主线程。

异步编程的注意事项

  • 错误处理:在异步操作中,错误处理尤为重要。使用 try...catch 块捕获 async/await 中的错误,使用 .catch() 方法处理 Promise 中的错误。
  • 执行顺序:异步操作的执行顺序可能与代码编写顺序不同,需要注意控制异步操作的执行顺序。

代码问题分析1

console.log(1);
setTimeout(function() {
    console.log(2);
}, 0);
new Promise(function(resolve) {
    console.log(3);
    // 调用 resolve 方法,将 Promise 状态变为 resolved
    resolve();
}).then(function() {
    console.log(4);
});
console.log(5);

代码执行流程

  1. 同步代码执行

    • console.log(1) 立即执行,输出 1
    • setTimeout 是异步函数,其回调函数被放入宏任务队列,等待当前调用栈清空后执行。
    • new Promise 的构造函数同步执行,console.log(3) 输出 3,然后调用 resolve() 方法将 Promise 状态变为 resolved
    • console.log(5) 立即执行,输出 5
  2. 微任务执行

    • Promise.then 回调函数被放入微任务队列。在同步代码执行完毕后,JavaScript 引擎会优先处理微任务队列,因此 console.log(4) 执行,输出 4
  3. 宏任务执行

    • 最后,JavaScript 引擎处理宏任务队列,执行 setTimeout 的回调函数,输出 2

最终输出顺序

1
3
5
4
2

代码问题分析2

console.log(1);

setTimeout(function() {
    console.log(2);
}, 0);

new Promise(function(resolve, reject) {
    console.log(3);
    // 拒绝 Promise
    reject(new Error('Promise 被拒绝')); 
})
  .then(function() {
        console.log(4);
    })
  .catch(function(error) {
        // 处理 Promise 被拒绝的情况
        console.error('捕获到错误:', error.message); 
    });

console.log(5);

代码执行流程解释

  1. 同步代码执行

    • console.log(1) 是同步代码,会立即执行,输出 1
    • setTimeout 是异步函数,其回调函数会被放入宏任务队列,等待当前调用栈清空后执行。尽管延迟时间设置为 0 毫秒,它还是会在同步代码之后执行。
    • new Promise 的构造函数是同步执行的,其中的 console.log(3) 会立即执行,输出 3。接着调用 reject() 方法,将 Promise 的状态变为已拒绝。
    • console.log(5) 是同步代码,会立即执行,输出 5
  2. 微任务和错误处理

    • 由于 Promise 被拒绝,.then() 方法中的回调函数不会执行。因为 .then() 方法只处理 Promise 成功(fulfilled)的情况。同时,由于没有使用 .catch() 方法来捕获拒绝错误,会在控制台抛出一个未处理的拒绝错误。
  3. 宏任务执行

    • 最后,JavaScript 引擎会检查宏任务队列,执行其中的任务。setTimeout 的回调函数会被执行,输出 2

输出顺序

1
3
5
捕获到错误: Promise 被拒绝
2

代码问题分析3

async function async1() {

console.log('async1 start')

await async2()

// await后面的代码可以理解为promise.then(function(){ 回调执行的 })

console.log('async1 end')

}

async function async2() {

console.log('async2')

}

console.log('script start')

setTimeout(function() {

console.log('setTimeout')

}, 0)

async1()

console.log('script end')

1. 同步代码执行

  • console.log('script start'):这是同步代码,会立即执行,输出 script start
  • setTimeout:这是一个异步函数,它的回调函数会被放入宏任务队列,等待当前调用栈清空后执行,所以不会立即输出 setTimeout
  • async1():调用 async1 函数,async1 函数内部开始执行。
    • console.log('async1 start'):同步代码,立即执行,输出 async1 start
    • await async2():调用 async2 函数。
      • console.log('async2')async2 函数内部的同步代码,立即执行,输出 async2await 会暂停 async1 函数后续代码的执行,将 async1 函数剩余代码放入微任务队列。
  • console.log('script end'):同步代码,立即执行,输出 script end

2. 微任务执行

此时同步代码执行完毕,JavaScript 引擎会先检查微任务队列。async1 函数中 await 后面的代码 console.log('async1 end') 在微任务队列中,所以会执行该代码,输出 async1 end

3. 宏任务执行

微任务队列清空后,JavaScript 引擎会检查宏任务队列。setTimeout 的回调函数在宏任务队列中,所以会执行该回调函数,输出 setTimeout

输出顺序

script start
async1 start
async2
script end
async1 end
setTimeout