世界上最高效的学习方法,是把你学到的知识,通过自己理解的方式,教会另一个人。共勉
1. Event Loop
很多人都听说过 JS 是单线程的,但是线程是什么?进程又是什么?它们之间有什么关系?
从本质上说,这两个名词都是关于 CPU 时间片的一个描述。进程描述了 CPU 在运行指令、加载及保存上下文所需时间。线程则是进程中更小的一个单位,描述执行一段指令所需时间。 这个概念放到浏览器上说,打开一个 Tab 页是一个进程,其中的 UI 渲染、JS 代码执行、DOM 加载、Http 请求等都是一个个线程;放到 JS 代码执行上来说,一个函数是一个进程,而函数内变量的定义、其他函数的调用等任务都是一个个线程。
JS 执行是单线程的,意味着 JS 在执行代码的时候一次只能处理一个任务,必须按队列顺序逐个执行。JS 的主要功效是处理前端交互,其中就包括操作 DOM 节点。试想若 JS 是多线程,在处理网页交互时,一个线程需要删除 DOM 节点,另一个线程却是要操作同一个 DOM 节点,这样该如何判断先执行哪个线程?但若队列中存在多个任务,上一个任务的执行会阻塞下一个任务,导致代码执行效率低下。就像 AJAX 请求线程,发出请求后需要等待响应结果,期间 CPU 却是空闲的。对此,JS的事件循环机制(Event Loop)很好地解决了问题。
2. 同步任务和异步任务
JavaScript 将任务分为两种:同步任务和异步任务。
-
同步任务:执行完后能立即得出结果的任务。同步任务在主线程中执行,在执行过程中产生堆栈,堆中存储复杂数据类型(Object),栈中存储基本数据类型(String、Number、Boolean、Null、Undefined、Symbol)。
-
异步任务:执行后无法立即得出结果,需要等待一段时间获得相应的任务。其中又分为宏任务
(Macrotask)和微任务(Microtask)。
宏任务:主程序Script、setTimeout、setInterval、setImmediate、I/O操作(mouse click、keypress、network event)、UI渲染、requestAnimationTrame等。
微任务:promise、MutationObserver、process.nextTick()、mutation、Object.oberse等。
3. 主线程、执行栈和队列
4. Event Loop
- 把同步任务推入主线程,异步任务推入任务队列;
- 把主线程中的同步任务推入执行栈,开始执行;
- 执行栈为空后,读取任务队列中的微任务,推入执行栈;
- 循环执行微任务队列,直到微任务列表为空;
- 读取宏任务列表中的第一条宏任务,推入执行栈;
- 这时的宏任务又相当于一个线程,重复上述步骤1-5,直到任务队列为空。
实例:
const a = 1;
const arr = [1, 2, 3];
console.log('1');
console.log(a, arr);
setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
});
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5');
});
}, 100);
new Promise(function (resolve) {
console.log('6');
resolve();
}).then(function () {
console.log('7');
});
process.nextTick(function () {
console.log('8');
setImmediate(() => {
console.info('9');
});
new Promise(function (resolve) {
console.log('10');
resolve();
}).then(function () {
console.log('11');
});
setTimeout(function () {
console.log('12');
setImmediate(() => {
console.info('13');
});
process.nextTick(function () {
console.log('14');
});
new Promise(function (resolve) {
console.log('15');
resolve();
}).then(function () {
console.log('16');
});
}, 100);
process.nextTick(function () {
console.log('17');
});
});
执行结果:1 1,[1,2,3] 6 7 8 10 17 11 9 2 4 5 3 12 15 16 13 14
解析:
上述代码就相当于一个进程,里面的函数执行即为一个个线程,一个个任务。
- 把同步任务推入主线程,异步任务推入任务队列;
- 把主线程中的同步任务推入执行栈,开始执行;得出结果1 1,[1,2,3]
- 执行栈为空后,读取任务队列中的微任务,推入执行栈;得出结果6 7
- 循环执行微任务队列,直到微任务列表为空;得到结果 8 10 17 11 9
- 读取宏任务列表中的第一条宏任务,推入执行栈; 得到结果 2 4 5 3
- 这时的宏任务又相当于一个线程,重复上述步骤1-5,直到任务队列为空。