用户工具

站点工具


javascript:第八章事件循环

第八章 事件循环与执行机制

本章目标

  • 深入理解JavaScript的单线程模型
  • 掌握调用栈(Call Stack)的工作原理
  • 理解任务队列(Task Queue)和微任务队列(Microtask Queue)
  • 掌握事件循环(Event Loop)的执行机制
  • 学会分析异步代码的执行顺序

8.1 JavaScript的单线程模型

8.1.1 为什么是单线程

JavaScript是一门单线程编程语言,这意味着它在同一时间只能执行一个任务。这种设计有其历史原因和实际考量:

历史原因:

  • JavaScript最初被设计用于浏览器中的表单验证和简单的DOM操作
  • 如果多线程同时操作DOM,会导致复杂的同步问题

实际优势:

  • 代码逻辑更简单,无需处理线程安全问题
  • 避免了死锁、竞态条件等多线程常见问题
  • 开发者可以更专注于业务逻辑
// 单线程示例
console.log('A');
console.log('B');
console.log('C');
// 输出必然是 A → B → C,顺序确定

8.1.2 异步的必要性

尽管JavaScript是单线程的,但它需要处理各种I/O操作(网络请求、文件读写、定时器等)。如果这些操作是同步的,会阻塞主线程,导致页面卡顿。

// 如果这是同步的,会阻塞3秒钟
const result = syncNetworkRequest(); // 阻塞3秒
console.log('请求完成');
// 在这3秒内,页面无法响应用户操作
 
// 异步方式不会阻塞
asyncNetworkRequest(function(result) {
    console.log('请求完成');
});
console.log('继续执行其他代码');

8.2 调用栈(Call Stack)

8.2.1 什么是调用栈

调用栈是一种数据结构,用于记录程序执行的位置。当一个函数被调用时,它会压入栈顶;当函数执行完毕后,它会从栈顶弹出。

function first() {
    console.log('进入first函数');
    second();
    console.log('离开first函数');
}
 
function second() {
    console.log('进入second函数');
    third();
    console.log('离开second函数');
}
 
function third() {
    console.log('进入third函数');
    console.log('离开third函数');
}
 
first();
 
// 调用栈变化:
// 1. first() 入栈
// 2. second() 入栈
// 3. third() 入栈
// 4. third() 出栈
// 5. second() 出栈
// 6. first() 出栈

8.2.2 栈溢出(Stack Overflow)

如果调用栈中的函数调用层数过多(通常是无限递归),会导致栈溢出错误。

// 错误的递归示例
function infiniteRecursion() {
    infiniteRecursion();
}
 
// infiniteRecursion(); // RangeError: Maximum call stack size exceeded
 
// 正确的递归应该有终止条件
function factorial(n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
 
console.log(factorial(5)); // 120

8.3 任务队列(Task Queue)

8.3.1 宏任务(Macrotask)

宏任务是JavaScript中异步操作的一种类型,主要包括:

  • setTimeout / setInterval
  • setImmediate(Node.js环境)
  • I/O操作
  • UI渲染
  • MessageChannel
console.log('1');
 
setTimeout(function() {
    console.log('2');
}, 0);
 
console.log('3');
 
// 输出顺序:1 → 3 → 2
// 即使setTimeout的延迟为0,回调函数也是异步执行的

8.3.2 微任务(Microtask)

微任务是另一种异步操作类型,优先级高于宏任务,主要包括:

  • Promise.then() / catch() / finally()
  • MutationObserver
  • queueMicrotask()
  • process.nextTick(Node.js环境)
console.log('1');
 
setTimeout(function() {
    console.log('2(宏任务)');
}, 0);
 
Promise.resolve().then(function() {
    console.log('3(微任务)');
});
 
console.log('4');
 
// 输出顺序:1 → 4 → 3(微任务) → 2(宏任务)

8.4 事件循环(Event Loop)

8.4.1 事件循环的工作流程

事件循环是JavaScript异步执行的核心机制,其工作流程如下:

1. 执行同步代码,清空调用栈
2. 执行所有微任务队列中的任务
3. 如果有必要,进行UI渲染
4. 从宏任务队列中取出一个任务执行
5. 重复步骤2-4
console.log('同步代码1');
 
setTimeout(function() {
    console.log('setTimeout 1');
    Promise.resolve().then(function() {
        console.log('Promise 2');
    });
}, 0);
 
Promise.resolve().then(function() {
    console.log('Promise 1');
    setTimeout(function() {
        console.log('setTimeout 2');
    }, 0);
});
 
console.log('同步代码2');
 
// 输出顺序分析:
// 同步代码1
// 同步代码2
// Promise 1(微任务)
// setTimeout 1(宏任务)
// Promise 2(微任务,在setTimeout回调中产生)
// setTimeout 2(宏任务)

8.4.2 事件循环的可视化

我们可以将事件循环想象成一个无限循环:

while (true) {
    // 1. 执行同步代码
    executeSyncCode();
    
    // 2. 清空微任务队列
    while (microtaskQueue.length > 0) {
        executeMicrotask();
    }
    
    // 3. 渲染UI(如果需要)
    renderUI();
    
    // 4. 执行一个宏任务
    if (macrotaskQueue.length > 0) {
        executeMacrotask();
    }
}

8.5 异步代码执行顺序分析

8.5.1 基础案例分析

案例1:基本执行顺序

console.log('A');
 
setTimeout(function() {
    console.log('B');
}, 0);
 
Promise.resolve().then(function() {
    console.log('C');
});
 
console.log('D');
 
// 答案:A → D → C → B
// 解释:
// 1. 同步代码:A、D
// 2. 微任务:C
// 3. 宏任务:B

案例2:嵌套异步

console.log('1');
 
setTimeout(function() {
    console.log('2');
    Promise.resolve().then(function() {
        console.log('3');
    });
}, 0);
 
Promise.resolve().then(function() {
    console.log('4');
    setTimeout(function() {
        console.log('5');
    }, 0);
});
 
console.log('6');
 
// 答案:1 → 6 → 4 → 2 → 3 → 5

案例3:复杂的异步组合

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
 
async function async2() {
    console.log('async2');
}
 
console.log('script start');
 
setTimeout(function() {
    console.log('setTimeout');
}, 0);
 
async1();
 
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
 
console.log('script end');
 
// 答案:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

8.5.2 async/await的执行分析

async/await是Promise的语法糖,理解其执行机制对掌握事件循环很重要。

async function test() {
    console.log('A');
    await Promise.resolve();
    console.log('B');
}
 
console.log('C');
test();
console.log('D');
 
// 输出:C → A → D → B
// await 之前的代码是同步执行的
// await 之后的代码相当于放入了微任务队列

8.6 实际应用场景

8.6.1 保证DOM更新后执行代码

// 错误的方式
function updateUI() {
    element.style.width = '100px';
    console.log(element.offsetWidth); // 可能获取到旧值
}
 
// 正确的方式:使用requestAnimationFrame或setTimeout
function updateUI() {
    element.style.width = '100px';
    requestAnimationFrame(function() {
        console.log(element.offsetWidth); // 获取到新值
    });
}

8.6.2 批量处理与性能优化

// 将大量计算分批处理,避免阻塞主线程
function processLargeArray(array, chunkSize) {
    let index = 0;
 
    function processChunk() {
        const chunk = array.slice(index, index + chunkSize);
        // 处理当前批次
        chunk.forEach(function(item) {
            // 处理item
        });
 
        index += chunkSize;
 
        if (index < array.length) {
            // 使用setTimeout让出主线程
            setTimeout(processChunk, 0);
        }
    }
 
    processChunk();
}
 
// 使用
const largeArray = new Array(100000).fill(0);
processLargeArray(largeArray, 1000);

8.6.3 实现nextTick

// 简单的nextTick实现
function nextTick(callback) {
    if (typeof Promise !== 'undefined') {
        Promise.resolve().then(callback);
    } else if (typeof MutationObserver !== 'undefined') {
        const observer = new MutationObserver(callback);
        const textNode = document.createTextNode('1');
        observer.observe(textNode, { characterData: true });
        textNode.data = '2';
    } else {
        setTimeout(callback, 0);
    }
}
 
// 使用
console.log('1');
nextTick(function() {
    console.log('2');
});
console.log('3');
// 输出:1 → 3 → 2

8.7 浏览器与Node.js的差异

8.7.1 浏览器中的事件循环

浏览器中的事件循环主要处理:

  • 用户交互事件
  • 定时器
  • 网络请求回调
  • Promise回调

8.7.2 Node.js中的事件循环

Node.js的事件循环分为6个阶段:

1. **timers**:执行setTimeout和setInterval的回调
2. **I/O callbacks**:执行延迟到下一个循环迭代的I/O回调
3. **idle, prepare**:内部使用
4. **poll**:检索新的I/O事件
5. **check**:执行setImmediate回调
6. **close callbacks**:执行close事件的回调
// Node.js特有示例
setTimeout(function() {
    console.log('timeout');
}, 0);
 
setImmediate(function() {
    console.log('immediate');
});
 
// 输出顺序不固定,取决于当前事件循环的状态

本章习题

基础练习

练习1:执行顺序预测 预测以下代码的输出顺序:

console.log('1');
 
setTimeout(function() {
    console.log('2');
}, 0);
 
Promise.resolve().then(function() {
    console.log('3');
});
 
console.log('4');

练习2:复杂执行顺序 预测以下代码的输出:

setTimeout(function() {
    console.log('A');
}, 0);
 
Promise.resolve().then(function() {
    console.log('B');
    setTimeout(function() {
        console.log('C');
    }, 0);
});
 
Promise.resolve().then(function() {
    console.log('D');
});
 
console.log('E');

练习3:async/await分析 分析以下代码的输出:

async function foo() {
    console.log('foo start');
    await bar();
    console.log('foo end');
}
 
async function bar() {
    console.log('bar');
}
 
console.log('script start');
foo();
console.log('script end');

进阶练习

练习4:实现sleep函数 使用Promise实现一个sleep函数,延迟指定时间后执行后续代码。

练习5:任务调度器 实现一个任务调度器,支持添加任务、设置优先级、控制并发数。

class TaskScheduler {
    constructor(concurrency) {
        // 实现
    }
 
    add(task, priority) {
        // 实现
    }
}

练习6:微任务与宏任务的转换 解释以下代码的输出,并说明为什么:

Promise.resolve().then(function() {
    console.log('Promise 1');
    setTimeout(function() {
        console.log('setTimeout 1');
    }, 0);
});
 
setTimeout(function() {
    console.log('setTimeout 2');
    Promise.resolve().then(function() {
        console.log('Promise 2');
    });
}, 0);

思考题

1. 为什么微任务的优先级高于宏任务?这种设计有什么好处?
2. async/await中的await关键字做了什么?它与Promise有什么关系?
3. 在浏览器中,如果微任务队列无限增长会发生什么?如何避免?
4. 解释requestAnimationFrame在事件循环中的位置。

参考答案

练习1答案: 输出顺序:1 → 4 → 3 → 2

练习2答案: 输出顺序:E → B → D → A → C

练习3答案: 输出顺序:script start → foo start → bar → script end → foo end

练习4答案:

function sleep(ms) {
    return new Promise(function(resolve) {
        setTimeout(resolve, ms);
    });
}
 
// 使用
async function example() {
    console.log('开始');
    await sleep(1000);
    console.log('1秒后');
}

练习6答案: 输出顺序:Promise 1 → setTimeout 2 → Promise 2 → setTimeout 1

小结

本章我们深入学习了JavaScript的事件循环机制:

  • 单线程模型:JavaScript是单线程语言,通过异步机制避免阻塞
  • 调用栈:用于跟踪函数调用,后进先出(LIFO)
  • 任务队列:分为宏任务队列和微任务队列
  • 事件循环:不断从队列中取出任务执行的机制
  • 执行优先级:同步代码 > 微任务 > 宏任务

理解事件循环对于编写高效、正确的异步代码至关重要。在下一章中,我们将学习JavaScript的面向对象编程。

javascript/第八章事件循环.txt · 最后更改: 127.0.0.1