0%

现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。

标记清除

先认为所有对象都是要清除的,然后遍历正在使用的那些对象,以及由那些对象所引用的其他对象。所有这些对象都找出来之后,剩下的就是没有在使用的对象了,就可以被垃圾回收了。

引用计数

这种方式常常会引起内存泄漏,低版本的IE使用这种方式。机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时就会被回收。

定义

MDN对闭包的定义:

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

换种好理解的说法:

闭包是指那些能够访问自由变量的函数。自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

具体可以从两个角度来看什么函数是闭包:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:闭包就是能够读取其他函数内部变量的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

闭包的用途

  1. 可以读取函数内部的变量;
  2. 让变量的值始终保持在内存中;

为什么闭包会造成内存泄漏?

早期的IE,无法自动回收闭包引用的变量,变量用不上又回收不掉自然就造成了内存泄漏。其实这个问题本身就是有问题的,闭包不会造成内存泄漏,程序写错了才会造成内存泄漏。

模拟过程

A:听得到吗?

B:听得到,你呢?(B在回复的时候,说明A发送信息没问题,B接收信息没问题)

A:听得到。(A在回复的时候,说明B发送信息没问题,A接收信息没问题)

。。。

。。。

。。。

A:我要闭嘴了,确认请挥手。

B:🙋‍♂️。(B这里挥手示意收到A消息,说明A发送信息没问题,B接收信息没问题)

B:我也要闭嘴了,确认请挥手。(这里B回复A,跟B挥手示意不是一次进行,是因为在B在收到A消息的时候,可能手上的事情还没做完,等做完了再回复)

A:🙋‍♂️。(这里A挥手,说明B发送信息没问题,A接收信息没问题)

科普

TCP 协议通过三次握手的形式建立一个可靠的连接,建立成功之后开始发送数据。发送数据完了之后,会以四次挥手的形式来终止连接。

一、描述一下 TCP 连接三次握手的过程?

  • 第一次握手:客户端尝试连接服务器,向服务器发送 SYN (Synchronization 的缩写)包,并且设置一个序列号 seq(Sequence 的缩写),假设 seq=m;发送完成之后,客户端进入 SYN_SEND 状态等待服务器确认;

  • 第二次握手:服务端接收客户端 syn 包,做出回应;服务端会回送 SYN 和 ACK 给客户端。ACK 的全写是 acknowledgment ,意为答复,ACK 的值是根据客户端发送过来的 seq 来决定的,发给给客户端时,ACK 的值是根据客户端发送的 syn=m 来确认的(假设在 m 的基础上 +1),这样客户端收到回应的时候就能准确知道是服务端的回应了;服务端回应里里的 seq = n 是服务端自己生成的,给客户端进行回应使用的。发送完成之后,服务器进入 SYN_RCVD 状态;

  • 第三次握手:客户端收到服务端的 SYN+ACK 包,向服务端发送确认包 ACK(ack=n+1,根据刚刚收到的服务端发送的 seq = n 确定的),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,表示连接成功,完成三次握手,这个时候就可以传输数据了。

img

二、为什么握手需要三次,两次不行吗?

三次握手的目的是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

“已失效的连接请求报文段”的产生在这样一种情况下:client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据。这样,server 的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。—— 谢希仁版《计算机网络》

三、描述一下四次挥手的过程?

  • 第一次挥手(FIN=1,seq=x):假设客户端想要关闭连接,客户端发送一个 FIN (Finish 的缩写,表示结束)标志位 置为 1 的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。发送完毕后,客户端进入 FIN_WAIT_1 状态。

  • 第二次挥手(ACK=1,ACKnum=x+1):服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

  • 第三次挥手(FIN=1,seq=y):服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为 1。发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个 ACK。

  • 第四次挥手(ACK=1,ACKnum=y+1):客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

  • 最后,客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。

img

四、为什么挥手需要四次,三次不行吗?

握手的时候,客户端和服务端打个招呼,服务端可以直接把自己的 SYN 信息和对客户端的回应 ACK 信息一起带上,但是挥手的时候,客户端说我要断开了,服务端还没发完最后的数据,因此需要先回应一下客户端:我收到你的断开的请求了,但是你要等我把最后的内容给你,所以这里分开了 2 步:

  • 回应客户端;
  • 发送自己的最后一个数据。

五、为什么四次挥手里,客户端进入 TIME_WAIT 状态后,需要等待最大报文段生存的时间后,才能关闭?

客户端在发送完 ACK 报文段后,再经过 2MSL 时间,就可以使连接持续的时间所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求的报文段。

  1. DNS 域名解析;

  2. 建立 TCP 连接(三次握手);

  3. 发送 HTTP 请求;

  4. 服务器处理请求;

  5. 返回响应结果;

  6. 关闭 TCP 连接(四次挥手);

  7. 浏览器解析 HTML;

  8. 浏览器布局渲染(重排和重绘);

网页的生成过程是怎样的?

img

在这个过程中:

  1. HTML 被 HTML 解析器解析成 DOM 树

  2. CSS 则被 CSS 解析器解析成 CSSOM 树

  3. 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree)

  4. 生成布局(flow),即将所有渲染树的所有节点进行平面合成

  5. 将布局绘制(paint)在屏幕上

其中,第 4 步和第 5 步是最耗时的部分,这两步合起来就是我们通常所说的渲染。在网页生成的过程中,至少会渲染一次;并且,在用户操作界面的过程中,还会不断地重新渲染。也就是会不断地发生重排和重绘。

什么是重排?

当渲染树 Render tree 中的一部分(或全部)因为 DOM 的变化影响了元素的几何信息(DOM 对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,并将其安放在界面中的正确位置,这个过程就称为重排(reflow),重排也叫作回流。

什么是重绘?

当渲染树 Render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,而没有影响到布局,例如改变 visibility、outline、背景色等属性,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观,这个过程就称为重绘(repaint)。

重排与重绘的关系是怎样的?

重绘不会引起重排,但重排一定会引起重绘。一个元素的重排通常会带来一系列的反应,甚至触发整个文档的重排和重绘,性能代价是高昂的。

什么情况会触发重排和重绘?

1、触发重排的条件

  • 页面渲染初始化时;(这个无法避免)

  • 浏览器窗口改变尺寸;

  • 元素尺寸改变时;

  • 元素位置改变时;

  • 元素内容改变时;

  • 添加或删除可见的 DOM 元素时。

2、触发重绘的条件

常见的引起重绘的属性:

  • color

  • border-style

  • visibility

  • background

  • text-decoration

  • background-image

  • background-position

  • background-repeat

  • outline-color

  • outline

  • outline-style

  • border-radius

  • outline-width

  • box-shadow

  • background-size

display: none 和 visibility: hidden 有什么区别?

  • 通过 display: none 隐藏一个 DOM 节点,会触发重排和重绘;
  • 通过 visibility: hidden 隐藏一个 DOM 节点,只触发重绘,因为没有几何变化。

浏览器的渲染队列是什么意思?

当我们修改了元素的几何属性,导致浏览器触发重排或重绘时,它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。

以下代码将会触发几次渲染?

1
2
3
4
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

根据前面的描述,这段代码理论上会触发4次重排+重绘,因为每一次都改变了元素的几何属性。但实际上最后只触发了一次重排,这都得益于浏览器的渲染队列机制。

但是如果我们像下面这样:

1
2
3
4
5
6
7
8
div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);

这段代码就会触发4次重排+重绘,因为在 console 中请求了这几个样式信息,无论何时浏览器都会立即执行渲染队列的任务(强制刷新队列),即使该值与操作中修改的值没关联。

强制刷新队列的 style 样式请求:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight

  • scrollTop、scrollLeft、scrollWidth、scrollHeight

  • clientTop、clientLeft、clientWidth、clientHeight

  • getComputedStyle()、 或者 IE 的 currentStyle

我们在开发中应当避免这些能够强制刷新队列的操作。

如何优化重排?

1、读写分离

还是拿上面的强制刷新队列的例子,如果我们改成这样:

1
2
3
4
5
6
7
8
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);

这次只触发了一次重排,因为:在第一个 console 的时候,浏览器把之前上面四个写操作的渲染队列都给清空了;剩下的 console,因为渲染队列本来就是空的,所以并没有触发重排,仅仅拿值而已。

2、样式集中改变

将多次改变样式属性的操作合并成一次操作,减少 DOM 访问

1
2
3
4
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

优化:

1
el.className += " className"; // 直接改变 class

3、离线操作

如果要批量添加 DOM,可以让要操作的元素进行「离线处理」,处理完后一起更新。离线处理的意思是:

  • 隐藏要操作的 DOM

  • 在要操作 DOM 之前,通过 display 属性隐藏 DOM,当操作完成之后,再将元素的 display 属性为可见,因为不可见的元素不会触发重排和重绘。

  • 通过使用 DocumentFragment 创建一个DOM 碎片,在它上面批量操作 DOM,操作完成之后,再添加到文档中,这样只会触发一次重排。

4、position 属性的应用

将需要多次重排的元素,position 属性设为 absolute 或 fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

5、在内存中构建

在内存中多次操作节点,完成后再添加到文档中去。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的 html 片段,再一次性添加到文档中去,而不是直接操作 DOM,循环添加每一行。

关键词:事件冒泡和目标元素。

科普:

一个事件触发后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

事件冒泡: 当一个元素上的事件被触发的时候,比如说鼠标点击了一个按钮,同样的事件将会在那个元素的所有的祖先元素中被触发。这个事件从事件的原始元素开始一直冒泡到DOM树最上层。

目标元素:任何事件的目标元素都是最开始的那个元素,在老IE下,目标元素是window.event.srcElement,其他浏览器event.target。

使用

把事件处理器添加到一个父级元素上,等待一个事件从它的子级里冒泡上来,并且可以得知这个事件是从哪个元素开始的。

优点:

  1. 可以大量节省内存占用,减少事件注册;

  2. 在DOM更新后无需重新绑定事件处理器。

缺点:

  1. 事件冒泡的过程也需要耗时,越靠近顶层,事件的”事件传播链”越长,也就越耗时;

  2. 不是所有的事件都是能冒泡的,如blur、focus、load和unload。也不是所有事件都适合用事件代理的,如mousemove事件触发非常频繁会导致性能瓶颈,mouseout怪异的表现很难用事件代理来管理;

  3. 从浏览器的角度,相当于将父元素标记了一个非快速滚动区域,浏览器合成线程中将注册了事件浏览器的区域标记为非快速滚动区域。当用户事件发生在这些区域时,合成线程会将输入事件发送给主线程来处理。如果输入事件不是发生在非快速滚动区域,合成线程就无需主线程的参与来合成一个新的帧。输入事件代表着来自于用户的任何手势动作,所以用户滚动页面、触碰屏幕以及鼠标移动等操作都是输入事件。当用户的输入事件不是需要主线程处理的,比如在非快速滚动区域做了一个滚动的操作,合成线程每次都会告知主线程并且等主线程处理完才开始干活,用户体验流畅度会有影响。为了减轻这种情况的发生,可以为事件监听器传递pass: true选项。这个选项会告诉浏览器仍要在主线程中监听事件,合成线程页可以继续合成新的帧。

  1. 额外标签法:在最后一个浮动标签后,新加一个标签,给其设置clear:both;
  1. 父级添加overflow属性:父元素添加overflow:hidden/auto;

  2. 使用after伪元素清除浮动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
优点:符合闭合浮动思想,结构语义化正确

缺点:ie6-7不支持伪元素:after,使用zoom:1触发hasLayout.
.clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/
content: "";
display: block;
clear:both;
}
.clearfix{
zoom: 1;/*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
}

<body>
<div class="father clearfix">
<div class="big">big</div>
<div class="small">small</div>
<!--<div class="clear">额外标签法</div>-->
</div>
<div class="footer"></div>

优点:符合闭合浮动思想,结构语义化正确。

缺点:ie6-7不支持伪元素:after,使用zoom:1触发hasLayout。

  1. 父级div定义height;

  2. 父级div也一起浮动;

追本溯源

1.为什么要用浮动?

以前提出浮动的概念,主要是为了解决左边图片右边文字的需求,后来浮动在网页布局上应用比较广泛。

2.什么情况又要清除浮动?

当子元素都设置浮动之后,而父元素本身又没有设置宽高(设置浮动前宽高是由子元素撑起来的),会导致父元素的高度塌陷,原来被撑起来的height被置为0,最终就会影响父元素的布局。

说到底,就是因为所有的子元素都浮动,会导致父元素高度坍塌影响布局。在这种情况下,就要清除浮动。

  1. 用正确的标签做正确的事情;

  2. HTML 语义化让页面的内容结构化,结构更清晰,便于对浏览器,搜索引擎解析;

  3. 即使在没有样式 CSS 情况下也以一种文档格式显示,并且是容易阅读的;

  4. 搜索引擎的爬虫也依赖于 HTML 标记确定上下文和各个关键字的权重,利于 SEO;

  5. 使阅读源代码的人更容易将网站分块,便于阅读维护理解。

时间复杂度

科普:

算法的时间复杂度是一个函数,它定性描述该算法的运行时间。算法的时间复杂度通常用大O符号表述,定义为T[n] = O(f(n))。大O表示法是一种特殊的表示法,指出了算法的速度有多快。

从快到慢列出常见5种大O运行时间:

O(log n):也叫对数时间,这样的算法包括二分查找;

O(n):也叫线性时间,这样的算法包括简单查找;

O(n * log n):这样的算法包括快速排序;

O(n2):这样的算法包括选择排序;

O(n! ):这样的算法包括旅行商问题的解决方案。

注意:

  • 大O表示法指出了最糟情况下的运行时间;

  • 算法的速度指的并非时间,而是操作数的增速;

  • 谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加;

  • 算法的运行时间用大O表示法表示。

空间复杂度

科普

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。

注意:

  • 通常来说,只要算法不涉及到动态分配的空间以及递归、栈所需的空间,空间复杂度通常为0(1);
  • 算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。

常见排序算法的时间复杂度和空间复杂度

img

冒泡排序

1
2
3
4
5
6
7
8
9
10
11
function bubbleSort (arr) {
let length = arr.length
for(let i = 0; i < length; i++) {
for(let j = 0; j < length - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr
}

选择排序

1
2
3
4
5
6
7
8
9
10
11
function selectSort (arr) {
let length = arr.length
for(let i = 0; i < length; i++) {
for (let j = i + 1; j < length; j++) {
if (arr[j] < arr[i]) {
[arr[i], arr[j]] = [arr[j], arr[i]]
}
}
}
return arr
}

快速排序

1
2
3
4
5
6
7
8
9
10
11
function quickSort (arr) {
if (arr.length <= 1) { return arr }
let midIndex = Math.floor(arr.length / 2)
let midValue = arr.splice(midIndex, 1)
let left = []
let right = []
for (let i = 0; i< arr.length; i++) {
arr[i] < midValue ? left.push(arr[i]) : right.push(arr[i])
}
return quickSort(left).concat(midValue, quickSort(right))
}

插入排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function insertionSort(arr) {
var length = arr.length;
var preIndex, current;
for(var i=1; i<length; i++){
preIndex = i-1;
current = arr[i];
while (preIndex>=0 && arr[preIndex]>current){
arr[preIndex+1] = arr[preIndex]
preIndex--
}
arr[preIndex+1] = current
}
return arr;
}