一、进程与线程
进程是计算机中运行的程序的实例,它包含了程序的代码和程序在运行时所需要的资源。进程的特点是独立性,一个进程不会直接访问另一个进程的数据。每个进程有自己的内存空间,互相之间不会干扰。
线程是进程的更小单位,是操作系统能够进行运算调度的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源。线程之间共享相同的上下文,包括代码段、数据段和打开的文件等。
二、浏览器新开一个 Tab 页(进程)
当你在浏览器中新开一个 Tab 页时,涉及到多个线程协同工作:
- 渲染线程:负责页面渲染,将 HTML、CSS 转换成可视化的界面。
- HTTP 请求线程:处理网络请求,发送 HTTP 请求并接收响应。
- JS 引擎线程:执行 JavaScript 代码,处理页面中的交互和动态效果。
这些线程之间可以同时工作,提高了浏览器的效率。但是,渲染线程和 JS 引擎线程是互斥的,因为它们都涉及到对 DOM 的操作,为了避免冲突,浏览器会限制它们的并发执行。
三、JavaScript 是单线程的
JavaScript 是单线程的语言,意味着在同一时刻只能执行一个任务。这一设计的优点有:
- 节约内存:不需要为多线程之间共享的数据分配额外的内存。
- 无锁概念:由于只有一个线程,避免了多线程之间可能出现的锁的问题,减少了上下文切换的时间。
四、异步编程
在 JavaScript 中,为了处理异步操作,引入了宏任务和微任务的概念。
- 宏任务(macrotask) :包括整体代码、setTimeout、setInterval、I/O 操作等,它们会被放入到任务队列中,等待执行。
- 微任务(microtask) :包括 promise.then()、MutationObserver、process.nextTick()等,微任务的执行时机在宏任务执行结束后、渲染之前。
五、Event-Loop
JavaScript 执行的事件循环(Event-Loop)是一个重要的概念,它决定了代码的执行顺序。
- 执行同步代码(属于宏任务)。
- 当执行栈为空时,查询是否有异步任务需要执行。
- 执行微任务队列中的任务。
- 如果需要,浏览器可能会进行页面渲染。
- 执行下一个宏任务。
以下是一个简单的例子,演示了异步编程和 Event-Loop 的执行顺序:
console.log("Start");
//(setTimeout) - 将被放入宏任务队列中
setTimeout(() => {
console.log("Timeout");
}, 0);
// Promise.resolve().then() - 将被放入微任务队列中
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
在这个例子中,输出的顺序是 "Start","End","Promise","Timeout"。这展示了宏任务和微任务的执行顺序,以及 Event-Loop 如何协调它们的执行。以下是详细讲解步骤:
-
开始执行:
- 脚本开始执行,并且“Start”被记录到控制台。
-
设置超时:
setTimeout遇到该函数。它安排宏任务(回调)至少在指定的延迟(在本例中为 0 毫秒)后执行。该宏任务被放置在宏任务队列中。
-
承诺:
- 创建
Promise.resolve().then(...)一个微任务(回调)并安排它在微任务队列中执行。
- 创建
-
结尾:
- “End”被记录到控制台。
现在,脚本的初始同步执行已经完成。调用堆栈为空,JavaScript 运行时检查任务队列中的任务。
-
事件循环迭代:
- 事件循环开始迭代。
-
微任务执行:
- 事件循环首先检查微任务队列并找到由
Promise.resolve().then(...). “Promise”被记录到控制台。
- 事件循环首先检查微任务队列并找到由
-
宏任务执行:
- 处理完所有微任务后,事件循环检查宏任务队列。它找到
setTimeout回调并将“超时”记录到控制台。
- 处理完所有微任务后,事件循环检查宏任务队列。它找到
解释:
- 事件循环在宏任务之前处理微任务。这就是为什么“Promise”在“Timeout”之前被记录。
- 即使延迟
setTimeout设置为0,并不意味着回调会立即执行。在许多浏览器中,最小延迟实际上为 4 毫秒。 - 微任务通常用于高优先级任务,并在下一次渲染之前执行,因此适合更新 UI 等任务。
了解事件循环以及宏任务和微任务之间的区别对于编写高效且响应迅速的异步 JavaScript 代码至关重要。