JavaScript 闭包与事件轮询机制
·
一、事件轮询机制(Event Loop):JS 异步执行的核心
JavaScript 是单线程语言,同一时间只能执行一个任务。为解决单线程下 “耗时任务阻塞后续代码” 的问题,JS 设计了 事件轮询机制,通过 “同步任务优先执行、异步任务排队等待” 的规则,实现非阻塞的代码执行。
1. 核心概念铺垫
在理解事件轮询前,需先明确以下基础概念:
| 概念 | 定义 | 关键说明 |
|---|---|---|
| 进程 | 系统中正在运行的应用程序(如浏览器、Node.js) | 一个进程可包含多个线程,每个线程共享进程的内存空间 |
| 线程 | 进程内的执行路径,是 CPU 调度的基本单位 | JS 主线程负责执行同步代码,另有专门线程处理异步任务(如定时器、网络请求) |
| 单线程 | 同一时间只能执行一个任务,任务需排队 | JS 设计为单线程的核心原因:避免多线程操作 DOM 时的冲突(如一个线程创建 DOM,一个线程删除 DOM) |
| 任务队列 | 存放待执行的异步任务的队列 | 分为 “宏任务队列” 和 “微任务队列”,优先级不同 |
2. 事件轮询的执行流程
事件轮询的本质是 “主线程与任务队列的循环交互”,具体步骤如下:
- 执行同步任务:主线程优先执行所有同步代码,直到同步任务执行完毕(主线程为空)。
- 处理异步任务:
- 异步任务(如
setTimeout、Promise.then)触发时,先进入 Event Table 注册函数(记录 “任务完成后需执行的回调”)。 - 当异步任务的触发条件满足(如定时器到期、Promise 状态改变),Event Table 会将回调函数移入对应的 任务队列(宏任务队列 / 微任务队列)。
- 异步任务(如
- 清空微任务队列:主线程为空后,先检查 微任务队列,将队列中所有微任务依次执行完毕(微任务执行过程中产生的新微任务,也会在本轮清空)。
- 执行一个宏任务:微任务队列清空后,从 宏任务队列 中取出第一个宏任务,放入主线程执行。
- 循环重复:重复步骤 1-4,形成 “事件循环”。
3. 宏任务与微任务:异步任务的优先级
异步任务分为 宏任务 和 微任务,二者的执行优先级有明确区别:微任务优先级 > 宏任务优先级(同一轮事件循环中,微任务先于宏任务执行)。
3.1 宏任务(Macro Task)
- 定义:由 “宿主环境”(浏览器 / Node.js)触发的异步任务,执行优先级较低。
- 常见类型:
setTimeout、setInterval(定时器)- DOM 事件(如
click、load) - 网络请求(如
Ajax、fetch) script标签(整个脚本的执行属于第一个宏任务)
3.2 微任务(Micro Task)
- 定义:由 “JS 引擎” 触发的异步任务,执行优先级较高。
- 常见类型:
Promise.then()、Promise.catch()、Promise.finally()(注意:new Promise(...)内部的代码是同步的,仅回调函数是微任务)async/await(await后面的代码会被封装为微任务)queueMicrotask()(专门用于添加微任务的 API)
3.3 执行顺序案例(关键!)
通过案例理解执行顺序:同步代码 → 所有微任务 → 一个宏任务
console.log(1); // 同步代码:1
// 宏任务:setTimeout 回调
setTimeout(function() {
console.log(2); // 最后执行(宏任务优先级低)
}, 0);
// Promise:new Promise 内部是同步代码,then 回调是微任务
let pro = new Promise((resolve, reject) => {
console.log(3); // 同步代码:2
resolve(1000); // 标记 Promise 为成功状态
console.log(4); // 同步代码:3
});
// 微任务:Promise.then 回调
pro.then(data => {
console.log(data); // 微任务:4(同步代码后执行)
});
console.log(5); // 同步代码:5
// 最终输出顺序:1 → 3 → 4 → 5 → 1000 → 2
二、闭包:延长变量生命周期的核心技巧
1. 闭包的引出:经典定时器问题
先看一个常见的 “坑”:使用 var 循环创建定时器,期望输出 0-4,实际输出 5 个 5。
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // 输出:5 5 5 5 5(而非 0-4)
});
}
console.log('a'); // 输出:a(同步代码优先)
问题原因:
var声明的变量 没有块级作用域,循环中的i是全局变量,循环结束后i的值固定为 5。setTimeout是异步宏任务,回调函数会在 “同步代码(循环 + console.log ('a'))执行完毕后” 才执行,此时i已变为 5。
2. 闭包的定义与特性
2.1 定义
闭包(Closure)是能够访问另一个函数作用域中变量的函数。简单来说:当一个内层函数被外层函数返回并在外部调用时,内层函数依然能访问外层函数的局部变量,这就是闭包。
2.2 三大核心特性
- 函数嵌套函数:闭包一定存在函数嵌套结构(内层函数是闭包)。
- 内层函数引用外层函数的变量 / 参数:这是闭包的核心目的 ——“跨作用域访问变量”。
- 外层函数的变量不会被垃圾回收:由于内层函数引用了外层变量,JS 引擎会保留这些变量在内存中(延长变量生命周期),直到闭包不再被引用。
3. 闭包的实现与应用
3.1 解决 “定时器循环问题”(闭包方案)
通过 “自执行函数” 创建闭包,为每次循环的 i 生成独立的作用域,保存 i 的当前值:
for (var i = 0; i < 5; i++) {
// 自执行函数:立即执行,将当前 i 作为参数传入,生成独立作用域
(function(i) {
setTimeout(function () {
console.log(i); // 输出:0 1 2 3 4(闭包保存了每次循环的 i 值)
});
})(i); // 传入当前循环的 i 值
}
console.log('a'); // 输出:a(同步代码优先)
简化方案:用 let 替代 var
let 有块级作用域,每次循环会创建独立的 i,本质也是利用了闭包特性:
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // 输出:0 1 2 3 4(let 块级作用域替代闭包)
});
}
3.2 闭包的核心作用:延长变量作用域
闭包可以让 “全局函数” 访问 “函数内部的局部变量”,且局部变量不会被回收:
function person(name) {
// 外层函数的局部变量:name
var name = name;
// 内层函数(闭包):引用外层变量 name
function sayName() {
console.log(name);
}
return sayName; // 返回闭包
}
// 全局变量 fun 接收闭包
var fun = person('王小明');
fun(); // 输出:王小明(全局函数 fun 访问到 person 内部的 name)
3.3 解决 “DOM 点击事件下标问题”
需求:给 5 个 li 绑定点击事件,点击时输出当前 li 的下标(用 var 会导致所有点击输出 5,用闭包解决):
html 预览
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
var oli = document.getElementsByTagName('li');
// 闭包方案:自执行函数保存每次循环的 i
for (var i = 0; i < oli.length; i++) {
oli[i].onclick = (function(i) {
// 返回点击回调函数(闭包),保存当前 i
return function() {
console.log(i); // 点击第 n 个 li,输出 n-1(0-4)
}
})(i);
}
</script>
4. 闭包的注意事项
- 内存消耗问题:由于闭包会保留外层函数的变量在内存中,滥用闭包会导致内存占用过高,甚至内存泄漏(如 IE 浏览器)。
- 解决方案:在闭包不再使用时,手动将引用的变量设为
null,释放内存:function outer() { var num = 1; function inner() { console.log(num); } return inner; } var clo = outer(); clo(); // 1 clo = null; // 手动释放闭包引用,让 num 可被垃圾回收
三、自执行函数:创建临时作用域的工具
1. 定义与作用
自执行函数(Immediately Invoked Function Expression,IIFE)是定义后立即执行的函数,核心作用是:
- 创建临时作用域,避免变量污染全局(早期没有
let/const时,常用 IIFE 隔离作用域)。 - 为闭包传递参数(如前面的定时器案例)。
2. 常见语法形式
自执行函数需满足 “函数表达式” 的语法(避免被解析为函数声明),常见写法如下:
// 1. 括号包裹函数表达式(最常用)
(function() { console.log('自执行1'); })();
(function() { console.log('自执行2'); }());
// 2. 一元运算符开头(利用运算符将函数转为表达式)
!function() { console.log('自执行3'); }();
+function() { console.log('自执行4'); }();
-function() { console.log('自执行5'); }();
~function() { console.log('自执行6'); }();
// 3. void 关键字开头
void function() { console.log('自执行7'); }();
// 4. new 关键字开头
new function() { console.log('自执行8'); }();
注意事项:
- 若上一行代码未用分号
;结束,自执行函数的括号可能被解析为 “函数调用”,导致语法错误。因此建议在 IIFE 前加;:// 安全写法:前加分号 ;(function() { console.log('安全自执行'); })(); - 自执行函数通常是匿名的(后续不再调用),也可命名,但命名仅在函数内部有效。
四、核心知识点总结
- 事件轮询机制:JS 单线程的异步解决方案,执行顺序为 “同步代码 → 所有微任务 → 一个宏任务”,循环往复。
- 宏任务 vs 微任务:微任务(如
Promise.then)优先级高于宏任务(如setTimeout),同一轮事件循环先清空微任务。 - 闭包:能访问外层函数变量的内层函数,核心作用是 “跨作用域访问变量” 和 “延长变量生命周期”,需注意内存消耗问题。
- 自执行函数:定义后立即执行,用于创建临时作用域,常配合闭包传递参数,避免全局变量污染。
更多推荐
所有评论(0)