「硬核JS」一次搞懂JS运行机制 - 读后感

深入浅出、面面俱到,很推荐这篇文章:

掘金_「硬核JS」一次搞懂JS运行机制icon-default.png?t=N7T8https://juejin.cn/post/6844904050543034376#heading-15

一、行进与线程

1. 进程:

  • “CPU资源分配的最小单位”
  • 一个进程 = 该程序 + 该程序使用的内存 + 该程序使用的系统资源
  • 一个CPU在一个时间点只能运行一个进程,借助 “时间片轮转调度算法” 实现多个进程之间的切换,以达到同时运行多个进行的目的

2. 线程:

  • “CPU调度的最小单位”
  • 不同的线程表示一个进程中不同的执行路线
  • 一个进程可以有多个线程
  • 独有资源:栈、寄存器(物理寄存器的副本)
  • 共享资源:堆、全局变量、静态变量、文件等公用资源
  • 共享的环境:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录、进程用户ID、进程组ID(操作系统--多线程之间共享哪些资源?

拓展:虚拟线程

  • 定义:

虚拟线程是一种逻辑上的线程,它并不直接对应于操作系统的物理线程。由Java虚拟机在内部管理和调度,在一个物理线程上可以创建多个虚拟线程,并通过合理的调度算法实现并发执行

  • 原理:

基于Java虚拟机的线程调度和上下文切换机制。当虚拟线程被创建时,它会被分配给一个物理线程进行执行。虚拟线程的调度由Java虚拟机负责,根据线程优先级、调度策略和资源可用性等因素进行决策。

虚拟线程的上下文切换是通过保存和恢复线程上下文实现的。当一个虚拟线程的执行时间片用完或者发生阻塞时,Java虚拟机会保存该线程的上下文状态,并切换到其他可执行的虚拟线程。当虚拟线程再次被调度执行时,它的上下文状态将被恢复,从上次离开的地方继续执行。

3. 进程与线程的区别:

  • 进程之间相互独立,但同一进程下各个线程之间共享程序的内存以及程序级资源

4. 多进程与多线程:

  • 多线程意味着在一个程序中,同一时间可以同时执行多个任务
  • 多进程意味着可以在同一时间运行多个程序

5. 单线程的JS:

  • 尽管借助Web Worker可以为JS创建多个线程,但这些线程都受到主线程的控制,因此本质上依旧是单线程

二、浏览器

1. 浏览器是多进程的

一个Tab页就是一个进程,首页也很消耗CPU

2. 浏览器都有哪些进程?

  • Browser进程

    • 浏览器的主进程(负责界面显示、交互、管理,网络资源的管理等),该进程只有一个
  • 第三方插件进程

    • 每种类型的插件对应一个进程,使用该插件时才创建
  • GPU进程

    • 该进程也只有一个,用于3D绘制等等
  • 渲染进程(重)

    • 也就是浏览器内核(Renderer进程,内部是多线程)
    • 每个Tab页面都有一个渲染进程,互不影响,负责页面渲染,脚本执行,事件处理等

3. 为什么浏览器是多进程?

  • 避由于某个插件或Tab页的异常影响整个浏览器

三、渲染进程(Renderer)

渲染进程的主要线程:

  • GUI渲染线程
    • 解析HTML、CSS生成DOM Tree、CSSOM,再组装成Render Tree
    • 重绘、回流时,对页面的绘制
    • GUI渲染线程与JS引擎线程是互斥的
      • 当JS引擎执行时,GUI线程会被挂起(相当于冻结了)。GUI更新会被保存在一个队列中,等到JS引擎空闲时立即被执行
  • JS引擎线程
    • 也就是JS内核(例如Chrome的V8引擎),负责解析JavaScript脚本,管理着一个 “执行栈”,一个“微任务队列
  • 事件触发线程
    • 控制 “事件循环”,管理着一个 “宏任务队列”(event queue)
    • 当JS执行时,如果遇到 “异步任务”,“事件触发线程” 将任务添加到对应的线程中(比如定时器操作,便把定时器任务添加到定时器线程),等异步任务有了结果,便把它们的回调添加到 “宏任务队列”,等待JS引擎线程空闲时处理
  • 定时触发器线程(setTimeout、setInterval)
    • 执行到定时器时,先在 “定时器线程” 进行 “定时” 与 “计时”,计时完毕后,将回调函数交给 “事件触发线程”,再由 “事件触发线程” 将回调函数添加到 “任务队列” 中,等待JS引擎线程空闲后执行
    • W3C规定,setTimeout中小于4ms的事件间隔算为4ms
  • 异步http请求线程
    • 执行到一个http请求时,先把 “异步请求任务” 添加到 “异步请求线程” 中,请求状态变更后,把回调交给 “事件触发线程”,由 “事件触发线程” 将回调函数添加到 “任务队列

我们会发现,浏览器上所有线程的工作都很单一且独立,非常符合 “单一原则”。

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

“异步http请求线程” 只管理http请求,同样不关心回调对应的结果,请求成功后就把回调扔给 “事件触发线程”;

“事件触发线程” 只关心将异步回调推入任务队列。

四、事件循环(Event Loop)

1. 从“同步”、“异步”的角度看事件循环

script分为 “同步任务” 与 “异步任务”,“同步任务” 在 “执行栈” 中执行,“异步任务” 有了运行结果,就将其回调放入 “任务队列” 中。“执行栈” 中的 “同步任务” 全部执行完毕,就开始读取 “任务队列”,依次添加到 “执行栈” 中执行。

注意,await以前的代码,相当于new Promise的同步代码,await以后的代码相当与Promise.then的异步。

2. 从“宏任务”、“微任务”角度看事件循环

注意!宏任务(除最大的 script 外)、微任务 都是异步任务!

1)宏任务(macrotask)

宏任务的每次每次执行都是从头到尾地执行,不会在中途停止下来去执行其他任务。

常见的宏任务:

  • script代码
  • setTimeout
  • setInterval
  • requestAnimationFrame

requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:

  • requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
  • 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。

2)微任务(microtask

常见微任务

  • Promise.then()
  • catch
  • finally
  • Object.observe
  • MutationObserver

3)Event Loop流程

当一个 “宏任务” 执行完,会在渲染前,将执行期间所产生的所有 “微任务” 都执行完。然后执行下一个 “宏任务”,如此循环往复。

宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...

渲染时,在一个GUI线程中,会将所有UI改动优化合并。 

注意,并不是每一个微任务执行完毕过后都会进行渲染,而是按照帧的频率处理渲染操作。每一帧内可能循环多次,浏览器每一帧才会渲染一次页面,因此,一帧内的所有渲染最终会合并为一次。

3. 结合“同步任务”、“异步任务”、“宏任务”、“微任务”分析事件循环

  • 首先,“微任务” 的事件回调在 “JS引擎线程” 管理的 “微任务队列” 中;“宏任务” 的事件回调在 “事件触发线程” 管理的 “宏任务队列(消息队列)” 中
  • <script>是最大的 “宏任务”,也是一个宏任务中唯一的一个 “同步任务”,在主线程(JS引擎线程)中执行
  • 执行时,若遇到 “异步任务”,判断 “异步任务” 是 “微任务” 还是 “宏任务”
  • 宏任务进入到 “事件表”(Event Table)中,并在里面注册回调函数
  • 若是 “宏任务”,由事件触发线程来判断这个 “宏任务” 是否拥有自己的 “专属执行线程”
  • 有,就把该任务交给 “专属线程”,专属线程完成对应事件后,将事件对应的回调再转交给 “事件触发线程”,事件触发线程再将 “事件表” 中对应的回调函数移到 “宏任务队列” 中
  • “微任务” 也会进入到另一个 “事件表” 中,并在里面注册回调函数,每当指定的事件完成时,由JS引擎线程将 “事件表” 中对应的回调函数移到 “微任务队列” 中
  • “主线程” 执行完<script>后,开始执行 “全局执行上下文” 管理的 “微任务队列”
  • “微任务队列” 执行完,再开始执行下一个 “宏任务”,一直这样循环下去,便是事件循环Event Loop了。

我们一起来看一道面试题:

async function async1() {
  console.log('1');
  await async2();
  console.log('2');
}
async function async2() {
  console.log('3');
}
console.log('4');

setTimeout(() => {
  console.log('5');
}, 0)

async1();

new Promise(function (resolve) {
  console.log('6');
  resolve();
}).then(function () {
  console.log('7');
})

这道题重点考察了Promise、async/await、setTimeout的任务类型。

可能有一定基础的小伙伴很快就可以回答出来,Promise的.then属于微任务,setTimeout属于宏任务。

那么,await 关键字后面的语句是什么类型?await 下面的语句又属于什么任务类型?

我们通过答案来反推一下(一定要先尝试写一下自己的答案哈):

4
1
3
6
2
7
5

和你的答案一样吗?

我们一起来推理一下,首先打印 4  1 这很好理解,同步任务,自然是直接执行。

后面紧接着打印了 3,就表示 await 关键字引导的语句,属于同步任务,并没有被归为异步任务。await 仅阻塞下面的语句

紧接着打印了 6,这点很简单,因为 new Promise 的部分依旧属于同步任务。

大家都知道 .then 是异步任务中的微任务,2 在 7 前面被打印,自然就可以推导出,await 下面的语句,也属于微任务

为什么呢?本质上,await 下面的语句,其实就相当于 .then 里面的语句。同样是 .then 自然 2 就在 7 前面被打印了。

最后打印宏任务的定时器回调。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
第一章 概述 1.1 Javascript简史 在20世纪90年代,也就是早期的WEB站点上,所有的网页内容都是静态的,所谓静态是指,除了点击超链接,你无法通过任何方式同页面进行交互,比 如让页面元素接受事件,修改字体等。人们于是迫切的需要一种方式来打破这个局限,于是到了1996年,网景(Netscape)公司开始研发一种新的语言 Mocha,并将其嵌入到自己的浏览器Netscape中,这种语言可以通过操纵DOM(Document Object Model,文档对象模型)来修改页面,并加入了对鼠标事件的支持。Mocha使用了C的语法,但是设计思想上主要从函数式语言Scheme那里取得了灵 感。当Netscape 2发布的时候,Mocha被改名为LiveScript,当时可能是想让LiveScript为WEB页面注入更多的活力。后来,考虑到这个脚本语言的推 广,网景采取了一种宣传策略,将LiveScript更名为JavaScript,目的是为了跟当时非常流行的面向对象语言Java发生暧昧的关系。这种 策略显然颇具成效,以至于到现在很多初学者还会为JavaScript和Java的关系而感到困惑。 Javascript取得成功了之后,确实为页面注入了活力,微软也紧接着开发自己的浏览器脚本语言,一个是基于BASIC语言的 VBScript,另一个是跟Javascript非常类似的Jscript,但是由于Javascript已经深入人心,所以在随后的版本中,微软的 IE几乎是将Javascript作为一个标准来实现。当然,两者仍然有不兼容的地方。1996年后期,网景向欧洲电脑厂商协会(ECMA)提交了 Javascript的设计,以申请标准化,ECMA去掉了其中的一些实现,并提出了ECMAScript-262标准,并确定Javascript的正 式名字为ECMAScript,但是JavaScript的名字已经深入人心,故本书中仍沿用Javascript这个名字。 1.1.1动态网页 WEB页面在刚开始的时候,是不能动态修改其内容的,要改变一个页面的内容,需要先对网站上的静态HTML文件进行修改,然后需要刷新浏览器。后来 出现的JSP,ASP等服务器端语言可以为页面提供动态的内容,但是如果没有JavaScript则无法在服务器返回之后动态的在前端修改页面,也无法有 诸如鼠标移上某页面元素则高亮该元素之类的效果,因此JavaScript的出现大大的丰富了页面的表现,提高了用户体验。 而当 AJAX流行起来之后,更多的非常绚丽的WEB应用涌现了,而且呈越来越多的趋势,如Gmail,Google Map,Google Reader,Remember the milk,facebook等等优秀的WEB2.0应用,都大量的使用了JavaScript及其衍生的技术AJAX。 1.1.2浏览器之战 1.1.3标准 1.2 JavaScript语言特性 JavaScript 是一门动态的,弱类型,基于原型的脚本语言。在JavaScript中“一切皆对象”,在这一方面,它比其他的OO语言来的更 为彻底,即使作为代码本身载体的function,也是对象,数据与代码的界限在JavaScript中已经相当模糊。虽然它被广泛的应用在WEB客户 端,但是其应用范围远远未局限于此。下面就这几个特点分别介绍: 1.2.1动态性 动态性是指,在一个Javascript对象中,要为一个属性赋值,我们不必事先创建一个字段,只需要在使用的时候做赋值操作即可,如下例: Js代码 1. //定义一个对象 2. var obj = new Object(); 3. //动态创建属性name 4. obj.name = "an object"; 5. //动态创建属性sayHi 6. obj.sayHi = function(){ 7. return "Hi"; 8. } 9. obj.sayHi(); //定义一个对象 var obj = new Object(); //动态创建属性name obj.name = "an object"; //动态创建属性sayHi obj.sayHi = function(){ return "Hi"; } obj.sayHi(); 加入我们使用Java语言,代码可能会是这样: Js代码 1. class Obj{ 2. String name; 3. Function sayHi; 4. public Obj(Sting name, Function sayHi){ 5. this.name = name; 6. this.sayHi = sayHi; 7. } 8. } 9. Obj obj = new Obj("an object", new Function()); class Obj{ String name; Function sayHi; public Obj(Sting name, Function sayHi){ this.name = name; this.sayHi = sayHi; } } Obj obj = new Obj("an object", new Function()); 动态性是非常有用的,这个我们在第三章会详细讲解。 1.2.2弱类型 与Java,C/C++不同,Javascript是弱类型的,它的数据类型无需在声明时指定,解释器会根据上下文对变量进行实例化,比如: Js代码 1. //定义一个变量s,并赋值为字符串 2. var s = "text"; 3. print(s); 4. //赋值s为整型 5. s = 12+5; 6. print(s); 7. //赋值s为浮点型 8. s = 6.3; 9. print(s); 10. //赋值s为一个对象 11. s = new Object(); 12. s.name = "object"; 13. print(s.name); //定义一个变量s,并赋值为字符串 var s = "text"; print(s); //赋值s为整型 s = 12+5; print(s); //赋值s为浮点型 s = 6.3; print(s); //赋值s为一个对象 s = new Object(); s.name = "object"; print(s.name); 结果为: text 17 6.3 Object 可见,Javascript的变量更像是一个容器,类似与Java语言中的顶层对象Object,它可以是任何类型,解释器会根据上下文自动对其造 型。 弱类型的好处在于,一个变量可以很大程度的进行复用,比如String类型的name字段,在被使用后,可以赋值为另一个Number型的对象,而 无需重新创建一个新的变量。不过,弱类型也有其不利的一面,比如在开发面向对象的Javascript的时候,没有类型的判断将会是比较麻烦的问题,不过 我们可以通过别的途径来解决此问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麦田里的POLO桔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值