javaScript运行机制

进程与线程

进程:

  • 进程是操作系统资源分配的最小单位,是程序的运行实例。

  • 每个进程都有独立的内存空间,一个进程中所有线程共享进程的资源。

  • 进程间不共享内存,通信依靠文件、信号、管道等机制。

  • 创建与撤销进程需要操作系统提供的帮助,如fork()和exec()系统调用。

线程:

  • 线程是操作系统调度的基本单位,共同属于一个进程的多个执行流。

  • 多个线程共享进程的资源,如内存、文件描述符等。

  • 上下文切换成本比进程低,但不能利用多处理器资源。

  • 线程在同一个地址空间内。可实现并发执行同一个程序的多个部分。

区分:

  • 进程是资源分配的基本单位,线程是CPU调度和分派的基本单位。

  • 进程间通信开销大,线程间通信开销小。

  • 操作系统默认支持有限个进程但可能支持数以千计的线程。

  • 进程独享其资源集,线程共享进程资源。

所以简单来说,进程是系统资源分配的独立实体,线程是程序内部并发执行的基本单位。

JS为什么是单线程

  1. 历史原因:在设计JavaScript时,浏览器技术较为简单,采用单线程模型可以较好地实现。
  2. 同步需求:早期JavaScript主要用于网页交互,强调同步顺序执行。
  3. 目的简单:JavaScript最初目的仅仅是为动态网页添加简单交互代码,复杂度要求不高,单线程足以满足。
  4. 环境限制:早期浏览器硬件配置低下,开启多线程需要更多系统资源和细粒度同步机制,提高执行成本。
  5. 安全限制:多线程容易导致竞态条件和数据一致性问题,增加语言实现和应用开发难度。
  6. 事件驱动:JavaScript采用事件循环和回调函数机制,可以并发运行部分任务,在一定程度上弥补单线程效率不足。
  7. 历史兼容:

    如果JavaScript采用多线程模型,会对既有代码和程序造成不可兼容改变。

    所以基于历史因素和技术限制,JavaScript采用单线程模型更具可行性。但随着浏览器和硬件能力增强,可能会引入可选的多线程特性。

总结:早期浏览器技术,硬件配置简单底下,而js最初目的仅仅是为动态网页添加简单交互代码,复杂度要求不高,强调同步顺序执行,开启多线程成本太高,而且多线程易导致数据一致性和竞态问题,增加实现难度。所以单线程足以实现。

当然使用单线程也并不是完全没有问题,如果有一个任务需要长时间运行,那么它会阻塞整个程序,导致用户界面无响应。为了解决这个问题,JavaScript 采用了事件循环(Event Loop)模型,将长时间运行的任务分解成一系列小任务,然后在主线程的空闲时间间隙中执行这些小任务,从而避免了长时间的阻塞,实现部分并发,弥补单线程效率不足。

现在如果改用多线程, 将对既有代码产生不兼容影响。

所以,基于历史环境、目的与限制,单线程模型更适合JS的初衷。但同时,新标准也在探索如何在此基础上增加并发能力。

浏览器

浏览器是多进程的,拿Chrome来说,我们每打开一个Tab页就会产生一个进程,我们使用Chrome打开很多标签页不关,电脑会越来越卡,不说其他,首先就很耗CPU

浏览器包含哪些进程

主要线程

  1. 渲染进程(Renderer Process) :负责页面渲染工作,将HTML、CSS和JavaScript解析为用户界面。每个标签页对应一个独立的渲染进程。
  2. 浏览器进程(Browser Process):管理其他进程,处理用户界面交互和浏览器设置等工作。每打开一个浏览器实例就会有一个浏览器进程。
  3. 插件进程(Plugin Process):每种类型的插件对应一个进程,仅当使用该插件时才创建。
  4. GPU进程(GPU Process):最多一个,用于3D绘制等。

次要的

  1. 声音进程(Audio Process):专门处理音频相关工作的进程,防止音频任务影响网页执行。
  2. 网络进程(Network Process):专门处理网络请求的进程,提供DNS解析和网络连接管理功能。
  3. 文件进程(File Process):负责文件操作,与文件系统交互,提供文件读写接口。
  4. 辅助进程(Helper Process):比如 responsible for downloading files或上传文件时的进程

为什么浏览器要多进程

假设浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差

同理如果插件崩溃了也会影响整个浏览器

所以

  1. 提高系统稳定性:如果一个插件或网络应用崩溃,浏览器可以利用多进程将崩溃的进程与浏览器主体进行隔离,避免整个浏览器或所有网络应用受到影响。
  2. 提高响应速度:多进程意味着操作系统可以并发地运行网络应用,从而提高整体响应速度。
  3. 提高安全性:多进程可以在一个严格的沙箱内运行网络应用和插件,从而降低发生错误时的损失。同时,任务管理器可以跟踪每个网络应用和插件的资源使用率,无需重启浏览器就可以终止停止响应的网络应用或插件。

简述渲染进程Renderer(重)

页面的渲染,JS的执行,事件的循环,都在渲染进程内执行,所以我们要重点了解渲染进程

渲染进程是多线程的,我们来看渲染进程的一些常用较为主要的线程

GUI渲染线程
  • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
    • 解析html代码(HTML代码本质是字符串)转化为浏览器认识的节点,生成DOM树,也就是DOM Tree
    • 解析css,生成CSSOM(CSS规则树)
    • 把DOM Tree 和CSSOM结合,生成Rendering Tree(渲染树)
  • 当我们修改了一些元素的颜色或者背景色,页面就会重绘(Repaint)
  • 当我们修改元素的尺寸,页面就会回流(Reflow)
  • 当页面需要Repaing和Reflow时GUI线程执行,绘制页面
  • 回流(Reflow)比重绘(Repaint)的成本要高,我们要尽量避免Reflow和Repaint
  • GUI渲染线程与JS引擎线程是互斥的
    • 当JS引擎执行时GUI线程会被挂起(相当于被冻结了)
    • GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
JS引擎线程
  • JS引擎线程就是JS内核,负责处理Javascript脚本程序(例如V8引擎)
  • JS引擎线程负责解析Javascript脚本,运行代码
  • JS引擎一直等待着任务队列中任务的到来,然后加以处理
    • 浏览器同时只能有一个JS引擎线程在运行JS程序,所以js是单线程运行的
    • 一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
  • GUI渲染线程与JS引擎线程是互斥的,js引擎线程会阻塞GUI渲染线程
    • 就是我们常遇到的JS执行时间过长,造成页面的渲染不连贯,导致页面渲染加载阻塞(就是加载慢)
    • 例如浏览器渲染的时候遇到<script>标签,就会停止GUI的渲染,然后js引擎线程开始工作,执行里面的js代码,等js执行完毕,js引擎线程停止工作,GUI继续渲染下面的内容。所以如果js执行时间太长就会造成页面卡顿的情况
事件触发线程
  • 属于浏览器而不是JS引擎,用来控制事件循环,并且管理着一个事件队列(task queue)
  • 当js执行碰到事件绑定和一些异步操作(如setTimeOut,也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会走事件触发线程将对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
  • 因为JS是单线程,所以这些待处理队列中的事件都得排队等待JS引擎处理
定时触发器线程
  • 负责处理定时器任务,setIntervalsetTimeout所在线程
  • 浏览器定时计数器并不是由JavaScript引擎计数的(因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确)
  • 通过单独线程来计时。当一个定时器被触发时,定时触发器线程会把对应的任务添加到事件队列中,等待JS引擎的处理。注意,定时触发器线程与JS引擎线程是互斥的,当JS引擎执行时定时触发器线程会被挂起,待JS引擎空闲后才会执行定时任务。
  • W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
异步http请求线程
  • 浏览器专门用来发起网络请求的线程,它和JS引擎线程是相互独立的。在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中再由JavaScript引擎执行
  • 简单说就是当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行

事件循环(Event Loop)初探

首先要知道,JS分为同步任务和异步任务

同步任务都在主线程(这里的主线程就是JS引擎线程)上执行,会形成一个执行栈

主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调

一旦执行栈中的所有同步任务执行完毕(也就是JS引擎线程空闲了),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行

let setTimeoutCallBack = function() {
  console.log('我是定时器回调');
};
let httpCallback = function() {
  console.log('我是http请求回调');
}

// 同步任务
console.log('我是同步任务1');

// 异步定时任务
setTimeout(setTimeoutCallBack,1000);

// 异步http请求任务
ajax.get('/info',httpCallback);

// 同步任务
console.log('我是同步任务2');

上述代码执行过程

JS是按照顺序从上往下依次执行的,可以先理解为这段代码时的执行环境就是主线程,也就是也就是当前执行栈

首先,执行console.log('我是同步任务1')

接着,执行到setTimeout,会移交给定时器线程,通知定时器线程 1s 后将 setTimeoutCallBack 这个回调交给事件触发线程处理,在 1s 后事件触发线程会收到 setTimeoutCallBack 这个回调并把它加入到事件触发线程所管理的事件队列中等待执行

接着,执行http请求,会移交给异步http请求线程发送网络请求,请求成功后将 httpCallback 这个回调交由事件触发线程处理,事件触发线程收到 httpCallback 这个回调后把它加入到事件触发线程所管理的事件队列中等待执行

再接着执行console.log('我是同步任务2')

至此主线程执行栈中执行完毕,JS引擎线程已经空闲,开始事件触发线程发起询问,询问事件触发线程事件队列中是否有需要执行的回调函数,如果有将事件队列中的回调事件加入执行栈中,开始执行回调,如果事件队列中没有回调,JS引擎线程会一直发起询问,直到有为止

到了这里我们发现,浏览器上的所有线程的工作都很单一且独立,非常符合单一原则

定时触发线程只管理定时器且只关注定时不关心结果,定时结束就把回调扔给事件触发线程

异步http请求线程只管理http请求同样不关心结果,请求结束把回调扔给事件触发线程

事件触发线程只关心异步回调入事件队列

而我们JS引擎线程只会执行执行栈中的事件,执行栈中的代码执行完毕,就会读取事件队列中的事件并添加到执行栈中继续执行,这样反反复复就是我们所谓的事件循环(Event Loop)

图解

首先,执行栈开始顺序执行

判断是否为同步,异步则进入异步线程,最终事件回调给事件触发线程的任务队列等待执行,同步继续执行

执行栈空,询问任务队列中是否有事件回调

任务队列中有事件回调则把回调加入执行栈末尾继续从第一步开始执行

任务队列中没有事件回调则不停发起询问

宏任务(macrotask) & 微任务(microtask)

概念:

  • 宏任务(macrotask):包括整体代码、setTimeout、setInterval、setImmediate、I/O等。需要推入任务队列尾部运行。

  • 微任务(microtask):包括Process.nextTick(node)、Promise.then/catch、Object.observe(废弃)   finally等。需要在当前任务执行完后执行。微任务可以理解成在当前宏任务执行后立即执行的任务(插队)

两者的执行顺序是:

  1. 执行完当前同步代码进入事件循环的check阶段

  2. 执行完所有微任务进入check阶段

  3. 推入下一个宏任务执行

  4. 重复2-3步骤

所以微任务具有异步执行的特点,但优先级高于宏任务,能够“插队”执行。这对跨任务通信十分重要。

简单区分宏任务与微任务

看了上述宏任务微任务的解释你可能还不太清楚,没关系,往下看,先记住那些常见的宏微任务即可

我们通过几个例子来看,这几个例子思路来自掘金云中君,通过渲染背景颜色来区分宏任务和微任务,很直观,我觉得很有意思,所以这里也用这种例子

找一个空白的页面,在console中输入以下代码

document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:pink';

结果:

我们看到上面动图背景直接渲染了粉红色,根据上文里讲浏览器会先执行完一个宏任务,再执行当前执行栈的所有微任务,然后移交GUI渲染,上面四行代码均属于同一次宏任务,全部执行完才会执行渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉上,只会看到页面变成粉红色

再接着看

document.body.style = 'background:blue';
setTimeout(()=>{
    document.body.style = 'background:black'
},200)

上述代码中,页面会先卡一下蓝色,再变成黑色背景,之所以会卡一下蓝色,是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色

 再来看

document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
    console.log(2);
    document.body.style = 'background:pink'
});
console.log(3);

控制台输出 1 3 2 , 是因为 promise 对象的 then 方法的回调函数是异步执行,所以 2 最后输出

页面的背景色直接变成粉色,没有经过蓝色的阶段,是因为,我们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了粉色,然后才执行的渲染

微任务宏任务注意点

  • 浏览器会先执行一个宏任务,紧接着执行当前执行栈产生的微任务,再进行渲染,然后再执行下一个宏任务
  • 微任务和宏任务不在一个任务队列
    • 例如setTimeout是一个宏任务,它的事件回调在宏任务队列,Promise.then()是一个微任务,它的事件回调在微任务队列,二者并不是一个任务队列
    • 以Chrome 为例,有关渲染的都是在渲染进程中执行,渲染进程中的任务(DOM树构建,js解析…等等)需要主线程执行的任务都会在主线程中执行,而浏览器维护了一套事件循环机制,主线程上的任务都会放到消息队列中执行,主线程会循环消息队列,并从头部取出任务进行执行,如果执行过程中产生其他任务需要主线程执行的,渲染进程中的其他线程会把该任务塞入到消息队列的尾部,消息队列中的任务都是宏任务
    • 微任务是如何产生的呢?当执行到script脚本的时候,js引擎会为全局创建一个执行上下文,在该执行上下文中维护了一个微任务队列,当遇到微任务,就会把微任务回调放在微队列中,当所有的js代码执行完毕,在退出全局上下文之前引擎会去检查该队列,有回调就执行,没有就退出执行上下文,这也就是为什么微任务要早于宏任务,也是大家常说的,每个宏任务都有一个微任务队列(由于定时器是浏览器的API,所以定时器是宏任务,在js中遇到定时器会也是放入到浏览器的队列中)

图解宏任务和微任务

首先执行一个宏任务,执行结束后判断是否存在微任务

有微任务先执行所有的微任务,再渲染,没有微任务则直接渲染

然后再接着执行下一个宏任务

图解完整的Event Loop

首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务异步任务两部分

同步任务会直接进入主线程依次执行

异步任务会再分为宏任务和微任务

宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中

微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中

当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务

上述过程会不断重复,这就是Event Loop,比较完整的事件循环

关于Promise

new Promise(() => {}).then() ,我们来看这样一个Promise代码

前面的 new Promise() 这一部分是一个构造函数,这是一个同步任务

后面的 .then() 才是一个异步微任务,这一点是非常重要的

new Promise((resolve) => {
	console.log(1)
  resolve()
}).then(()=>{
	console.log(2)
})
console.log(3)

上面代码输出1 3 2

关于 async/await 函数

async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种

所以在使用await关键字与Promise.then效果类似

setTimeout(() => console.log(4))

async function test() {
  console.log(1)
  await Promise.resolve()
  console.log(3)
}

test()

console.log(2)

上述代码输出1 2 3 4

可以理解为,await 以前的代码,相当于与 new Promise 的同步代码,await 以后的代码相当于 Promise.then的异步

接下来这个来自网上随意找的一个比较简单的面试题,求输出结果

function test() {
  console.log(1)
  setTimeout(function () { 	// timer1
    console.log(2)
  }, 1000)
}

test();

setTimeout(function () { 		// timer2
  console.log(3)
})

new Promise(function (resolve) {
  console.log(4)
  setTimeout(function () { 	// timer3
    console.log(5)
  }, 100)
  resolve()
}).then(function () {
  setTimeout(function () { 	// timer4
    console.log(6)
  }, 0)
  console.log(7)
})

console.log(8)

JS是顺序从上而下执行

执行到test(),test方法为同步,直接执行,console.log(1)打印1

test方法中setTimeout为异步宏任务,回调我们把它记做timer1放入宏任务队列

接着执行,test方法下面有一个setTimeout为异步宏任务,回调我们把它记做timer2放入宏任务队列

接着执行promise,new Promise是同步任务,直接执行,打印4

new Promise里面的setTimeout是异步宏任务,回调我们记做timer3放到宏任务队列

Promise.then是微任务,放到微任务队列

console.log(8)是同步任务,直接执行,打印8

主线程任务执行完毕,检查微任务队列中有Promise.then

开始执行微任务,发现有setTimeout是异步宏任务,记做timer4放到宏任务队列

微任务队列中的console.log(7)是同步任务,直接执行,打印7

微任务执行完毕,第一次循环结束

检查宏任务队列,里面有timer1、timer2、timer3、timer4,四个定时器宏任务,按照定时器延迟时间得到可以执行的顺序,即Event Queue:timer2、timer4、timer3、timer1,依次拿出放入执行栈末尾执行 (插播一条:浏览器 event loop 的 Macrotask queue,就是宏任务队列在每次循环中只会读取一个任务)

执行timer2,console.log(3)为同步任务,直接执行,打印3

检查没有微任务,第二次Event Loop结束

执行timer4,console.log(6)为同步任务,直接执行,打印6

检查没有微任务,第三次Event Loop结束

执行timer3,console.log(5)同步任务,直接执行,打印5

检查没有微任务,第四次Event Loop结束

执行timer1,console.log(2)同步任务,直接执行,打印2

检查没有微任务,也没有宏任务,第五次Event Loop结束

结果:1,4,8,7,3,6,5,2

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值