进程与线程:
多进程与多线程:
并行与并发:
浏览器 JavaScript 引擎的两大特点:单线程、非阻塞(通过 Event Loop 事件循环机制实现)。
作为浏览器脚本语言,JavaScript 可以操作 DOM,多线程同时操作 DOM 可能会出现线程安全问题(多个线程竞争一个资源——DOM)
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如 I/O 事件)时,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调,浏览器 JavaScript 引擎中这个规则就是事件循环机制。
常见的阻塞:alert、confirm、prompt
宏任务(macrotask)队列:
script(整体代码)setTimeout、setIntervalrequestAnimationFramesetImmediate(Node.js 环境)微任务(microtask)队列:
Promise.thenMutationObserver、IntersectionObserver、PerformanceObserver)process.nextTick(Node.js 环境)宏任务队列可以有多个,微任务队列只有一个
宏任务:先进先出的执行原则
当有高优先级的任务时无法“插队”,所以引入微任务:每个宏任务结束后,都要清空所有的微任务。
浏览器中的宏任务队列有多个,如:定时器任务队列、网络请求任务队列等,浏览器将根据自己的算法(考虑队列优先级、搁置时间等因素)决定下一个宏任务从哪个队列中取
Node.js 采用 V8 作为 JS 的解析引擎,而 I/O 处理方面使用了自己设计的 libuv,libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是其中的实现
libuv 中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
setTimeout() 和 setInterval() 安排的回调setImmediate() 之外的几乎所有回调),node 会在适当的时候阻塞在这里setImmediate() 的回调socket.on('close', ...)Node.js V10 及以前:
Node.js V11 及以后:和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列
在浏览器和 Node.js 环境都支持
Promise.resolve().then(callback);
在浏览器和 Node.js 环境都支持,是一个全局的方法,它接受一个回调函数作为参数,并将这个回调函数放入微任务队列中
queueMicrotask(callback)
MutationObserver 是一个可以用来监视 DOM 变化的 API,仅在浏览器环境可用。当使用 MutationObserver 创建一个观察者并开始观察某个 DOM 元素时,每当这个元素或其子元素发生变化,MutationObserver 就会将提供的回调函数添加到微任务队列
const observer = new MutationObserver(callback);
const element = document.createTextNode("");
observer.observe(element, {
characterData: true,
});
const change = () => {
element.data = `${Date.now()}}`;
};
change();
process.nextTick 是 Node.js 特有的 API,它的回调函数会在当前操作完成后,下一个事件循环开始前立即执行,也是微任务的一种
process.nextTick(callback);
const micros = [
{
name: 'queueMicrotask',
test: () => {
return typeof queueMicrotask === "function";
},
run: (callback: Task) => {
queueMicrotask(callback);
},
},
{
name: 'Promise',
test: () => {
return typeof Promise !== "undefined";
},
run: (callback: Task) => {
Promise.resolve().then(callback);
},
},
{
name: 'process.nextTick',
test: () => {
return (
typeof process === "object" && typeof process.nextTick === "function"
);
},
run: (callback: Task) => {
process.nextTick(callback);
},
},
{
name: 'MutationObserver',
test: () => {
return (
typeof MutationObserver !== "undefined" && typeof window !== "undefined"
);
},
run: (callback: Task) => {
const observer = new MutationObserver(callback);
const element = document.createTextNode("");
observer.observe(element, {
characterData: true,
});
element.data = `${Date.now()}`;
},
},
];
export const runMicroTask = (callback: Task) => {
const runner = micros.find((item) => item.test());
if (!runner) {
throw new Error('当前环境不支持微任务');
}
runner.run(callback);
};
仅浏览器环境
提供了一种在不同文档(例如主线程和 worker 线程,或者不同的 iframes)之间,或者在同一文档的不同部分之间进行直接通信的方式
MessageChannel 有两个属性,port1 和 port2,它们都是 MessagePort 类型的对象。可以在一个地方使用 port1 发送消息,在另一个地方使用 port2 接收消息,反之亦然
const channel = new MessageChannel();
channel.port1.onmessage = callback;
const change = function () {
channel.port2.postMessage(0);
};
change();
上面的代码中 channel.port2 将一个消息异步地发送到 prot1 端口,然后 channel.port1.onmessage 的回调函数会加入到宏任务队列等待执行
仅浏览器环境
由浏览器在下一次重绘之前调用传入给该方法的回调函数。由于是和渲染帧同步的,所以可以认为是天然的宏任务
requestAnimationFrame(() => requestAnimationFrame(callback));
调用两次 requestAnimationFrame 的[[016.requestAnimationFrame#调用时机|原因]]
在浏览器和 Node.js 环境都支持
setTimeout 指定时间后将回调函数插入到任务队列中setInterval 循环指定时间后将回调函数不断的插入到任务队列中setTimeout(callback, 1000)
setInterval(callback, 1000)
Node.js 特有的 API,用于将一个回调函数排入事件循环队列,它将在当前事件循环(也就是当前的宏任务)完成后,下一个事件循环开始前被执行
setImmediate(callback);
const macros = [
{
name: 'MessageChannel',
test: () => {
return (
typeof MessageChannel !== "undefined" && typeof window !== "undefined"
);
},
run: (callback: Task) => {
const channel = new MessageChannel();
channel.port1.onmessage = callback;
channel.port2.postMessage(0);
},
},
{
name: 'setImmediate',
test: () => {
return typeof setImmediate === "function";
},
run: (callback: Task) => {
setImmediate(callback);
},
},
{
name: 'requestAnimationFrame',
test: () => {
return (
typeof requestAnimationFrame !== "undefined" && typeof window !== "undefined"
);
},
run: (callback: Task) => {
requestAnimationFrame(() => requestAnimationFrame(callback));
}
},
{
name: 'setTimeout',
test: () => {
return true;
},
run: (callback: Task) => {
setTimeout(callback, 0);
},
}
]
export const runMacroTask = (callback: Task) => {
const runner = macros.find((item) => item.test());
if (!runner) {
throw new Error('当前环境不支持宏任务');
}
runner.run(callback);
};