前端面试总结与思考

1、谈谈你对tcp三次握手和四次挥手的理解?

序列号seq:占4个字节,用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,第一个字节的编号由本地随机产生;给字节编上序号后,就给每一个报文段指派一个序号;序列号seq就是这个报文段中的第一个字节的数据编号。

确认号ack:占4个字节,期待收到对方下一个报文段的第一个数据字节的序号;序列号表示报文段携带数据的第一个字节的编号;而确认号指的是期望接收到下一个字节的编号;因此当前报文段最后一个字节的编号+1即为确认号。

确认ACK:占1位,仅当ACK=1时,确认号字段才有效。ACK=0时,确认号无效

同步SYN:连接建立时用于同步序号。当SYN=1,ACK=0时表示:这是一个连接请求报文段。若同意连接,则在响应报文段中使得SYN=1,ACK=1。因此,SYN=1表示这是一个连接请求,或连接接受报文。SYN这个标志位只有在TCP建产连接时才会被置1,握手完成后SYN标志位被置0。

终止FIN:用来释放一个连接。FIN=1表示:此报文段的发送方的数据已经发送完毕,并要求释放运输连接

PS:ACK、SYN和FIN这些大写的单词表示标志位,其值要么是1,要么是0;ack、seq小写的单词表示序号。

字段 含义

URG 紧急指针是否有效。为1,表示某一位需要被优先处理

ACK 确认号是否有效,一般置为1。

PSH 提示接收端应用程序立即从TCP缓冲区把数据读走。

RST 对方要求重新建立连接,复位。

SYN 请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1

FIN 希望断开连接。

三次握手过程理解

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

四次挥手过程理解

1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

常见面试题

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

【问题3】为什么不能用两次握手进行连接?

答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。

现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

2、react中setState什么时候是同步的,什么时候是异步的?

setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。

合成事件:就是react 在组件中的onClick等都是属于它自定义的合成事件

原生事件:比如通过addeventListener添加的,dom中的原生事件

setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。

setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

 state = { val: 0 }
  batchUpdates = () => {
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
 }

其实val只是1

具体大家可以看看react的源码~

————————————————

3、介绍下重绘和回流,以及如何进行优化?

回流(Reflow) 和 重绘(Repaint) 可以说是每一个web开发者都经常听到的两个词语,我也不例外,可是我之前一直不是很清楚这两步具体做了什么事情。而且很尴尬的是每每提到性能优化的时候,我们可以说出 减少回流及其重绘 可以提高页面性能,当然但是一深入问到有什么方式呢?可能就说不出具体体现了,所以整理一下有关这方面的知识 ↓

从浏览器的渲染的过程出发

从上面这个图上,我们可以看到,浏览器渲染过程如下:

解析HTML,生成DOM树,解析CSS,生成CSSOM树

将DOM树和CSSOM树结合,生成渲染树(Render Tree)

Layout(回流): 根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)

Painting(重绘): 根据渲染树以及回流得到的几何信息,得到节点的绝对像素

Display(展示): 将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层,这里我们不展开详细说明)

渲染过程看起来很简单,让我们来具体了解下每一步具体做了什么。

生成渲染树

为了构建渲染树,浏览器主要完成了以下工作:

从DOM树的根节点开始遍历每个可见节点。

对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。

根据每个可见节点以及其对应的样式,组合生成渲染树。

第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:

一些不会渲染输出的节点,比如<script>、<meta>、<link>等。

一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。

从上面的例子来讲,我们可以看到span标签的样式有一个display:none,因此,它最终并没有在渲染树上。

注意:渲染树只包含可见的节点

回流(Reflow)

前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。

为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历,我们可以以下面这个实例来表示:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值。(如下图)

重绘(Repaint )

最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

既然知道了浏览器的渲染过程后,我们就来探讨下,何时会发生回流重绘。

何时发生回流重绘

我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

添加或删除可见的DOM元素

元素的位置发生变化

元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)

内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代

页面一开始渲染的时候(这肯定避免不了)

浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点

注意:回流一定会触发重绘,而重绘不一定会回流

浏览器的渲染机制、优化机制及其处理动画流程

1. 浏览器渲染机制

浏览器采用流式布局模型(Flow Based Layout)

浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree)

有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上

由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一

2. 浏览器优化机制

现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值

主要包括以下属性或方法:

offsetTop、offsetLeft、offsetWidth、offsetHeight

scrollTop、scrollLeft、scrollWidth、scrollHeight

clientTop、clientLeft、clientWidth、clientHeight

width、height

getComputedStyle()、getBoundingClientRect()

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来

3. 浏览器处理动画流程

浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree)

分割图层:浏览器根据z-index,和脱离了原来dom层的dom解构进行分层

计算样式:分解图层完毕后,将所有的图层批量进行样式计算。这里有些属性是CPU去计算的,有些属性是GPU去计算的

reflow -> relayout -> paint set up -> repaint:这一系列过程其实是页面从回流到重绘发生的步骤,这也是为什么回流必然引起重绘,而重绘却不一定要回流的原因

GPU:重绘后的“画布”交给GPU去处理

组合布局:浏览器组合布局,然后页面就出现了

如何减少回流和重绘

CSS

使用 transform 替代 top

使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)

避免使用table布局,可能很小的一个小改动会造成整个 table 的重新布局

尽可能在DOM树的最末端改变class,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点

避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多

<div>
  <a> <span></span> </a>
</div>
<style>
  span {
    color: red;
  }
  div > a > span {
    color: red;
  }
</style>

对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平

将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame

避免使用CSS表达式,可能会引发回流

将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层

CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能

我们可以先看个 demo例子 。我通过使用chrome的Performance捕获了动画一段时间里的回流重绘情况,实际结果如下图

从图中我们可以看出,在动画进行的时候,有Layout(回流),既然有回流当然会有painting(重绘)。但是按理论来说使用是没有回流及重绘的,至于这点我查阅了一下其他资料也并没有相关于这方面的说明,如果有我会及时更新上来,亦或许可能是我测试有问题的原因吧,暂不纠结我们需要知道css3硬件加速是可以提升页面性能的 ~

重点

使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘

对于动画的其它属性,比如background-color这些,还是会引起重绘的,但是不会引起回流,但在性能面前它还是可以提升的

css3硬件加速的缺点

当然,css3硬件加速还是有坑的:

如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。

在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

JavaScript

避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性

代码展示:

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';

有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。当然,大部分现代浏览器都对其做了优化,因此,只会触发一次重排。但是如果在旧版的浏览器或者在上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次重排,因此我们可以合并所有的改变然后依次处理,比如我们应该改成

const el = document.getElementById('test');

el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';

避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中

代码展示:

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
  li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
const ul = document.getElementById('list');
appendDataToElement(ul, data);

使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档,所以应该改成 ↓

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);

避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来

代码展示:

function initBox() {

for (let i = 0; i < paragraphs.length; i++) {

paragraphs[i].style.width = box.offsetWidth + 'px';

}

}

这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为 ↓

const width = box.offsetWidth;
function initBox() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}

对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流

在这对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流,代码可以看页面 → demo例子

打开这个例子后,我们可以打开控制台,控制台上会输出当前的帧数(虽然不准)。但是我们发现帧数一直都没到60。

我们还可以参考 Performance性能火焰图,可以看出当我们动画不是决定定位的时候,从图中可以看到Rendering(渲染计算,包括回流)和Painting(重绘)在录制的性能阶段一直处于高峰,从环状图也可以看出,这个回流(Layout,可以放大每一个Rendering就可以看到)一直在进行计算着

当我们点击按钮后,就是将动画脱离文档流后,控制台的fs帧数就可以稳定60啦,我们再去抓取动画的Performance ,此时我们可以看到 Rendering和Painting 所占的比例很少了,可见动画设置为绝对定位脱离文档流,可大大优化我们页面的性能!

整理

最后整理一下css中哪些样式属性会导致回流和重绘,及其什么时候开启GPU加速

触发回流

1)盒子模型相关属性:

width * height

offset * client * scroll

padding * margin

border * border-width

min-height(width) * max-height(width)

display

2)定位和浮动

top * bottom * left * right

position

float

clear

3)改变节点内部文字结构

text-align * line-height * vertical-align

overflow * overflow-y * overflow-x

font-family * font-size * font-weight

white-space

触发重绘

border-style * border-radius

visibility * text-decoration

color * background * background-image * background-position * background-repeat * background-size

outline-color * outline * outline-style * outline-width

box-shadow

触发GPU加速

概念:

创建了新的layer,属性改变直接由GPU处理,加快处理速度。使得有一些属性的改变可以略过relayout(回流计算),减少浏览器在动画运行时所需要做的工作

缺点:GPU使用会增加内存消耗,同时也会影响字体的抗锯齿效果,导致文本在动画期间会显得有些模糊

以 chrome浏览器为例,符合以下情况,则会创建一个独立的layer:

1)transform(3d转换)

2) video标签

3)混合插件(如 Flash)

4) isolation == isolate

5)opacity < 1

6)filter != normal

7)z-index != auto || 0 + 父元素display: flex|inline-flex

8)mix-blend-mode != normal

9)position == fixed || absolute

10)-webkit-overflow-scrolling == touch

11)will-change:指定可以形成新layer的元素

————————————————

4、聊聊redux和vuex的共同点与区别?

相同点:

1.都是通过store来作为全局状态存储对象

  1. 改变store的直接方法(vuex中的mutation和redux中的reducer)只允许同步操作。

不同点

  1. vuex只有展示组件(通过全局根部植入直接访问store),而redux中展示组件通过容器组件连接store再进行访问。 另外vuex自带module化功能,而redux是没有的。
  2. vuex中消除了action的概念
  3. vuex只能配合vue而redux可以配合任何框架
  4. vuex中的异步操作只能在action中进行,而redux中没有特别的为异步操作创建一个方法。

【其他一些补充】

vuex中改变store的唯一方法就是通过mutation,异步方法通过action最后也是通过mutation来改变store。这里说下为什么vuex要用action,个人理解是因为所有异步函数是不能追踪的,由于vuex需要通过mutation记录每次store的变化,因此mutation中不允许有异步操作就像redux中的reducer中的操作必须也是同步的一样。

5、为什么前端要提倡模块化开发

前端的发展总会让我们眼前一亮,这又有什么规范出来了,上个规范我还没理解透彻呢。但不管未来怎么发展,了解历史还是非常重要的,以史为镜,可以知得失。知道了规范的发展历史,才能更好的了解目前的规范。

没有模块化,前端代码会怎么样?
  • 变量和方法不容易维护,容易污染全局作用域
  • 加载资源的方式通过script标签从上到下。
  • 依赖的环境主观逻辑偏重,代码较多就会比较复杂。
  • 大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人奔溃。
当年我们是怎么引入资源的。

script.png

看着上面的script标签,是不是有一种很熟悉的感觉。通过script引入你想要的资源,从上到下的顺序,这其中顺序是非常重要的,资源的加载先后决定你的代码是否能够跑的下去。当然我们还没有加入defer和async属性,不然加载的逻辑会更加复杂。这里引入的资源还是算少的,过多的script标签会造成过多的请求。同时项目越大,到最后依赖会越来越复杂,并且难以维护,依赖模糊,请求过多。全局污染的可能性就会更大。那么问题来了,如何形成独立的作用域?

defer和async的区别

defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

模块化的基石

立即执行函数(immediately-invoked function expression),简称IIFE,其实是一个javaScript函数。可以在函数内部定义方法以及私有属性,相当于一个封闭的作用域。例如下面的代码:

let module = (function(){
    let _private = 'myself';
    let fun = () =>{
        console.log(_private)
    }
    return {
        fun:fun
    }
})()
module.fun()//myself
module._private//undefined

以上代码便可以形成一个独立的作用域,一定程度上可以减少全局污染的可能性。这种写法可是现代模块化的基石。虽然能够定义方法,但是不能定义属性,这时候各种前端规范就陆续登场了。

首先登场的是common.js

最先遵守CommonJS规范是node.js。这次变革让服务端也能用js爽歪歪的写了,我们的javaScript并不止于浏览器,服务端也能分一杯羹,被人称为模块化的第一座里程碑。想想长征二万五,第一座里程碑在哪里?

CommomJS模块的特点
  • 模块内的代码只会运行在模块作用域内,不会污染到全局作用域
  • 模块的可以多次引入,但只会在第一次加载的时候运行一次,后面的运行都是取缓存的值。想要让模块再次运行,必须清楚缓存。
// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})
  • 模块的加载顺序,遵循在代码中出现的顺序。
为什么要少用exports

exports只是一个变量,指向module.exports,也就是exports只是一个引用而已。所以对外输出模块的时候,我们就可以通过exports添加方法和和属性。通过module.exports对外输出其实也是读取module.exports的变量。但是使用exports时要非常的小心,因为稍不注意就会切断和module.exports的联系。例如:

exports = function(x) {console.log(x)};

上面的代码运行之后,exports不再指向module.exports。如果你难以区分清楚,一般最好就别用exports,只使用module.exports就行。

怎么区分模块是直接执行,还是被调用执行。

require.mainAPI就有这样的作用,如果模块是直接执行,那么这时require.main属性就指向模块本身。例如下面:

require.main === module
为什么客户端不使用commonjs规范?

我们知道客户端(浏览器)加载资源主要是通过网络获取,一般本地读取的比较少,而node.js主要是用于服务器编程,模块文件一般都存在于本地硬盘上,然后I/O读取是非常快速的,所以即使是同步加载也是能够适用的,而浏览器加载资源必须通过异步获取,比如常见的ajax请求,这时候AMD规范就非常合适了,可以异步加载模块,允许回调函数。

客户端的规范不仅仅只有AMD,还有CMD.

每个规范的兴起背后总有一些原因,requirejs的流行是因为commonjs未能满足我们需要的效果,sea.js被创造的原因也是因为requirejs不能满足一些场景。

AMD和CMD的区别

-

AMD

CMD

原理

define(id ?,dependencies ?,factory)定义了一个单独的函数“define”。id为要定义的模块。依赖通过dependencies传入factory是一个工厂参数的对象,指定模块的导出值。

CMD规范与AMD类似,并尽量保持简单,但是更多的与common.js保持兼容性。

优点

特别适用于浏览器环境的异步加载 ,且可以并行加载。依赖前置,提前执行。定义模块时就能清楚的声明所要依赖的模块

依赖就近,延迟执行。按需加载,需要用到时再require

缺点

开发成本较高,模块定义方式的语义交为难理解,不是很符合通过的模块化思维方式。

依赖SPM打包,模块的加载主观逻辑交重。

体现

require.js

sea.js

ES6让前端模块化触手可及
概念

ES6的模块不是对象,import语法会被JavaScript引擎静态分析,请注意,这是一个很重要的功能,我们通常使用commonjs时,代码都是在运行时加载的,而es6是在编译时就引入模块代码,当然我们现在的浏览器还没有这么强大的功能,需要借助各类的编译工具(webpack)才能正确的姿势来使用es6的模块化的功能。也正因为能够编译时就引入模块代码,所以使得静态分析就能够实现了。

ES6模块化有哪些优点
  • 静态化编译 如果能够静态化,编译的时候就能确定模块的依赖关系,以及输出和输入的变量,然后CommonJS和AMD以及CMD都只能在运行代码时才能确定这些关系。
  • 不需要特殊的UMD模块化格式 不再需要UMD模块的格式,将来服务器和浏览器都会支持ES6模块格式。目前各种工具库(webpack)其实已经做到这一点了。
  • 目前的各类全局变量都可以模块化 比如navigator现在是全局变量,以后就可以模块化加载。这样就不再需要对象作为命名空间。
需要注意的地方
  • export语句输出的接口,通过import引入之后,与其对应的值是动态的绑定关系,也就是模块的内的值即使改变了,也是可以取到实时的值的。而commonJS模块输出的是值的缓存,不存在动态更新。
  • 由于es6设计初衷就是要静态优化,所以export命令不能处于块级作用域内,如果出现就会报错,所以一般export统一写在底部或则顶层。
function fun(){
  export default 'hello' //SyntaxError
}
  • import命令具有提升效果,会提升到整个模块的头部首先执行。例如:
fun()
import {fun} from 'myModule';

上面的代码import的执行早于fun调用,原因是import命令是编译阶段执行的,也就是在代码运行之前。

export default使用

export default就是输出一个叫default的变量或方法,然后系统允许你为它取任意名字。所以,你可以看到下面的写法。

//modules.js
function add(x,y){
  return x*y
}
export {add as default};
//等同于
export default add;

//app.js
import {default add foo} from 'modules';
//等同于
import foo from 'modules'

这是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

特别技巧侦查代码是否处于ES6模块中

利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。

const isNotModuleScript = this !== undefined;
6、在输入框中如何判断输入的是一个正确的身份证

var test=/^

\d{15}(\d\d[0-9Xx])?

$/

7、实现一个sleep函数?

sleep函数作用是让线程休眠,等到指定时间在重新唤起。

方法一:这种实现方式是利用一个伪死循环阻塞主线程。因为JS是单线程的。所以通过这种方式可以实现真正意义上的sleep()。

function sleep(delay) {
  var start = (new Date()).getTime();
  while ((new Date()).getTime() - start < delay) {
    continue;
  }
}
function test() {
  console.log('111');
  sleep(2000);
  console.log('222');
}
test()

方法二:定时器

function sleep1(ms, callback) {
                setTimeout(callback, ms)
            }
            //sleep 1s
            sleep1(1000, () => {
                console.log(1000)
            })

方法三:es6异步处理

const sleep = time => {
 return new Promise(resolve => setTimeout(resolve,time)
 ) } 
 sleep(1000).then(()=>{ console.log(1) })

方法四:yield后面是一个生成器 generator

function sleepGenerator(time) {
 yield new Promise(function(resolve,reject){
 setTimeout(resolve,time);
  }) 
} 
sleepGenerator(1000).next().value.then(()=>{console.log(1)}) 

方法五:es7---- async/await是基于Promise的,是进一步的一种优化

function sleep(time) {
 return new Promise(resolve =>
   setTimeout(resolve,time)
 ) } async function output() {
 let out = await sleep(1000); 
 console.log(1); 
 return out;
} 
output();

注意点:

async用来申明里面包裹的内容可以进行同步的方式执行,await则是进行执行顺序控制,每次执行一个await,程序都会暂停等待await返回值,然后再执行之后的await。

await后面调用的函数需要返回一个promise,另外这个函数是一个普通的函数即可,而不是generator。

await只能用在async函数之中,用在普通函数中会报错。

await命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。

其实,async / await的用法和co差不多,await和yield都是表示暂停,外面包裹一层async 或者 co来表示里面的代码可以采用同步的方式进行处理。不过async / await里面的await后面跟着的函数不需要额外处理,co是需要将它写成一个generator的。

————————————————

8、为什么通常在发送数据埋点请求的时候使用的是1*1像素的透明gif图片?
  • 避免跨域(img 天然支持跨域)
  • 利用空白gif或1x1 px的img是互联网广告或网站监测方面常用的手段,简单、安全、相比PNG/JPG体积小,1px 透明图,对网页内容的影响几乎没有影响,这种请求用在很多地方,比如浏览、点击、热点、心跳、ID颁发等等,
  • 图片请求不占用 Ajax 请求限额
  • GIF的最低合法体积最小(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节)
  • 不会阻塞页面加载,影响用户的体验,只要new Image对象就好了,一般情况下也不需要append到DOM中,通过它的onerror和onload事件来检测发送状态。
  • 示例: <script type="text/javascript"> var thisPage = location.href; var referringPage = (document.referrer) ? document.referrer : "none"; var beacon = new Image(); beacon.src = "http://www.example.com/logger/beacon.gif?page=" + encodeURI(thisPage) + "&ref=" + encodeURI(referringPage); </script>
 9、a.b.c.d和a['b'] ['c']['d'],那个性能更高?

10、介绍下webpack热跟新原理,

是如何做到在不刷新浏览器的前提下更新页面的

模块热替换(Hot Module Replacement)

模块热替换功能会在应用程序运行过程中替换、添加或删除模块,无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

保留在完全重新加载页面时丢失的应用程序状态。

只更新变更内容,以节省宝贵的开发时间。

调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。

webpack-dev-server实现热更新(HMR)

webpack-dev-server就是一个基于node.js和webpack的小型服务器。

热更新可以做到在不刷新浏览器的前提下更新页面。

安装webpack-dev-server
npm install webpack-dev-server --g
npm install webpack-dev-serve --save-dev

配置webpack.config.js文件
const webpack=require('webpack');//引入webpack
    entry:__dirname+'/src/main.js',
    output:{
        publicPath:'/dist',//必须加publicPath
        path:__dirname+'/dist',
        filename:'bundle.js'
    },
    devServer:{
        host:'localhost',
        port:'8080',
        open:true//自动拉起浏览器
        hot:true,//热加载
        //hotOnly:true
    },
    plugins:[
    //热更新插件
        new webpack.HotModuleReplacementPlugin()
    ]

但是通过日志发现页面先热更新然后又自动刷新,这和自动刷新是一样的。

如果只需要触发HMR,可以再加个参数hotOnly:true,这时候只有热更新,禁用了自动刷新功能。

如果需要自动刷新就不需要设置热更新。

热跟新必须有以下5点:

1.引入webpack

2.output里加publicPath

3.devServer中增加hot:true

4.devServer中增加hotOnly:true

5.在plugins中配置 new webpack.HotModuleReplacementPlugin()

————————————————

11、为什么普通for循环的性能远远高于forEach的性能,请解释其中的原因。

总结如下: 1.如果只是遍历集合或者数组,用foreach好些,快些,因为for每遍历一次都要判断一下条件。 2.如果对集合中的值进行修改,就要用for循环了。其实foreach的内部原理其实也是Iterator,但它不能像Iterator

一样可以人为的控制,而且也不能调用iterator.remove();更不能使用下标来访问每个元素,所以不能用于增加,

删除等复杂的操作。

-----------------------------------------------------------------------------------------------------------

for循环和foreach的区别

关于for循环和foreach的区别,你真的知道,用了那么多年使用起来已经很熟悉了,可突然问我讲讲这两的区别,一下还真把我给卡住了一下,

下面从源码的角度简单分析一下吧;

for循环的使用

for循环通过下标的方式,对集合中指定位置进行操作,每次遍历会执行判断条件 i<list.size(),满足则继续执行,执行完一次i++;

[java] view plain copy
for(int i=0;i<list.size();i++)  
{  
    System.out.println(i + ":" + list.get(i));  
}  

也就是说,即使在for循环中对list的元素remove和add也是可以的,因为添加或删除后list中元素个数变化,继续循环会再次判断i<list.size(); 也就是说list.size()值也发生了变化,所以

是可行的,具体操作如下代码

[java] view plain copy
for (int i = 0; i < list.size(); i++) {  
 if (i == 3) {  
    list.add("中间插入的一个字符串");  
   }  
 if (i == 5) {  
    {  
     list.remove(6);  
    }  
   }  
   System.out.println(i + ":" + list.get(i));  
  }  

增强for循环:foreach循环的原理

同样地,使用foreach遍历上述集合,注意foreach是C#中的写法,在Java中写法依然是for (int i : list)

写法for(String str : list)

查看文档可知,foreach除了可以遍历数组,还可以用于遍历所有实现了Iterable<T>接口的对象。

用普通for循环的方式模拟实现一个foreach,由于List实现了Iterable<T>,

过程如下:首先通过iterator()方法获得一个集合的迭代器,然后每次通过游标的形式依次判断是否有下一个元素,如果有通过 next()方法则可以取出。 注意:

执行完next()方法,游标向后移一位,只能后移,不能前进。

用传统for循环的方式模拟 增强for循环

和for循环的区别在于,它对索引的边界值只会计算一次。所以在foreach中对集合进行添加或删掉会导致错误,抛出异常java.util.ConcurrentModificationException

[java] view plain copy
private static void testForeachMethod(ArrayList<String> list) {  
 int count = 0; // 记录index 
 for (String str : list) {  
   System.out.println(str);  
   count++;  
 if (count == 3) {  
 // foreach中修改集合长度会抛出异常 
 // list.add("foreach中插入的ABC"); 
   }  
  }  
 }  

具体可以从源码的角度进行理解

1.首先是调用iterator()方法获得一个集合迭代器

初始化时

expectedModCount记录修改后的个数,当迭代器能检测到expectedModCount是否有过修改

在创建迭代器之后,除非通过迭代器自身的 removeadd 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出

ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException

因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。

12、介绍下npm模块安装机制,

为什么输入npm install就可以自动安装对应的模块?

1. npm 模块安装机制:

  • 发出npm install命令
  • 查询node_modules目录之中是否已经存在指定模块
    • npm 向 registry 查询模块压缩包的网址
    • 下载压缩包,存放在根目录下的.npm目录里
    • 解压压缩包到当前项目的node_modules目录
    • 若存在,不再重新安装
    • 若不存在

2. npm 实现原理

输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):

  1. 执行工程自身 preinstall

当前 npm 工程如果定义了 preinstall 钩子此时会被执行。

  1. 确定首层依赖模块

首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。

工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

  1. 获取模块

获取模块是一个递归的过程,分为以下几步:

  • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
  • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
  • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
  1. 模块扁平化(dedupe)

上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。

从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

这里需要对重复模块进行一个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。

比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。

而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。

举个例子,假设一个依赖树原本是这样:

node_modules -- foo ---- lodash@version1

-- bar ---- lodash@version2

假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:

node_modules -- foo

-- bar

-- lodash(保留的版本为兼容版本)

假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:

node_modules -- foo -- lodash@version1

-- bar ---- lodash@version2

  1. 安装模块

这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。

  1. 执行工程自身生命周期

当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。

最后一步是生成或更新版本描述文件,npm install 过程完成。

原文发布于微信公众号 - 李才哥(liqi13695515224)

原文发表时间:2019-09-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券