JavaScript异步编程与执行机制全面解析

一、从一道经典面试题说起

先看这段代码,你能准确说出输出结果吗?

async function async1 () {
    console.log('async1 start');
    await new Promise(resolve => {
        console.log('promise1')
    })
    console.log('async1 success');
    return 'async1 end'
}

console.log('script start')
async1().then(res => console.log(res))
console.log('script end')

正确答案是:

script start
async1 start
promise1
script end

为什么后面的代码不执行?让我们深入分析。

二、JavaScript执行机制核心概念

2.1 单线程与事件循环

JavaScript是单线程语言,但通过事件循环(Event Loop) 实现了非阻塞异步操作。

console.log('1'); // 同步任务

setTimeout(() => {
    console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
    console.log('3'); // 微任务
});

console.log('4'); // 同步任务

// 输出顺序:1 → 4 → 3 → 2

2.2 任务队列:微任务 vs 宏任务

任务类型 示例 执行时机
微任务 Promise.thenprocess.nextTickMutationObserver 当前同步代码执行完后立即执行
宏任务 setTimeoutsetIntervalsetImmediate、I/O操作 下一次事件循环开始时执行

三、Promise深度解析

3.1 Promise的三种状态

// 1. pending(进行中)
const p1 = new Promise(() => {});
console.log(p1); // Promise { <pending> }

// 2. fulfilled(已成功)
const p2 = Promise.resolve('success');
console.log(p2); // Promise { 'success' }

// 3. rejected(已失败)
const p3 = Promise.reject('error');
console.log(p3); // Promise { <rejected> 'error' }

3.2 Promise状态不可逆

const promise = new Promise((resolve, reject) => {
    resolve('第一次resolve'); // 状态变为fulfilled
    reject('错误'); // 被忽略
    resolve('第二次resolve'); // 被忽略
});

promise.then(console.log); // 输出:第一次resolve

3.3 为什么面试题中的代码不执行?

await new Promise(resolve => {
    console.log('promise1')
    // 没有调用resolve/reject!
})

关键原因:

  • await 会暂停async函数执行,等待后面的Promise解决
  • 如果Promise永远不resolve/reject,就永远保持pending状态
  • 导致await后面的代码被"冻结",永远不会执行

四、async/await底层原理

4.1 async函数的本质

// async函数实际上是Promise的语法糖
async function example() {
    return 'hello';
}

// 等价于
function example() {
    return Promise.resolve('hello');
}

4.2 await的执行机制

async function test() {
    const result = await somePromise;
    console.log(result);
}

// 相当于
function test() {
    return somePromise.then(result => {
        console.log(result);
    });
}

4.3 注意async函数本身不是异步任务,但它总是返回Promise,并且内部的await会引入异步行为。

async function example() {
    console.log('同步代码'); // 这是同步执行的
    await something;        // 从这里开始产生异步
    console.log('异步代码'); // 这是异步执行的
}

五、微任务与宏任务的执行顺序

5.1 完整的事件循环流程

  1. 执行同步代码
  2. 执行当前所有的微任务
  3. 执行一个宏任务
  4. 重复步骤2-3
console.log('1'); // 同步

setTimeout(() => {
    console.log('2'); // 宏任务
    Promise.resolve().then(() => {
        console.log('3'); // 微任务(在宏任务内部)
    });
}, 0);

Promise.resolve().then(() => {
    console.log('4'); // 微任务
    setTimeout(() => {
        console.log('5'); // 宏任务(在微任务内部)
    }, 0);
});

console.log('6'); // 同步

// 输出顺序:1 → 6 → 4 → 2 → 3 → 5

六、微任务穿插

微任务穿插特指:当Promise的.then()回调返回另一个Promise时,JavaScript引擎需要创建额外的微任务来解析这个返回的Promise,导致其他已经存在的微任务"插队"先执行的特殊现象。

情况1:返回普通值(正常链式调用)

Promise.resolve().then(() => {
    console.log('1');
    return '普通值';
}).then((res) => {
    console.log(res); // '普通值'
});

Promise.resolve().then(() => {
    console.log('3');
});

输出:1 → 3 → 普通值

这里只是正常的微任务队列调度,不是真正的穿插

情况2:返回Promise(真正的微任务穿插)

Promise.resolve().then(() => {
    console.log('1');
    return Promise.resolve('2'); // 返回Promise!
}).then((res) => {
    console.log(res); // '2'
});

Promise.resolve().then(() => {
    console.log('3');
});

输出:1 → 3 → 2

这里发生了真正的微任务穿插

为什么会产生穿插?底层机制:

返回普通值时:

return '普通值';
  • 直接传递值给下一个then
  • 只产生1个新的微任务(下一个then的回调)

返回Promise时:

return Promise.resolve('2');
  • 需要2个额外的微任务
    1. 微任务A:等待返回的Promise解析
    2. 微任务B:将解析后的值传递给下一个then
  • 这2个微任务被追加到当前微任务队列末尾

执行过程对比:

无穿插(返回普通值):

微任务队列: [callback1, callback3]
执行callback1 → 输出1,产生微任务callback2
微任务队列: [callback3, callback2]
执行callback3 → 输出3
执行callback2 → 输出'普通值'

有穿插(返回Promise):

微任务队列: [callback1, callback3]
执行callback1 → 输出1,产生2个额外微任务[A, B]
微任务队列: [callback3, 微任务A, 微任务B]
执行callback3 → 输出3 ← 这就是"穿插"!
执行微任务A → 等待Promise解析
执行微任务B → 输出2

关键识别特征:

会发生微任务穿插

  • return Promise.resolve()
  • return new Promise()
  • return asyncFunction()(async函数返回Promise)

不会发生微任务穿插

  • return 普通值
  • throw new Error()
  • 不return(相当于return undefined)

七、常见面试题分析

7.1 题目一:混合执行顺序

console.log('1');

setTimeout(() => {
    console.log('2');
}, 0);

Promise.resolve().then(() => {
    console.log('3');
});

console.log('4');

// 结果:1 → 4 → 3 → 2

7.2 题目二:嵌套Promise

Promise.resolve().then(() => {
    console.log('1');
    return Promise.resolve('2');
}).then((res) => {
    console.log(res);
});

Promise.resolve().then(() => {
    console.log('3');
});

// 结果:1 → 3 → 2

7.3 题目三:async/await与Promise混合!!

async function async1() {
    console.log('1'); //这里是同步
    await async2();//调用async2,产生微任务
    console.log('2');//这里是异步  微任务!!
    //await后面的代码相当是当promise请求成功以后的then函数
}

async function async2() {
    console.log('3');
}

console.log('4');

setTimeout(() => {
    console.log('5');
}, 0);

async1();

new Promise(resolve => {
    console.log('6');
    resolve();
}).then(() => {
    console.log('7');
});

console.log('8');

// 结果:4 → 1 → 3 → 6 → 8 → 2 → 7 → 5

八、实战建议与最佳实践

8.1 避免Promise内存泄漏

// 错误做法:Promise永远pending
const promise = new Promise(() => {});

// 正确做法:添加超时机制
function withTimeout(promise, timeout) {
    return Promise.race([
        promise,
        new Promise((_, reject) => 
            setTimeout(() => reject(new Error('Timeout')), timeout)
        )
    ]);
}

8.2 错误处理最佳实践

// 方式1:使用.catch()
asyncFunction()
    .then(result => { /* 处理结果 */ })
    .catch(error => { /* 处理错误 */ });

// 方式2:使用try/catch with async/await
async function main() {
    try {
        const result = await asyncFunction();
        // 处理结果
    } catch (error) {
        // 处理错误
    }
}

九、总结

JavaScript的异步编程机制看似复杂,但掌握核心原理后就能游刃有余:

  1. 理解事件循环:同步 → 微任务 → 宏任务的执行顺序
  2. 掌握Promise状态:pending、fulfilled、rejected三种状态及转换
  3. 理解async/await:本质是Promise的语法糖,await会暂停函数执行
  4. 注意内存泄漏:避免创建永远pending的Promise

希望本文能帮助你深入理解JavaScript的异步执行机制,在面试和实际开发中都能得心应手!

思考题: 你能解释为什么Promise.then是微任务而setTimeout是宏任务吗?欢迎在评论区讨论!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐