深入理解浏览器渲染原理及js代码运行原理——前端之JavaScript高级之二【Day29-Day35】

944 阅读41分钟

挑战坚持学习1024天——前端之JavaScript高级

最近有阅读这一句话,编程除了算法和数据结构,什么也不属于我们。仔细思考下来会觉得很有道理,所谓程序员,有很多人被调侃为新时代农民工。其实本质上我们跟农民工干的活式一样的,我们没有所谓属于自己的东西,我们所学的技能都是站在巨人的肩膀上。每天写代码的过程中都像式在板砖,从这里搬到那里,可能真的属于我们自己的就只有数据结构算法了吧。但农民工也有底层顶层之分,也可以从农民工成长为架构师/cto。只要有梦并为之努力,会离自己向往的生活会越来越近。

js基础部分可到我文章专栏去看 ---点击这里

Day29【2022年8月22日】

学习重点: 浏览器的渲染原理(上) 浏览器工作原理参考网站 web.dev/howbrowsers…

1.网页的解析过程

1.1解析一:HTML解析过程

因为默认情况下服务器会给浏览器返回index.html文件,所以解析HTML是所有步骤的开始:解析HTML,会构建DOM Tree:

image.png

1.2解析二:生成CSS规则

在解析的过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件:

  • 注意:下载CSS文件是不会影响DOM的解析的;

浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树:

  • 我们可以称之为 CSSOM(CSS Object Model,CSS对象模型);

image.png

1.3解析三 – 构建Render Tree

当有了DOM Tree和 CSSOM Tree后,就可以两个结合来构建Render Tree了 image.png 注意一:link元素不会阻塞DOM Tree的构建过程,但是会阻塞Render Tree的构建过程

  • 这是因为Render Tree在构建时,需要对应的CSSOM Tree;

注意二:Render Tree和DOM Tree并不是一一对应的关系,比如对于display为none的元素,压根不会出现在render tree中;

1.4解析四 – 布局(layout)和绘制(Paint)

第四步是在渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体。

  • 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息;

  • 布局是确定呈现树中所有节点的宽度、高度和位置信息;

第五步是将每个节点绘制(Paint)到屏幕上

  • 在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上实际的像素点;

  • 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img)

image.png

2.浏览器渲染流程

浏览器渲染主要有以下步骤:

  • 首先解析收到的文档,根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
  • 然后对 CSS 进行解析,生成 CSSOM 规则树。
  • 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
  • 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
  • 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。 流程如下图所示:

8b9c27221d7203c311cbd48122fa9e0.png 注意这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

3.今日精进

人生中充满了很多选择,所以人生允满了很多变数。细小的选择影响心情,重大的选择影响人生,关健的的选择决定一生。不要抱怨,抱怨就是一种选择;要努力改变,这才是正确的选择,过去的选择造就了现在的人生,现在的选择决定了未来的人生,所以,学会积极吸收正能量的事物,这是关乎一生的选择。

Day30【2022年8月23日】

学习重点: 浏览器的渲染原理(下)

1.回流和重绘解析

1.1回流(重排)

1.1.1理解回流reflow:(也可以称之为重排)

  • 第一次确定节点的大小和位置,称之为布局(layout)。
  • 之后对节点的大小、位置修改重新计算称之为回流。

1.1.2什么情况下引起回流呢?

  • 比如DOM结构发生改变(添加新的节点或者移除节点);

  • 比如改变了布局(修改了width、height、padding、font-size等值)

  • 比如窗口resize(修改了窗口的尺寸等)

  • 比如调用getComputedStyle方法获取尺寸、位置信息;

1.2重绘

1.2.1理解重绘repaint:

  • 第一次渲染内容称之为绘制(paint)。
  • 之后重新渲染称之为重绘。

1.2.2什么情况下会引起重绘呢?

  • 比如修改背景色、文字颜色、边框颜色、样式等;

回流一定会引起重绘,所以回流是一件很消耗性能的事情。所以在开发中要尽量避免发生回流。 重绘不一定引起回流,而回流一定会引起重绘。

1.3避免回流的方式

1.3.1修改样式时尽量一次性修改

  • 比如通过cssText修改,比如通过添加class修改

1.3.2.尽量避免频繁的操作DOM

  • 我们可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成,再一次性的操作;

1.3.3.尽量避免通过getComputedStyle获取尺寸、位置等信息;

1.3.4.对某些元素使用position的absolute或者fixed

  • 并不是不会引起回流,而是开销相对较小,不会对其他元素造成影响。

2.合成和性能优化

2.1特殊解析 – composite合成

绘制的过程,可以将布局后的元素绘制到多个合成图层中。

  • 这是浏览器的一种优化手段;

默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的;而一些特殊的属性,会创建一个新的合成层( CompositingLayer ),并且新的图层可以利用GPU来加速绘制;

  • 因为每个合成层都是单独渲染的;

那么哪些属性可以形成新的合成层呢?常见的一些属性:

  • 3D transforms

  • video、canvas、iframe

  • opacity 动画转换时;

  • position: fixed

  • will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化;

  • animation 或 transition 设置了opacity、transform;

分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用。

2.2性能优化

**(1)针对JavaScript:**JavaScript既会阻塞HTML的解析,也会阻塞CSS的解析。因此我们可以对JavaScript的加载方式进行改变,来进行优化:

(1)尽量将JavaScript文件放在body的最后

(2) body中间尽量不要写<script>标签

(3)<script>标签的引入资源方式有三种,有一种就是我们常用的直接引入,还有两种就是使用 async 属性和 defer 属性来异步引入,两者都是去异步加载外部的JS文件,不会阻塞DOM的解析(尽量使用异步加载)。三者的区别如下:

  • script 立即停止页面渲染去加载资源文件,当资源加载完毕后立即执行js代码,js代码执行完毕后继续渲染页面;
  • async 是在下载完成之后,立即异步加载,加载好后立即执行,多个带async属性的标签,不能保证加载的顺序;
  • defer 是在下载完成之后,立即异步加载。加载好后,如果 DOM 树还没构建好,则先等 DOM 树解析好再执行;如果DOM树已经准备好,则立即执行。多个带defer属性的标签,按照顺序执行。

(2)针对CSS:使用CSS有三种方式:使用link、@import、内联样式,其中link和@import都是导入外部样式。它们之间的区别:

  • link:浏览器会派发一个新等线程(HTTP线程)去加载资源文件,与此同时GUI渲染线程会继续向下渲染代码
  • @import:GUI渲染线程会暂时停止渲染,去服务器加载资源文件,资源文件没有返回之前不会继续渲染(阻碍浏览器渲染)
  • style:GUI直接渲染

外部样式如果长时间没有加载完毕,浏览器为了用户体验,会使用浏览器会默认样式,确保首次渲染的速度。所以CSS一般写在headr中,让浏览器尽快发送请求去获取css样式。

所以,在开发过程中,导入外部样式使用link,而不用@import。如果css少,尽可能采用内嵌样式,直接写在style标签中。

(3)针对DOM树、CSSOM树:

可以通过以下几种方式来减少渲染的时间:

  • HTML文件的代码层级尽量不要太深
  • 使用语义化的标签,来避免不标准语义化的特殊处理
  • 减少CSSD代码的层级,因为选择器是从左向右进行解析的

(4)减少回流与重绘:

  • 操作DOM时,尽量在低层级的DOM节点进行操作
  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局
  • 使用CSS的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。

3.defer和async属性

3.1script元素和页面解析的关系

我们现在已经知道了页面的渲染过程,但是JavaScript在哪里呢?

  • 事实上,浏览器在解析HTML的过程中,遇到了script元素是不能继续构建DOM树的;

  • 它会停止继续构建,首先下载JavaScript代码,并且执行JavaScript的脚本;

  • 只有等到JavaScript脚本执行结束后,才会继续解析HTML,构建DOM树;

为什么要这样做呢?

  • 这是因为JavaScript的作用之一就是操作DOM,并且可以修改DOM;

  • 如果我们等到DOM树构建完成并且渲染再执行JavaScript,会造成严重的回流和重绘,影响页面的性能;

  • 所以会在遇到script元素时,优先下载和执行JavaScript代码,再继续构建DOM树;

但是这个也往往会带来新的问题,特别是现代页面开发中:

  • 在目前的开发模式中(比如Vue、React),脚本往往比HTML页面更“重”,处理时间需要更长;

  • 所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到;

为了解决这个问题,script元素给我们提供了两个属性(attribute):defer和async。

3.2defer属性

表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。在 IE7 及更早的版本中,对行内脚本也可以指定这个属性。 defer 属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree。

  • 脚本会由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程;

  • 如果脚本提前下载好了,它会等待DOM Tree构建完成,在DOMContentLoaded事件之前先执行defer中的代码;

所以DOMContentLoaded总是会等待defer中的代码先执行完成。

另外多个带defer的脚本是可以保持正确的顺序执行的。

从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中;

注意:defer仅适用于外部脚本,对于script默认内容会被忽略

3.3async属性

表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其他脚本加载。只对外部脚本文件有效。 async 特性与 defer 有些类似,它也能够让脚本不阻塞页面。

async是让一个脚本完全独立的:

  • 浏览器不会因 async 脚本而阻塞(与 defer 类似);

  • async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本;

  • async不会能保证在DOMContentLoaded之前或者之后执行;

defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的;

async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的;

二者的区别: 标记为 async 的脚本并不保证能按照它们出现的次序执行,而defer可以按出现次序执行。

实例

<!DOCTYPE html> 
<html> 
 <head> 
 <title>Example HTML Page</title> 
 <script async src="example1.js"></script> 
 <script async src="example2.js"></script> 
 </head> 
 <body> 
 <!-- 这里是页面内容 --> 
 </body> 
</html>

在这个例子中,第二个脚本可能先于第一个脚本执行。因此,重点在于它们之间没有依赖关系。给脚本添加 async 属性的目的是告诉浏览器,不必等脚本下载和执行完后再加载页面,同样也不必等到该异步脚本下载和执行后再加载其他脚本。正因为如此,异步脚本不应该在加载期间修改 DOM。

总结

  • 可以使用 defer 属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出的次序执行。

  • 可以使用 async 属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异步脚本不能保证按照它们在页面中出现的次序执行。

4.浏览器输入url到渲染的过程

c7aa55d6c88f4928d2c54d5559a767e.jpg

5.今日精进

人生,敢闯才有机会;敢拼才有未来。大胆行动起来,想常人不敢想之事,做常人不敢做的决定,选择常人不敢选择的路。不拼一把,怎么知道自己行不行;不博一把,怎么知道自己能不能成功。没有勇气走出第一步,往往就是人生的分水岭,成功是被危险逼出来的,害怕危险,就等于拒绝成功。

浏览器渲染原理好文推荐---传送门

Day31【2022年8月24日】

学习重点: JavaScript的运行原理(上) 认识v8引擎 V8引擎,它是当下使用最广泛的 JavaScript 虚拟机,全球有超过 25 亿台安卓设备,而这些设备中都使用了 Chrome 浏览器,所以我们写的 JavaScript 应用,大都跑在 V8 上。

V8 是由 Google 开发的开源 JavaScript 引擎,是 JavaScript 虚拟机的一种,模拟实际计算机各种功能来实现代码的编译和执行。我们可以简单地把 JavaScript 虚拟机理解成是一个翻译程序,将人类能够理解的 编程语言 JavaScript,翻译成机器能够理解的机器语言。目前主要用在 Chrome 浏览器和 Node.js 中。

1.V8引擎原理

1.1浏览器组成

浏览器内核主要分为两部分:渲染引擎(layout engineer 或 Rendering Engine) 和 JS引擎:

  • 渲染引擎负责取得网页的内容进行布局计和样式渲染,然后会输出至显示器或打印机
  • JS引擎则负责解析和执行JS脚本来实现网页的动态效果和用户交互
  • 最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎。

以webkit为例:

  • WebCore:负责HTML解析、布局、渲染等等相关的工作;
  • JavaScriptCore:解析、执行JavaScrip

总的来说,V8引擎较为激进,青睐可以提高性能的新技术,而JavaScriptCore引擎较为稳健,渐进式的改变着自己的性能。

1.2V8引擎执行原理

官方对V8引擎的定义:

V8 是 Google 的开源高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写。它用于 Chrome 和 Node.js 等。它实现了ECMAScriptWebAssembly,并在 Windows 7 或更高版本、macOS 10.12+ 以及使用 x64、IA-32、ARM 或 MIPS 处理器的 Linux 系统上运行。V8 可以独立运行,也可以嵌入到任何 C++ 应用程序中。

5439c59a9114a50e2fd2a79f5480820.png

1.3v8引擎架构

1.3.1 V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:

1.3.2 Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;

  • 如果函数没有被调用,那么是不会被转换成AST的;
  • Parse的V8官方文档:v8.dev/blog/scanne…

1.3.3 Ignition是一个解释器,会将AST转换成ByteCode(字节码)

  • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
  • 如果函数只调用一次,Ignition会解释执行ByteCode;
  • Ignition的V8官方文档:v8.dev/blog/igniti…

1.3.4 TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;

  • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
  • 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
  • TurboFan的V8官方文档:v8.dev/blog/turbof…

1.3.5 Orinoco 垃圾收集器(GC)

-负责将程序不再需要的内存空间回收。

  • 主要式三个过程识别活/死对象、回收/重用死对象占用的内存、压缩/碎片整理内存(可选) -官方文档:v8.dev/blog/trash-…

垃圾回收详情

7054e9c0998086699154ecdd34721e5.png 打标 确定可以收集哪些对象是垃圾收集的重要组成部分。垃圾收集器通过使用可达性作为“活跃度”的代理来做到这一点。这意味着必须保留当前在运行时内可访问的任何对象,并且可以收集任何无法访问的对象。

标记是发现可达对象的过程。GC 从一组已知对象指针开始,称为根集。这包括执行堆栈和全局对象。然后它跟随每个指向 JavaScript 对象的指针,并将该对象标记为可访问。GC 跟踪该对象中的每个指针,并递归地继续此过程,直到找到并标记运行时中可到达的每个对象。

扫地 清除是一个过程,其中死对象留下的内存间隙被添加到称为空闲列表的数据结构中。标记完成后,GC 会发现无法访问的对象留下的连续间隙,并将它们添加到适当的空闲列表中。空闲列表由内存块的大小分隔,以便快速查找。将来当我们想要分配内存时,我们只需查看空闲列表并找到适当大小的内存块。

压实 主要 GC 还根据碎片启发式方法选择疏散/压缩某些页面。您可以认为压缩有点像旧 PC 上的硬盘碎片整理。我们将幸存的对象复制到当前未压缩的其他页面中(使用该页面的空闲列表)。这样,我们可以利用死对象留下的内存中的小而分散的间隙。

复制幸存对象的垃圾收集器的一个潜在弱点是,当我们分配大量长寿命对象时,我们为复制这些对象付出了高昂的代价。这就是为什么我们选择只压缩一些高度碎片化的页面,而只对其他页面执行清扫,这不会复制幸存的对象。

1.4 v8引擎官方解析图

60d23a50efbfbb31b0a33ac785d3c37.png image.png

2.js执行上下文

js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)

  • 该对象 所有的作用域(scope)都可以访问;
  • 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
  • 其中还有一个window属性指向自己;

js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。那么现在它要执行谁呢?执行的是全局的代码块:

  • 全局的代码块为了执行会构建一个 Global Execution Context(GEC);
  • GEC会 被放入到ECS中 执行;

GEC被放入到ECS中里面包含两部分内容:

  • 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;这个过程也称之为变量的作用域提升(hoisting)

  • 第二部分:在代码执行中,对变量赋值,或者执行其他的函数;

2.1.执行上下文

执行上下文(以下简称“上下文”)的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定 、了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台 处理数据会用到它。

执行上下文可以理解为当前代码的执行环境,它会形成一个作用域。

2.1.1 执行上下文类型类型

(1)全局执行上下文

任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。全局上下文就是我们常说的 window 对象,因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。 组成:

  • 全局对象(浏览器里是 Window, Node 环境下是 Global)
  • this 变量。这里的 this ,指向的还是全局变量

(2)函数执行上下文

当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。 组成:

  • 函数上下文创建参数对象(arguments);
  • this。动态的,如果它被一个引用对象调用,那么 this 就指向这个对象;否则,this 的值会被设置为全局对象或者 undefined(在严格模式下)

(3)Eval 执行上下文

执行在eval函数中的代码会有属于他自己的执行上下文,用的比较少了了解即可,跟面试官吹逼的时候可以用。

2.1.2 执行上下文栈--执行上下文的管理

  • JavaScript引擎使用执行上下文栈来管理执行上下文
  • 当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
function testA() {
        console.log('执行第一个测试函数的逻辑');
        testB();
        console.log('再次执行第一个测试函数的逻辑');
      }
      
      function testB() {
        console.log('执行第二个测试函数的逻辑');
      }
      
      testA();
//执行第一个测试函数的逻辑
//执行第二个测试函数的逻辑
//再次执行第一个测试函数的逻辑
复制代码

执行顺序跟压入栈的顺序相反所以会先执行B-A-全局。 特点:

  • 栈,先进后出
  • 栈底永远是全局执行上下文环境window,其余的都是函数执行上下文
  • 当前正在运行的永远是栈顶的执行上下文

2.1.3 创建执行上下文

创建执行上下文有两个阶段:创建阶段执行阶段

1)创建阶段

(1)this绑定

  • 在全局执行上下文中,this指向全局对象(window对象)
  • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

(2)创建词法环境组件

  • 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
  • 词法环境的内部有两个组件:加粗样式:环境记录器:用来储存变量个函数声明的实际位置外部环境的引用:可以访问父级作用域

(3)创建变量环境组件

  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

2)执行阶段

此阶段会完成对变量的分配,最后执行完代码。

整个过程是一个动态的过程。

简单来说执行上下文就是指:

在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。

3.今日精进

在学习时经常因为要记忆东西太多而忘记,定期复盘、画思维导、输出记忆等方式往往会提升记忆留存率,但记忆方式是术,理解与运用、练习与复习才是道。

执行上下文与我基础部分一致,本篇重新复习一下可到基础部分查看----点击这里

v8引擎好文推荐----点击这里

Day32【2022年8月25日】

学习重点: JavaScript的运行原理(下)

1.浏览器中的 Event-Loop(事件循环)

当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入调用栈。后面每遇到一个函数调用,就会往栈中压入一个新的函数上下文。JS引擎会执行栈顶的函数,执行完毕后,弹出对应的上下文。

1.1 全局代码执行过程

每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中。

每个执行上下文都与其关联为一个变量对象。在源文本中声明的变量和函数作为变量对象的属性添加。对于函数代码,参数被添加为变量对象的属性。 当全局代码被执行的时候,变量对象就是全局对象

全局代码

  • 创建并初始化范围链以包含全局对象,而不包含其他对象。
  • 变量实例化使用全局对象作为变量对象,并使用属性属性{DontDelete}。

这就是是全局对象。

1.2 函数代码执行过程

在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack(函数执行栈)中。

因为每个执行上下文都会关联一个VO(变量对象),那么函数执行上下文关联的VO是什么呢?

  • 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object--激活对象);
  • 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数;
  • 这个AO对象会作为执行上下文的VO来存放变量的初始化; 当控件输入函数代码的执行上下文时,将创建一个称为激活对象的对象,并将其与执行上下文相关联。激活对象使用带有名称参数和属性的属性初始化。这个属性的初始值是下面描述的argument sobject。

然后,激活对象作为变量对象用于变量实例化。

1.3 关联作用域及作用域链

当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)

  • 作用域链是一个对象列表,用于变量标识符的求值;
  • 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;

每个执行上下文都与其关联一个作用域链。作用域链是在计算标识符时研究的对象列表。当控件进入执行上下文时,将创建一个作用域链,并根据代码的类型使用一组初始对象填充。在执行上下文中的执行过程中,执行上下文的作用域链仅受witch和catch的影响。

可看基础篇文章。详情

1.4 浏览器中的 Event-Loop 机制解析

主要三个角色函数调用栈宏任务(macro-task)队列微任务(micro-task)队列JS 的特性是单线程+异步,是为了降低程序复杂性,但同时为了多个事件能同时被处理,JS提供了异步的处理方式(其实JS本身是没有异步这一说法的,都是由执行环境所提供的)

1.4.1同步和异步的区别

  • 同步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会一直等待下去,直到消息返回为止再继续向下执行。
  • 异步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,这个时候进程会继续往下执行,不会阻塞等待消息的返回,当消息返回时系统再通知进程进行处理。

1.4.2事件循环执行理解

因为 js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码时,如果遇到异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务。 所谓“宏任务”与“微任务”,是对任务的进一步细分。具体的划分依据如图所示:

062a9057c114e7ed03b1082daedbefd.png

ab58b6fa1ad1fb0a82a6a62f2501412.png

  • 微任务包括: promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
  • 宏任务包括: script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。

注意:script(整体代码)它也是一个宏任务;此外,宏任务中的 setImmediate、微任务中的 process.nextTick 这些都是 Node 独有的。

执行过程

4e42b9491cb82e3da13ef001b2ea31a.png Event Loop 执行顺序如下所示:

  • 首先执行同步代码,这属于宏任务(在script中)
  • 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
  • 执行所有微任务
  • 当执行完所有微任务后,如有必要会渲染页面
  • 然后开始下一轮 Event Loop,执行宏任务中的异步代码

执行原则一个宏任务,一队微任务。 详细版本:

  1. 执行并出队一个 macro-task。注意如果是初始状态:调用栈空。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。这时首先执行并出队的就是 script 脚本;
  2. 全局上下文(script 标签)被推入调用栈,同步代码执行。在执行的过程中,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。这个过程本质上是队列的 macro-task 的执行和出队的过程
  3. 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的(如下图所示)。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空;
  4. 执行渲染操作,更新界面;
  5. 检查是否存在 Web worker 任务,如果有,则对其进行处理。

针对面试,咱们关注第1-3步就足够了。第4步第5步,面试时说了没错,不说也没人会难为你,不必较劲。

1.4.3 真题演练

console.log(1)

setTimeout(function() {
  console.log(2)
})

new Promise(function (resolve) {
  console.log(3)
  resolve()
 }).then(function () {
  console.log(4)
}).then(function() {
  console.log(5)
})

console.log(6)
//1
//3
//6
//4
//5
//2

首先被推入调用栈的是全局上下文,你也可以理解为是 script 脚本作为一个宏任务进入了调用栈,这个动作同时创建了全局上下文;与此同时,宏任务队列被清空,微任务队列暂时还是空的:

image.png 全局代码开始执行,跑通了第一个console:

console.log(1)

此时输出1。

接下来,执行到 setTimeout 这句,一个宏任务被派发了,宏任务队列里多了一个小兄弟:

image.png 再往下走,遇到了一个 new Promise。大家知道,Promise 构造函数中函数体的代码都是立即执行的,所以这部分逻辑执行了:

console.log(3)
resolve()

第一步输出了3,第二步敲定了 Promise 的状态为 Fullfilled,成功把 then 方法中对应的两个任务依次推入了微任务队列:

image.png 再往下走,就走到了全局代码的最后一句:

console.log(6)

这一步输出了6script脚本中的同步代码就执行完了。 不过大家注意,全局上下文并不会因此消失——它与页面本身共存亡。接下来,咱们就开始往调用栈里推异步任务了。本着“一个宏任务(一般指script中的同步代码执行),一队微任务”的原则咱们现在需要处理的是微任务队列里的所有任务

image.png 首先登场的是 then 中注册的第一个回调,这个回调会输出4

function () {
  console.log(4)
}

接着处理第二个回调:

image.png 这个回调会输出5:

function () {
  console.log(5)
}

如此一来,微任务队列就被清空了:

image.png 我们重新把目光放在宏任务队列上,将其队列头部的一个任务入栈:

image.png 对应的回调执行,输出2

function() {
  console.log(2)
}

执行完毕后,我们就结束了所有任务的处理,两个任务队列都空掉了:

image.png 此时,只剩下一个全局上下文,待你关闭标签页后,它也会跟着被销毁。 这是本题的完整执行过程。

setTimeout(function() {
  console.log(2)
})

new Promise(function (resolve) {
  resolve()
 }).then(function () {
  console.log(4)
}).then(function() {
  console.log(5)
})
//4
//5
//2

script中没同步代码,这相当于直接执行空执行了script代码,再执行微任务,不考虑script宏任务执行的同步代码的情况下,可以理解为微任务始终再宏任务前面(microtask 永远执行在 macrotask 前面)。

补充执行栈:可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。 回调队列是一种FIFO(first input first output)先进先出的数据结构。

2.今日精进

悲观者正确,乐观者成功。试着理解这句话,往往在行动前关注事物不确定性,犹豫、焦虑而不行动的人那他永远不会失败,所以他永远都是“正确”的。而乐观的人,分析过后会去行动。行动的人,就已经“成功”了。因为,无论结果好坏,他都成长了积累了经验。这是一种比结果更重要的“成功”。

Day33【2022年8月26日】

学习重点: Node事件循环机制

1.Node中的Event-Loop与浏览器异同

1.1 Node底层架构图

be76c2444cb2b1cf2c2002c3b0d9775.png Node整体上由这三部分组成:

  • 应用层:这一层就是大家最熟悉的 Node.js 代码,包括 Node 应用以及一些标准库。

  • 桥接层:Node 底层是用 C++ 来实现的。桥接层负责封装底层依赖的 C++ 模块的能力,将其简化为 API 向应用层提供服务。

  • 底层依赖:这里就是最最底层的 C++ 库了,支撑 Node 运行的最基本能力在此汇聚。其中需要特别引起大家注意的就是 V8 和 libuv:

  • node使用V8作为js解析引擎,I/O处理使用了自己设计的libuv,libuv是一个基于事件的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的api,事件循环机制也是它里面的实现。

    • V8 是 JS 的运行引擎,它负责把 JavaScript 代码转换成 C++,然后去跑这层 C++ 代码。
    • libuv:它对跨平台的异步I/O能力进行封装,同时也是我们本节的主角:Node 中的事件循环就是由 libuv 来初始化的。

libuv解释

libuv 是一个用 C 编写的支持多平台的异步 I/O 库,主要解决 I/O 操作容易引起阻塞的问题。 v8本身并没有异步运行的能力,而是借助浏览器的其他线程实现的,这也正是我们常说js是单线程的原因,因为其解析引擎只支持同步解析代码。  但在 Node.js 中,异步实现主要依赖于 libuv。

注意哈:这里第一个区别来了——浏览器的 Event-Loop 由各个浏览器自己实现;而 Node 的 Event-Loop 由 libuv 来实现。

node代码运行机制: ① V8引擎解析JavaScript脚本。

② 解析后的代码,调用Node API。

③ libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

④ V8引擎再将结果返回给用户。

1.2 libuv中的事件循环实现

image.png

  • timers计时器阶段:初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。

  • pending callbacks:直译过来是“被挂起的回调”,如果网络I/O或者文件I/O的过程中出现了错误,就会在这个阶段处理错误的回调(比较少见,可以略过);

  • idle, prepare:仅系统内部使用。这个阶段我们开发者不需要操心。(可以略过)。

  • poll (轮询阶段) :重点阶段,这个阶段会执行I/O回调,同时还会检查定时器是否到期。

    • 当回调队列不为空时:会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回调后,变为下面的情况。
    • 当回调队列为空时(没有回调或所有回调执行完毕):但如果存在有计时器(setTimeout、setInterval和setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的I/O操作完成,并马上执行相应的回调,直到所有回调执行完毕。
  • check(检查阶段) :会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。

  • close callbacks:处理一些“关闭”的回调,比如socket.on('close', ...)就会在这个阶段被触发。

1.3 宏任务与微任务

和浏览器中一样,Node 世界里也有宏任务与微任务之分。划分依据与我们上文描述的其实是一致的:

常见的 macro-task 比如: setTimeout、setInterval、 setImmediate、 script(整体代码)、I/O 操作、UI 渲染等。

常见的 micro-task 比如: process.nextTick、Promise、MutationObserver 等

需要注意的是,setImmediate 和 process.nextTick 是 Node 独有的

1.4 Node 中的事件循环流程

在这六个阶段中,大家需要重点关注的就是 timers、poll 和 check 这 三个阶段,相关的命题也基本上是围绕它们来做文章。不过在进行考点点拨之前,我们还是要把整个循环的流程给走一遍:

  1. 执行全局的 Script 代码(与浏览器无差);
  2. 把微任务队列清空:注意,Node 清空微任务队列的手法比较特别。在浏览器中,我们只有一个微任务队列需要接受处理;但在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个 next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 next-tick 队列中的任务,随后才会清空其它微任务
  3. 开始执行 macro-task(宏任务)。注意,Node 执行宏任务的方式与浏览器不同:在浏览器中,我们每次出队并执行一个宏任务;而在 Node 中,我们每次会尝试清空当前阶段对应宏任务队列里的所有任务(除非达到了系统限制);
  4. 步骤3开始,会进入 3 -> 2 -> 3 -> 2…的循环(整体过程如下所示):
micro-task-queue ----> timers-queue 
                            |
                            |
micro-task-queue ----> pending-queue
                            |
                            |
micro-task-queue ---->  polling-queue
                            |
                            |
micro-task-queue ---->  check-queue
                            |
                            |
micro-task-queue ---->  close-queue
                            |
                            |
micro-task-queue ----> timers-queue 

Node 中每次执行异步任务都是以批量的形式,“一队一队”地执行。循环形式为:宏任务队列 -> 微任务队列 -> 宏任务队列 —> 微任务队列… 这样交替进行。

1.5 nextTick 和 Promise.then

Node 中的 process.nextTick,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。 Node 中 micro-queue 的这个特征:

Node 清空微任务队列的手法比较特别。在浏览器中,我们只有一个微任务队列需要接受处理;但在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个 next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 next-tick 队列中的任务,随后才会清空其它微任务。

一般在面试题的考察过程中process.nextTick会结合Promise.then一起考察。

实例

Promise.resolve().then(function() {
  console.log("promise1")
}).then(function() {
  console.log("promise2")
});

process.nextTick(() => {
 console.log('nextTick1')
 process.nextTick(() => {
   console.log('nextTick2')
   process.nextTick(() => {
     console.log('nextTick3')
     process.nextTick(() => {
       console.log('nextTick4')
     })
   })
 })
})
//nextTick1 
//nextTick2 
//nextTick3 
//nextTick4 
//promise1 
//promise2

不管什么微任务,只要它不是 process.nextTick 派发的,全部都要排队在 process.nextTick 后面执行

1.6 setTimeout 和 setImmediate

setImmediate 该方法用来把一些需要长时间运行的操作放在一个回调函数里(中断长时间运行的操作),在浏览器完成后面的其他语句后,就立刻执行这个回调函数。不推荐在生产环境中使用。 没有延时时间作为入参,它只认一个执行时机——离它最近的那一次 check。

下面来看一个例子,首先在有些情况下,定时器的执行顺序其实是随机

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

对于以上代码来说,setTimeout 可能执行在前,也可能执行在后

  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的setTimeout 这个函数的第二个入参,它的取值范围是 [1, 2^31-1]。也就是说,它是不认识 0 这个入参的。强行给你1。
  • 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间, 那么在 timer 阶段就会直接执行 setTimeout 回调。 (进入了 timers 阶段,发现 setTimeout 定时器已经到时间了,直接执行 setTimeout 回调;结束 timers 阶段后,走啊走,走到了 check 阶段,顺理成章地又执行了 setImmediate 回调。)
  • 那么如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了。(进入了 timers 阶段,却发现 setTimeout 定时器还没到时间,于是往下走。走到 check 阶段,执行了 setImmediate 回调;在后面的循环周期里,才会执行 setTimeout 回调;)

顺序上的差别是由我们不可控的“事件循环初始化时间”导致的。因此上面执行顺序是随机的。

当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})

在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。check 阶段永远比 timers 阶段离 poll 更近,因此 setImmediate 总是比 setTimeout 先执行。 在 poll 阶段处理的回调中,如果既派发了 setImmediate、又派发了 setTimeout,那么这个顺序是板上钉钉的——一定是先执行 setImmediate,再执行 setTimeout。

上面都是 macrotask 的执行情况,对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列。

补充

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


setTimeout(() => {
  console.log('timeout2');
  Promise.resolve().then(function() {
    console.log('promise1');
  });
}, 0);

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

//执行script标签中的内容-微任务-setTimeout(里面的微任务)

这题很险,但各位只要记住一句话:Node11开始,Node的事件循环已经和浏览器趋同。注意是“趋同”而不是一毛一样。其中最明显的改变是:

Node11开始,timers 阶段的setTimeout、setInterval等函数派发的任务、包括 setImmediate 派发的任务,都被修改为:一旦执行完当前阶段的一个任务,就立刻执行微任务队列。

这就意味着,上面这道题,在浏览器和在 Node11 中跑出来的结果一毛一样——不信各位切换到高版本跑跑看。

低于node11结果

timeout1
timeout2
timeout3
promise1

在 timers 阶段,依次执行了所有的 setTimeout 回调、清空了队列——这符合我们前面对 Node 事件循环机制的描述。 符合node一队一队执行的机制

高于node11结果/浏览器内

timeout1
timeout2
promise1
timeout3

只要遇到 Node 事件循环相关的编码类题目,都在答题结束后补充上咱们前面对 Node11 版本的这部分讲解。要灵活变通

2.今日精进

能成事的人,往往极度自律,懂得延迟满足。在大大小小的诱惑面前,影响人一生的是延迟满足的自律能力,坚持自律将彻底改变一个人,改变只是痛苦一时不改变却要痛苦一生。

Day34【2022年8月27日】

今日精进

事在人为,人定胜天,人心再复杂,社会再复杂,用自己的意志,用自己的智慧,用自己的能力,用自己的人格魅力,也能把复杂化为简,将结果变得可控。

Day35【2022年8月28日】

学习重点: 浏览器内存泄漏

1.什么是内存泄漏

内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束,造成系统内存占用越来越大,最终导致程序运行缓慢甚至系统崩溃等严重后果。

在C++中,因为是手动管理内存,内存泄露是经常出现的事情。而现在流行的C#/Java/JavaScript等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄露。浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露。

2.那行情况些会造成浏览器内存泄漏

  • 第一种情况是由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。(在 JavaScript 文件头部加上 ‘use strict’,可以避免此类错误发生。启用严格模式解析 JavaScript
    ,避免意外的全局变量。)
function test() { me = 'xiuyan' }
  • 第二种情况是设置了 setInterval/setTimeout 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。 我们在实现轮询效果时,会用到 setInterval:
setInterval(function() {
    // 函数体
}, 1000);

或者链式调用 setTimeout:

setTimeout(function() {
  // 函数体
  setTimeout(arguments.callee, 1000);
}, 1000);
  • 第三种情况是获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。

const myDiv = document.getElementById('myDiv')

function handleMyDiv() {
    // 一些与myDiv相关的逻辑
}

// 使用myDiv
handleMyDiv()

// 尝试”删除“ myDiv
document.body.removeChild(document.getElementById('myDiv'));

DOM就像一棵双链接树,这意味着对树中某个节点的引用将使整个树停止进行垃圾收集。 让我们以在javascript中创建DOM元素为例。创建元素后,销毁它,但是忘记删除保存它的变量。这种情况导致分离的DOM,它不仅引用特定的DOM元素,而且还引用整个树。

为了避免内存泄漏,最佳实践是将myDiv放在侦听器中,这使其成为局部变量。当删除myDiv时,对象的路径将被切断,从而使垃圾回收器释放内存。

  • 第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。 如以下实例:
var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 'originalThing'的引用
      console.log("嘿嘿嘿");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("哈哈哈");
    }
  };
};
setInterval(replaceThing, 1000);

理解上面的代码有一个关键点在 V8 中,一旦不同的作用域位于同一个父级作用域下,那么它们会共享这个父级作用域。

在这段代码里, unused 是一个不会被使用的闭包,但和它共享同一个父级作用域的 someMethod,则是一个 “可抵达”(也就意味着可以被使用)的闭包。unused 引用了 originalThing,这导致和它共享作用域的 someMethod 也间接地引用了 originalThing。结果就是 someMethod “被迫” 产生了对 originalThing 的持续引用,originalThing 虽然没有任何意义和作用,却永远不会被回收。不仅如此,originalThing 每次 setInterval 都会改变一次指向(指向最近一次的 theThing 赋值结果),这导致无法被回收的无用 originalThing 越堆积越多,最终导致严重的内存泄漏。

内存泄漏其实并不是啥高深的命题,导致内存泄露的,往往是低级错误。说得直接点,那就是代码功夫不到家。

今日精进

思考为人处事方式:处事稳健,为人保持低调,在是是非非中打磨自身的性格,磨去棱角,与什么样的人相处说什么话,多听少说,话到嘴边留三分。

参考资料

  • JavaScript高级程序设计(第4版)
  • MDN
  • coderwhy大神资料参考
  • 解锁前端面试体系核心攻略

结语

志同道合的小伙伴可以加我,一起交流进步,我们坚持每日精进(互相监督思考学习,如果坚持不下来我可以监督你)。我们一起努力鸭! ——>点击这里

备注

按照时间顺序倒叙排列,完结后按时间顺序正序排列方便查看知识点,工作日更新。