《前端100问》20

11、算法手写题 数组扁平化去重

已知如下数组:

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

Array.from(new Set(arr.flat(Infinity))).sort(((a, b) => a - b))

Array.prototype.flat() 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

flat() 默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat() 方法的参数写成一个整数,表示想要拉平的层数,默认为1。

如果不管有多少层嵌套,都要转成一维数组,可以用 Infinity 关键字作为参数。

12、JS 异步解决方案的发展历程以及优缺点

按时间顺序:

  1. 回调函数(callback)
  2. Promise
  3. Generator
  4. async/await

优缺点:

回调函数(callback)

优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行)

缺点:回调地狱,不能用 try catch 捕获错误,不能 return

回调地狱的根本问题在于:

  • 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转
  • 嵌套函数过多的多话,很难处理错误

Promise

Promise就是为了解决callback的问题而产生的。

优点:解决了回调地狱的问题

缺点:无法取消 Promise ,错误需要通过回调函数来捕获

Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

缺点:需要手动调用函数执行,麻烦

async/await

async、await 是异步的终极解决方案

优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题。async 相比于 Generator内置了执行器,拥有更好的语义化

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低

13、Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

Promise 构造函数是同步执行的,而then方法里面的执行函数一般是放到微任务队列的异步任务所以是异步执行

扩展:什么是Promise?

Promise 是解决异步编程的一种方案。以前我们处理异步操作,一般都是通过回调函数来处理,典型的例子就好像使用 setTimeout 一样,如果执行操作函数里面还有 setTimeout,一层一层往下,都有的话。那么代码看起来十分臃肿,不利于维护,也很容易写出bug。

Promise 的出现,能够让异步编程变得更加可观,把异步操作按照同步操作的流程表达出来,避免层层嵌套的回调函数。

Promise 对象有三种状态,进行中pending、完成成功fulfilled、失败rejected,顾名思义,表示这个异步操作是进行中还是成功还是失败了。

Promise 的状态一旦确定了,就不会再更改了,这就是 promise(承诺)的由来吧,承诺状态确定了就是确定了。

然而 Promise 还是有不足的地方:

  1. 如果没有执行捕获错误的函数(如下述说的 catch,then 的第二个参数),则 Promise 内部发生的错误(虽然会报错但)是无法传递到 Promise 外部代码上的,因此外部脚本并不会因为错误而导致不继续执行下去。
  2. 一旦新建了,就无法中断它的操作。不像 setTimeout 那样,我还可以使用 clearTimeout 取消掉。

14、如何实现一个 new

new 在执行时,会做下面这四件事:

  1. 开辟内存空间,在内存中创建一个新的空对象。

  2. this 指向这个新的对象。

  3. 执行构造函数里面的代码,给这个新对象添加属性和方法。

  4. 返回这个新对象(所以构造函数里面不需要 return)

/**
 * 1. 创建了一个全新的对象。
 * 2. 这个对象会被执行[[Prototype]](也就是__proto__)链接。
 * 3. 生成的新对象会绑定到函数调用的this。
 * 4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
 * 5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
 */

function _new(fn, ...arg) {
  const obj = Object.create(fn.prototype)
  const ret = fn.apply(obj, arg)
  return ret instanceof Object ? ret : obj
}

15、简单讲解一下http2的多路复用

HTTP2.0 的出现,相比较于 HTTP1.x,大幅度提升了 Web 的性能。在与 1.x 版本语义上完全兼容的基础上大幅度减少了网络延迟,减少了前端在 Web 优化的工作量。

而实现以上优点的原因就是 HTTP2.0 采用了多路复用,即允许同时通过单一的 HTTP2.0 连接发起多重的 请求-响应 消息。就是连接一次可以发送多个请求-响应。

多路复用代替了 HTTP1.x 的 序列阻塞机制,所有的相同域名请求都通过同一个 TCP 连接并发完成。在HTTP1.x 中,并发多个请求需要多个 TCP 连接,浏览器为了控制资源会有 6 到 8 个 TCP 连接都限制。

HTTP2.0 采用二进制格式传输,取代了 HTTP1.x 的文本格式,二进制格式解析更高效。

在 HTTP2.0 中:

  • 同域名下所有通信都在单个连接上完成,消除了因多个 TCP 连接而带来的延时和内存消耗。
  • 单个连接上可以并行交错的请求和响应,之间互不干扰

HTTP2.0 中有两个很重要的概念:帧(frame)和流(stream)

帧代表着最小的数据单位,每个帧都会标识出该帧属于哪个流,流就是由多个帧组成的数据流。

多路复用,也就是在一个 TCP 连接中可以存在多条流,就是我们所说的可以发送多个请求,对端可以通过帧的标示知道,该帧属于哪个流(请求),通过这个技术及意义避免 HTTP 1.x 中的队头阻塞问题,极大的提高传输性能。

16、谈谈你对TCP三次握手和四次挥手的理解

三次握手:

Browser:先告诉服务器 “我要开始发起请求了,你那边可以吗?”
Server:服务器回复浏览器 “没问题,你发吧!”
Browser:告诉服务器 “好的,那我开始发了。”

四次挥手:

Browser:先告诉服务器 “我数据都发完了,你可以关闭连接了。”
Server:回复浏览器 “我先看看我这边还有没有数据没传完。”
Server:确认过以后,再次回复浏览器 “我这边数据传输完成了,你可以关闭连接了。”
Browser:告诉服务器 “好的,那我真的关闭了。你不用回复我了。”

Browser又等了2MSL,确认确实没有再收到请求了,才会真的关闭TCP连接。


三次握手:

从最开始双方都处于CLOSED状态。然后服务端开始监听某个端口,进入了LISTEN状态。

然后客户端主动发起连接,发送 SYN(同步序列编号) , 自己变成了SYN-SENT状态。

服务端接收到,返回SYN(对应客户端发来的SYN)和ACK(确认字符),自己变成了SYN-REVD

之后客户端再发送ACK给服务端,自己变成了ESTABLISHED状态;服务端收到ACK之后,也变成了ESTABLISHED状态。

四次挥手:

刚开始双方处于ESTABLISHED状态。客户端要断开了,向服务器发送 FIN(结束字段) 报文。发送后客户端变成了FIN-WAIT-1状态。注意, 这时候客户端同时也变成了half-close(半关闭)状态,即无法向服务端发送报文,只能接收

服务端接收后向客户端确认,变成了CLOSED-WAIT状态。客户端接收到了服务端的确认,变成了FIN-WAIT2状态。

随后,服务端向客户端发送FIN,自己进入LAST-ACK状态,客户端收到服务端发来的FIN后,自己变成了TIME-WAIT状态然后发送 ACK 给服务端。

这个时候,客户端需要等待足够长的时间,具体来说,是 2 个 MSL(Maximum Segment Lifetime,报文最大生存时间), 在这段时间内如果客户端没有收到服务端的重发请求,那么表示 ACK 成功到达,挥手结束,否则客户端重发 ACK。

补充:

TCP 三次握手的过程为什么是三次而不是两次、四次?

为什么不是两次?根本原因: 无法确认客户端的接收能力。

如果是两次,你现在发了 SYN 报文想握手,但是这个包滞留在了当前的网络中迟迟没有到达,TCP 以为这是丢了包,于是重传,两次握手建立好了连接。看似没有问题,但是连接关闭后,如果这个滞留在网路中的包到达了服务端呢?这时候由于是两次握手,服务端只要接收到然后发送相应的数据包,就默认建立连接,但是现在客户端已经断开了。这就带来了连接资源的浪费。

为什么不是四次?三次就足够了,再多用处就不大了。

为什么是四次挥手而不是三次?因为服务端在接收到FIN, 往往不会立即返回FIN, 必须等到服务端所有的报文都发送完毕了,才能发FIN。因此先发一个ACK表示已经收到客户端的FIN,延迟一段时间才发FIN。这就造成了四次挥手。

如果是三次挥手会有什么问题?等于说服务端将ACKFIN的发送合并为一次挥手,这个时候长时间的延迟可能会导致客户端误以为FIN没有到达客户端,从而让客户端不断的重发FIN

17、A、B 机器正常连接后,B 机器突然重启,问 A 此时处于 TCP 什么状态

如果A 与 B 建立了正常连接后,从未相互发过数据,这个时候 B 突然机器重启,问 A 此时处于 TCP 什么状态?如何消除服务器程序中的这个状态?(超纲题,了解即可)

A侧在超时退出之后一般会发送一个RST 包用于告知对端重置链路,并给应用层一个异常的状态信息,视乎同步IO与异步IO的差异,这个异常获知的时机会有所不同。

B侧重启之后,因为不存有之前A-B之间建立链路相关的信息,这时候收到任何A侧来的数据都会以RST作为响应,以告知A侧链路发生异常

18、React 中 setState 什么时候是同步的,什么时候是异步的?

19、React setState 笔试题,下面的代码输出什么?

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};

20、介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?

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

npm 实现原理

  1. 执行工程自身 preinstall

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

  2. 确定首层依赖模块

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

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

  3. 获取模块

获取模块是一个递归的过程

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

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

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

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

  1. 安装模块

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

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

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


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!