"深入理解进程、线程与JavaScript的单线程、异步编程及事件循环机制"
前言
在本篇教程中,我们将从线程与进程的基本概念出发,逐步深入到JavaScript的单线程机制、异步编程模式以及事件循环机制等核心内容。通过理论讲解与实例分析相结合的方式,帮助读者全面掌握JavaScript的异步编程技巧与最佳实践。
一,进程和线程
清楚的了解什么是进程什么是线程是了解V8执行机制的基础
进程(Process)
在操作系统层面,进程是系统进行资源分配和调度的一个独立单元,是应用程序运行的容器。每个进程都有自己的独立内存空间和系统资源。在JavaScript的浏览器环境中,每个打开的浏览器标签页或窗口都可以被视为一个独立的进程。
线程(Thread)
线程是进程中的一个实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的单位。线程共享进程的资源(如内存和文件句柄),但每个线程都有自己的执行栈和程序计数器。
总结
在Web浏览器中,当打开一个标签页时,浏览器会为该页面启动一个进程,该进程 内部包含多个线程,如
- 渲染线程:负责页面的渲染。
- JavaScript引擎线程:解析和执行JS代码。由于JS可以操作DOM,而DOM是由渲染线程管理的,因此JS引擎线程和渲染线程是互斥的,以防止竞态条件。
- HTTP请求线程:处理网络请求
js的加载会阻塞页面的渲染,js引擎在工作时会阻塞渲染线程,渲染线程和js引擎线程不能同时工作
二,V8编程
JavaScript 在单线程环境中执行,这意味着它一次只能处理一个任务。然而,为了支持异步编程和高效利用资源,JavaScript 引擎使用了事件循环和消息队列。进程通常包含多个线程,但 JavaScript 的运行环境(如浏览器或 Node.js)为了简化编程模型和避免线程间的复杂性,选择了单线程模型。但这并不意味着 JavaScript 无法处理并发操作;相反,通过异步编程和事件循环,JavaScript 可以高效地处理多个任务,而不会阻塞用户界面或造成资源浪费。
基础执行规则
对于JS的执行规则,基础的一点就是:遇到需要耗时的代码就先挂起,先执行不耗时代码;等不耗时代码执行完成再执行耗时代码
在执行代码的时候,V8会先将不耗时的代码先执行,之后再执行耗时的代码,举个例子
var a = 1
console.log(a); // 1
setTimeout(function () {
let b = 1
b++
console.log(b); // 2
}, 1000)
console.log(a); // 1
通过打印我们能得知上述打印结果是:1,1,2。这是一个V8在执行中很好的例子
JS的单线程的优点
JS的单线程特性意味着在同一时间内,只有一个JS代码块在执行。这有助于简化编程模型,避免了多线程编程中的复杂性,如锁、竞态条件等问题。同时,它也带来了性能上的优化,如减少了上下文切换的开销。但是 js的加载会阻塞页面的渲染,js引擎在工作时会阻塞渲染线程,渲染线程和js引擎线程不能同时工作依旧是一个问题。
通过事件循环(Event Loop)可以解决这个问题,它允许JS执行代码、处理用户交互、执行异步操作(如网络请求)等,而不会阻塞UI的更新。
三,异步编程
在JS中,代码分为同步代码和异步代码。同步代码按顺序直接执行,不阻塞主线程;而异步代码则会被放入任务队列中,等待主线程空闲时执行。 异步代码进一步分为微任务和宏任务:
- 微任务:包括
Promise.then()
,process.nextTick()
(Node.js特有),MutationObserver
等,这些任务会在当前执行栈清空后立即执行,且每个宏任务执行后,会先清空微任务队列。 - 宏任务:包括
script
(整体代码)、setTimeout
、setInterval
、setImmediate
(Node.js特有)、I/O操作、UI渲染等,这些任务会按照特定顺序(如浏览器或Node.js的实现)排队执行。
四,事件循环机制Event Loop
事件循环是异步编程的基础,通过管理任务队列来调度和执行异步任务 通过Event Loop,JS能够非阻塞地执行代码,使得在等待如网络请求、文件读写等异步操作完成时,能够继续处理其他任务。因此,深入理解Event Loop的工作原理,对于掌握JS的异步编程模式至关重要。
event-loop执行机制是面试的必考题,牢固掌握其运行机制是基本
事件循环机制基础步骤:
- 执行同步代码(宏任务)
- 同步代码执行完毕后,检查是否有异步需要执行(检查队列中是否有代码需要执行)
- 执行所有的微任务
- 微任务执行完毕后,如果有需要就会渲染页面
- 执行异步宏任务,开启下一次事件循环(回到第一步)
基础题
// 面试必考
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0)
}, 0)
console.log(7);
我们通过上题中的打印来洞悉V8的运行机制
详细运行机制:
所以最后的输出结果是 1,2,7,3,5,4,6
五,async await
async await是promise.then的语法糖,它允许你使用更加优雅的方式去处理函数的执行顺序。你需要再封装一个函数,async这个函数然后用await排列你需要执行的函数顺序。
基础用法
以这个例子来说,getData()先执行,再another(),another2()。我们给每个函数中还是return了new Promise,但是在最下面使用了async await替代.then用法。
let data = null
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
data = [1, 2, 3]
resolve()
}, 1000)
})
}
function another() {
return new Promise((resolve, reject) => {
setTimeout(() => {
data.push(4)
resolve()
}, 100)
})
}
function another2() {
console.log(data);
}
//ES7打造
async function foo() {
await getData()
await another()
another2()
}
foo()
最后执行结果:[1,2,3,4]
进阶题
在这道题中为我们还是遵循 事件循环机制的基础步骤,要知道async await是promise.then的语法糖,那么await就等同于是.then,那么分析就简单了。
- 首先执行同步代码:script start,async2 end,promise,script end
- 接着微任务出列:async1 end,then1,then2
- 最后宏任务出列:setTimeout
完美解决~
console.log('script start');
async function async1() {
await async2()
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
async1()
setTimeout(function () {
console.log('setTimeout');
}, 0)
new Promise(function (resolve, reject) {
console.log('promise');
resolve()
})
.then(() => {
console.log('then1');
})
.then(() => {
console.log('then2');
})
console.log('script end');
小结
学习JS时,理解线程与进程是基础。JS单线程机制避免了线程互斥问题,但受限于同步执行效率低且容易造成阻塞渲染的问题。为优化,引入异步机制,并通过微任务、宏任务与事件循环(Event-Loop)协同工作,高效处理并发任务,确保程序流畅执行。
转载自:https://juejin.cn/post/7394788843206066227