# 第11-20题

# 11、算法手写题

// 已知如下数组:
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
// 编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

[...new Set(arr.flat(Infinity))].sort((a,b) => a - b); 多写一句,如果sort函数中不传函数进去,默认按照字符编码进行排序1、10、111。诸如此类。

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

来自阮一峰 (opens new window)

# 1. 回调函数

优点:简单、容易理解和部署。

缺点:不利于代码阅读和维护,各个部分之间耦合度过高,流程混乱,而且每个任务只能指定一个回调函数。

# 2. 事件监听

优点:容易理解,可以绑定多个事件,每个事件指定多个回调函数,而且可以"去耦合",有利于实现模块化。

缺点:整个程序都变成事件驱动模型,运行流程会很不清晰。

# 3. 发布订阅

在前一步进一步升级,运行流程清晰。 例如jquery.subscribe('done', f2);

# 4. promise

是commonJS提出工作组提出的一种规范。 优点: 链式调用程序流程清晰,对比前三,如果一个任务完成,再添加回调函数就会立刻执行。所以 不用担心错过了某个信号和事件。

缺点:编写和理解。

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

const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve(5);
  console.log(2);
}).then(val => {
  console.log(val);
});

promise.then(() => {
  console.log(3);
});

console.log(4);

setTimeout(function() {
  console.log(6);
});

// 1 2 4 5 3 6

Promise函数是同步执行,then是异步执行。

# 14、手写一个new

function _new(Fn, ...args) {
  const obj = Object.create(Fn.prototype);
  const result = Fn.apply(obj, args);
  return result instanceof object ? result : obj;
}

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

http2.0采用了二进制传输格式传输,取消了HTTP1.x的文本格式,二进制格式更高效。

多路复用代替了HTTP1.x的序列和阻塞机制,所有相同的域名请求都通过同一个TCP连接并发完成。 由串行改为并行\开启了keep-alive,虽然可以用多次,但是同一时刻只能有一个HTTP请求

在HTTP1.x中,并非多个请求需要多个TCP连接,浏览器为了控制资源会有6-8的TCP连接受限制。

HTTP2中(帧(frame)和流(stream)):

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

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

三次握手之所以是三次,是因为三次是保证client和server均让对方知道自己的接受和发送能力没问题而保证的最小次数。

三次握手

  • 1、客户端发送syn包到服务器,等待服务器接受 (此时server可以判断出client具备发送能力。)
  • 2、服务器确认收到syn包并确认客户的syn,并发送回来一个syn + ack的包给客户端。 (此时client可以判断出server具备发送和接受的能力。此时client 还需让server知道自己的接受没问题。)
  • 3、客户端确认接收到服务器的syn + ack包,并向服务端发送确认包ack,二者相互建立联系 (此时client、server双方均保证自己的接受和发送能力。)

四次挥手

对比三次握手,中间多了一层等待服务器再一次响应回复相关数据的过程。

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

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

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

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

RST的设计用意在于链路发生意料之外的故障时告知链路上的各方释放资源(一般指的是NAT网关与收发两端);FIN的设计是用于在链路正常情况下的正常单向终止与结束。二者不可混淆。

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

在react中,如果是由React引起的事件处理(比如通过onClick引起的事件处理),调用setState不会同步更新, this.state, 除此之外,调用setState就会同步的执行setState,得到最新值。所谓的"除此之外",指的是 绕过React,通过AddEventListener直接添加的事件处理函数,或者setTimeout/setInterval产生的异步调用。

原因:在React的函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state,还是放到队列中, 回头再说。而isBatchingUpdates默认是false,也就表示setState会同步更新this.state。但是有一个函数 batchedUpdates,这个函数会把isBatchingUpdates改为true,而当react在调用事件处理函数之前就会调用 batchedUpdates这个函数。造成的后果,就是由React控制事件处理过程setState不会同步去更新this.state.

注意:setState所说的异步并不是在内部由异步代码实现,其实本身执行的代码和过程都是同步的,只是合成事件 和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中不能拿到最新的值,形成了所谓的"异步"。当然可以 通过setState的第二个参数中的callBack来得到最新的值。

# 19、代码执行顺序题

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;
  }
};

0 0 2 3

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

# npm安装机制

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

# npm原理

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

  1. 执行工程自身preinstall

当前npm工程如果定义了preinstall就会执行。 (npm 脚本有pre和post两个钩子。)

  1. 确定首层依赖模块

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

工程本身就是整个依赖数的根结点,每个首层依赖模块都是根结点下面的一个子节点,npm会开启多进程从每个 首层依赖模块寻找更深层次的节点。

  1. 获取模块

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

  • 获取模块信息 在下载一个模块之前,首先要确定其版本。这是因为pack.json中存在的往往都是semantic version(semver语义版本)。 如果此时有描述文件(yarn.lock和pack-lock.json)中有该模块的信息。那就直接拿就可以。 否则通过例如^1.1.0, 查找1.x.x的最新版本 *(^锁大版本,~锁中版本,小版本都要锁,那就什么不写。意味着安装最新版本)
  • 获取模块实体 上一步会获取到模块的压缩包的地址,(resolve字段),npm会用此地址检查本地缓存,缓存中就直接拿,如果没有 则从仓库下载。
  • 查找该模块依赖, 如果有依赖就回到第一步,如果没有就停止。
  1. 模块扁平化

上一步获取到的是最一颗完整的依赖树,其中可能包含大量重复的模块。比如A模块依赖lodash,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 过程完成。

Last Updated: 3/31/2022, 9:41:12 PM