[TOC]
HTTP
1. 请简单说一下对http协议和Tcp协议的了解
首先, Http协议是建立在Tcp协议基础上的。 http协议位于应用层, 主要解决包转数据的问题, tcp协议位于传输层, 主要解决的数据如何在网络中传输的问题 当用户发起一次请求的时候, http会通过tcp建立起一次连接。 当请求的数据传输完毕之后, http会立即将tcp连接断开。 所以http也是一种短连接, 一种无状态的连接。
2. 说一下对HTTP缓存的了解
主要分为强缓存和协商缓存。
强缓存有:
- cache-control 相对时间
- expires
协商缓存有:
-
[Etag, If-None-Match]:
Etag: Etag就像一个指纹, 资源的变化会导致ETag的变化, ETag可以保证每个资源都是唯一的
If-None-Match: 会将上次服务端返回的Etag发送给服务端,询问改资源是否有更新, 有变动的话会把
新的资源返回回来
-
[Last-Modified, If-Modified-Since]:
Last-Modified: 资源最后修改的时间
If-Modified-Sinec: 上次返回的Last-Modified的值
3. HTTP是如何建立连接的, 三次握手的过程?
通过三次握手建立连接,
三次握手的过程。
- 客户端发送一个SYN字段, 并指明客户端的初始序列号, 即ISN。 确定客户端的发送能力和服务端的接受能力是正常的。
- 服务端返回自己的SYN字段, 同样指明自己的ISN。同时为了确认客户端的SYN, 将ISC+1作为ACK的值, 如果丢失了, 就重传。确认了服务端的发送能力和接受能力是正常的。客户端的接受能力也是正常的。
- 第三次握手。为了确认服务器端的SYN, 客户端将ISN+1作为ACK的值返回服务器端。客户端发包, 服务端收到了。 确认了 双方的发送能力和接受能力都是正常的。 就可以建立链接正常通信了
4. HTTP是如何断开连接的, 四次挥手的过程?
通过四次挥手断开连接
四次挥手的过程
- 客户端发送一个FIN端, 同时包含一个ACK, 表示确认对方最近发送过来的数据
- 服务端将K值+1作为ACK的标志, 表明收到了上一个包。 这是上层的应用会被告知另一端发起了关闭操作。 通常这将引起应用程序发起自己的关闭操作
- 服务端发起自己的FIN端, ACK=K+1,Seq=L
- 客户端确认 ACK=L+1
5. 为什么建立连接是3次握手, 断开连接需要四次握手
服务端在LISTEN的状态下, 收到建立链接的SYN报文后, 把ACK和SYN放到一个报文里发送给客户端。
而关闭链接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了。
但是还能接受数据。
HTTP是否现在关闭发送数据通道, 需要上层应用来决定,
因此, 己方ACK和FIN一般都会分开发送,
6. Https与Http的区别
主要有以下几点区别
-
HTTPS把HTTP上层的传输协议由TCP/IP换成了SSL/TLS
-
HTTPS发送报文不再使用专门的Socket API, 而是调用专门的安全接口
-
HTTPS需要去专门的机构申请到CA证书, 一般免费的证书很少, 都需要交费。
-
HTTP是超文本传输协议,信息都是明文传输。HTTPS是具有安全性的ssl加密传输协议
-
HTTP和HTTPS使用完全不同的连接方式, 采用的端口也不一样, HTTP是80, HTTPS采用的是443
-
HTTP的连接很简单,是无状态的。HTTPS协议是由SSL+HTTP协议构建的可进行加密传输,身份认证的网络协议
比HTTP要安全。
7. 说一下对HTTP2的了解
-
HTTP2采用二进制格式进行传输数据。 而非HTTP1.X使用的文本格式。
-
HTTP2的传输的每个数据都是以消息的形式进行传输,而消息又由一个或多个帧组成
最后使用流标示进行重新组装
-
同域名下的只需要占用一个TCP连接。
9. 什么是HTTP队头堵塞, 怎么解决?
其实就是排队问题。 HTTP协议遵循一发一收的规则,从而形成了一个先进先出的队列。
如果最开始请求没有处理完。 那么后面的请求响应都会处在等待状态。
如何解决?
-
更换为HTTP2协议
http2中,每个请求和响应都被称为消息。 每个消息被拆分为若干个帧进行传输。 每个帧又由多个流组成。
每个流都会分配一个序号。 各个帧和流在连接上都进行独立传输。 到达之后在按照序号进行组装, 这样就解决了
队头堵塞的问题。
10. 什么是TCP队头堵塞, 怎么解决?
TCP的队头堵塞是指在传输的过程中, 其中一个分节的字段丢失, 后续的分节数据会被接收端一直保持, 知道丢失的数据重新
传递到接受端为止
如何解决?
- 更换为udp协议
11. 对Udp协议的了解
- udp一样是位于传输层的协议, 主要是为了解决数据的传输问题。
- Udp无连接, 不存在建立连接需要等待的时间
- udp没有拥塞控制,网络的波动不会影响源主机的发送
- 支持一对一, 一对多, 多对多发送消息
- udp面相数据报文, 数据之间不会有阻塞约束。
12. HTTPS建立连接的过程?
主要分为以下几步:
-
浏览器发送一个请求到服务器端
-
服务器端向用户发送自己的数字证书, 证书中有一个公钥来加密信息,私钥由服务器持有
-
客户端收到数组证书后, 回去检查数字证书的安全性和真伪性
之后呢, 客户端会发送给服务器一个随机字符串给服务器的私钥进行加密。
客户端使用公钥进行解密。 如果解密后的随机字符串一致。
说明对方确实是私钥的真正持有者
-
验证了服务器的身份之后, 可会断会生成一个对称加密算法和密钥。 客户端用公钥
加密后发送给服务器端。 服务器端用私钥对加密算法和密钥进行解密, 之后可以进行通信。
13. TCP如何在网络卡顿的时候不丢包?
- 重试策略
- TCP 的拥塞控制算法会在丢包时主动降低吞吐量
14. TCP是如何进行分包的?
15. Dns查询的过程,dns用什么协议发起dns查询的
以udp协议为主, 主要的查询协议是基于udp的,
但是在进行区域传输的时候会使用TCP协议。
拓展: 什么场景下面会需要进行数据传输?
DNS的规范规定了2种类型的DNS服务器,一个叫主DNS服务器,一个叫辅助DNS服务器。在一个区中主DNS服务器从自己本机的数据文件中读取该区的DNS数据信息,而辅助DNS服务器则从区的主DNS服务器中读取该区的DNS数据信息。当一个辅助DNS服务器启动时,它需要与主DNS服务器通信,并加载数据信息,这就叫做区传送(zone transfer)。
网络安全
1. 你都了解那些加密算法, 能简单说一下吗?
-
摘要加密
使用hash函数对消息或文本进行加密和计算, 比如说
md5和sha -
对称加密
双方使用同一个密钥和逆算法对消息进行加密和解密, 例如
des和aes -
非对称加密
公钥和私钥都可以对消息进行加密,一方加密,另一方既可以解密。
公钥无法推导出私钥。 所以公钥可以公开。私钥必须持有
比如说rsa
2. 说一下前端的一些攻击和防御手段。
-
XSS跨站脚本攻击
在一些表单中动态输入时,创建HTML标签元素, 将攻击的代码嵌入到服务器或者数据库中。
发送给用户。
解决方案: 处理尖括号
-
CSRF跨站请求伪造
网站B窃取了网站A的cookie,借助网站A中已经登陆的用户发起请求。
进行一些危险的攻击和动作
解决方案: 检查referer, 添加校验token, 不保存cookie, 不实用全局cookie
自定义header头并且校验。
-
injection注入
浏览器相关
1. 能介绍一下浏览器的事件循环吗?
2. 从输入url到页面加载发生了什么
-
DNS解析, 会从浏览器的缓存开始到顶级跟服务器, 查到到当前域名对应的ip和端口号服务
-
建立TCP连接。 HTTP通过3次握手,HTTPS通过验证证书,发送密钥和公钥确认身份建立连接
-
发送请求。接受请求, 处理请求
- 缓存中获取请求内容
- 服务端返回请求内容
-
解析页面。
-
构建渲染树
解析html(边下载边解析), 解析css
-
构建布局树, 更新元素的位置
-
构建分层树, 处理一些滚动之类的, position, z-index等属性
-
解析js: 重绘: 更新样式和属性。重排: 更新位置
事件循环
-
3. <事件循环>请说出以下函数的输出结果
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
// process.nextTick(() => console.log('nextTick'))
console.log('script end')
// script start
// async1 start
// async2
// promise1
// en d
// async1 end
// promise2
// nextTick
// setTimeout
3. 解析CSS会阻塞dom解析吗?
不会, css的解析只会阻塞dom的渲染, 不会阻塞dom的解析
4. cookie的samesite属性作用
通过限制第三方的cookie, 来防治CSRF攻击和用户追踪
- Strict: 完全禁止
- Lax: 大多数情况下不发送cookie
- None: 默认设置
5. 浏览器的渲染流程
- 首先需要处理输入时间,能够让用户得到最早的反馈。
- 接下来处理定时器,需要检查定时器是否到时间,并执行对应的回调。
- 接下来处理Begin Frame(开始帧), 即每一帧的事件, 包括window.resize、scroll、 media、 query、change等等。
- 接下来执行请求动画帧requestAnimationFramae, 即每次执行之前,会执行raf的回调。
- 紧接着进行Layout操作,包括计算布局和更新布局,即这个元素的样式是怎样的,他应该在页面如何展示。
- 接着进行Paint操作,得到书中每个节点的尺寸和位置信息。浏览器针对每个元素进行内容填充。
- 到这时以上六个阶段都已经完成了,接下来处于空闲阶段(idle peroid) , 可以在这时执行requestIdleCallback力注册的任务。
6. v8中的快慢属性
v8中的快慢属性主要是为了提高对象的访问速度来存在的。
主要分为两种, 一种排序属性, 一种常规属性
当一个对象的属性为字母时,并且数量少于10的时候, 会当作常规属性存储到一个线性结构中。
这个时候属性的访问速度时非常快的。 可以通过index索引直接访问。 即为快属性
当数量大于10的时候, 会以以非线性字典的形式存储。 这个时候就是慢属性
要了解 V8 是怎么提升对象属性访问速度的,需要了解2个概念:排序属性、常规属性
一开始见到这2个个概念,肯定会觉得疑惑,这都是啥玩意呀,不急,先来看看一个常见的代码段:遍历对象:
function Foo() {
this[100] = 'test-100'
this[1] = 'test-1'
this["B"] = 'bar-B'
this[50] = 'test-50'
this[9] = 'test-9'
this[8] = 'test-8'
this[3] = 'test-3'
this[5] = 'test-5'
this["A"] = 'bar-A'
this["C"] = 'bar-C'
}
var bar = new Foo()
for(key in bar){
console.log(`index:${key} value:${bar[key]}`)
}
/* Output Result:
index:1 value:test-1
index:3 value:test-3
index:5 value:test-5
index:8 value:test-8
index:9 value:test-9
index:50 value:test-50
index:100 value:test-100
index:B value:bar-B
index:A value:bar-A
index:C value:bar-C
*/
复制代码
通过观察我们可以发现,结果是先遍历以数字为键的,再是以字母为键的,而且数字键是从小到大,字母键是按先后顺序遍历的,之所以出现这样的结果,是因为在ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列,而数字属性就是排序属性(elements),字母属性就是常规属性(properties)
排序属性放在线性结构 elements 中,常规属性放在 properties 中,遍历从elements结构开始,不过当常规属性数量小于10时,其值是直接放在当前properties所属对象(快属性)中,可以节约遍历时间,但是当 properties 属性数量大于 10 个时,会以非线性字典形式存入 properties对象结构中(慢属性),V8就是通过这种快慢属性来提高元素访问速度的
4. 32位系统和64位系统的区别
第一,设计初衷不同。64位操作系统的设计初衷是:满足机械设计和分析、三维动画、视频编辑和创作,以及科学计算和高性能计算应用程序等领域中需要大量内存和浮点性能的客户需求。换句简明的话说就是:它们是高科技人员使用本行业特殊软件的运行平台。而32位操作系统是为普通用户设计的。
第二,要求配置不同。64位操作系统只能安装在64位电脑上(CPU必须是64位的)。同时需要安装64位常用软件以发挥64位(x64)的最佳性能。32位操作系统则可以安装在32位(32位CPU)或64位(64位CPU)电脑上。当然,32位操作系统安装在64位电脑上,其硬件恰似“大马拉小车”:64位效能就会大打折扣。
第三,运算速度不同。64位CPU GPRs(General-Purpose Registers,通用寄存器)的数据宽度为64位,64位指令集可以运行64位数据指令,也就是说处理器一次可提取64位数据(只要两个指令,一次提取8个字节的数据),比32位(需要四个指令,一次提取4个字节的数据)提高了一倍,理论上性能会相应提升1倍。计算机
第四,寻址能力不同。64位处理器的优势还体现在系统对内存的控制上。由于地址使用的是特殊的整数,因此一个ALU(算术逻辑运算器)和寄存器可以处理更大的整数,也就是更大的地址。比如,Windows Vista x64 Edition支持多达128 GB的内存和多达16 TB的虚拟内存,而32位CPU和操作系统最大只可支持4G内存。
第五,软件普及不同。目前,64位常用软件比32位常用软件,要少得多的多。道理很简单:使用64位操作系统的用户相对较少。因此,软件开发商必须考虑“投入产出比”,将有限资金投入到更多使用群体的软件之中。这也是为什么64位软件价格相对昂贵的重要原因。
Javascript相关
1. 说一下commonJs和esModule的区别
-
commonJs是被加载时运行,esModule是编译的时候运行
-
commonJs输出的是值的拷贝, esModule输出的是值的引用
-
commJS具有缓存,被加载时会运行整个文件并输出一个对象。拷贝在内存中。
下次加载文件时,直接从内存中取值
-
esModule支持Tree-shaking
2. js的数据结构?
基本数据类型:
Number, Boolean, String, Undefined, Null
引用数据类型:
Object, Array, Function
Es6:
Set, Map
3. js的继承
-
构造函数继承
ClassA.prototype = new ClassB()问题: 子类new出来的实例, 父类的属性会项目影响没有隔离(应用类型)
function Parent2() { this.name = 'parent2' this.arr = [1, 2, 3] } Parent2.prototype.say = function () { console.log(this.name) } function Child2() { this.type = 'child2' } Child2.prototype = new Parent2() //原型继承核心代码 let child2_1 = new Child2() let child2_2 = new Child2() console.log(child2_1.name) // parent2 console.log(child2_1.say()) // parent2 child2_1.arr.push(4) console.log(child2_1.arr, child2_2.arr) // [1, 2, 3, 4] [1, 2, 3, 4] -
原型链继承
问题: 方法需要在构造函数中创建, 无法复用,造成内存浪费
-
组合继承
子类构造函数中调用父类的构造函数, 同时将子类的prototype指向父类
问题: 调用两次父类的构造函数
-
寄生式继承
4. 数组和链表的区别
-
数组在内存中是一块连续的区域, 但是链表可以存在于内存中的任意地方
-
数组需要事先定义长度,需要预留出相应大小的内存, 如果需要拓展内存, 需要重新去进行分配, 效率很低
但是链表不指定大小,拓展更方便
-
链表增加或删除数据容易,查找元素复杂。 数组增加或插入元素效率更低, 但是查找元素的效率会更高
5. FragmentDocument跟一次性渲染有什么不同
6. 实现一个原声的Promise
let PENDING = 'pending';
let RESOLVED = 'resolved';
let FALIED = 'failed'
class MyPromise {
constructor(execute) {
this.value = undefined;
this.reason = undefined;
this.status = PENDING
this.resolveCallbacks = []
this.rejectCallbacks = []
let resolve = () => {
};
let reject = () => {}
try {
execute(resolve, reject)
}catch(e) {
this.reason = e
}
}
then(onFulfilled, onRejected) {
let promise2 = new MyPromise((resolve, reject) => {
if(this.status === RESOLVED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
// resolve(x)
resolvePromise(promise2, x, resolve, reject)
}catch(e) {
reject(e)
}
}, 0)
}
if(this.status === FALIED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
}catch(e) {
reject(e)
}
}, 0)
}
if(this.status === PENDING) {
this.resolveCallbacks.push(() => {
setTimeout(() => {
onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
}, 0)
})
this.rejectCallbacks.push(() => {
setTimeout(() => {
onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
}, 0)
})
}
resolve()
})
return promise2
}
}
const isPromise = (value) => {
return typeof value.then === 'function'
}
MyPromise.all = function(promises) {
let resultArr = [];
let index = 0
return newPromise((resolve, reject) => {
const processData = (data, index) => {
resultArr[index] = data;
if(++idx === promises.length) {
resolve(resultArr)
}
}
for(let i = 0; i< promises.length; i++) {
let currentValue = promises[i];
if(isPromise(currentValue)) {
currentValue.then(data => {
processData(data, i)
}, reject)
}else {
processData(data, i)
}
}
})
}
MyPromise.finall = function(callback) {
return this.then(data => {
return Promise.resolve(callback()).then(() => data)
}, error => {
return Promise.resolve(callback()).then(() => { throw error })
})
}
function resolvePromise(promise2, x , resolve, reject) {
if(promise2 === x) {
// 说明循环引用了promise
return reject(new TypeError('chaining '));
}
// 接下来要判断x的值是promise还是一个普通值
// 如果x不是对象或者不是函数, 直接成功
if((typeof x !== 'object' && x !== null) || typeof x !== 'function') {
// 判断一个promise, 看他是否具有一个then方法
try {
let then = x.then
if(typeof then === 'function') {
then.call(x, (y) => {
// resolve(y)
resolvePromise(promise2, y, resolve, reject)
}, r => {
reject(r)
})
}else {
resolve(x)
}
}catch {
reject(e)
}
}else {
// x就是一个普通值
resolve(x)
}
}
7. 手写bind函数
bind函数调用
func.bind(this, ...args)
实现一个bind函数
Function.prototype.myBind = function(context=window, ...preArgs) {
return (...args) => {
this.apply(context, [...preArgs, ...args])
}
}
8. 实现一个new函数
分析一下new函数的一个作用
通过new函数创建一个类的实例,
- 实例可以访问在构造函数中定义的属性
- 实例也可以访问类的prototype中的属性
- 运行contructor函数
那么来实现一个new
function objectFactory() {
var obj = new Object();
Contructor = [].shift.call(arguments);
obj.__proto__ = Contructor.prototype;
let ret = Contructor.apply(obj, arguments)
return typeof ret === 'object' ? ret : obj
}
9. 判断一个数据的类型
Object.prototype.toString.call(obj).slice(8, -1)
10. 实现一个深拷贝


11. js里有哪些位运算
按位与, 按位或, 按位异或, 左移, 右移,
React相关
1. setState是同步还是异步?
在不同场景中处理方式不同, 可能是同步也可能是一步的
- 在合成事件中是异步的
- 声明周期中调用是异步的
- 原生事件是同步的
- setTimeout中是同步的
- 批量更新中还是异步的
2. React 16中的生命周期
-
constructor挂载
-
getDerivedStateFromProps--prop更新的时候
-
render渲染函数
-
getSnapshotBeforeUpdate 触发commit之前,也可以读取dom
-
componentDidUpdate ComponenetWillUpdate componentWillUnMount--Commit阶段触发
3. diff算法
- treeDiff
- compinentDiff
- elementDiff
4. 调用setState之后发生了什么
- 在
setState的时候,React 会为当前节点创建一个updateQueue的更新列队。 - 然后会触发
reconciliation过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。 - 然后
React Scheduler会根据优先级高低,先执行优先级高的节点,具体是执行doWork方法。 - 在
doWork方法中,React 会执行一遍updateQueue中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。 - 当前节点
doWork完成后,会执行performUnitOfWork方法获得新节点,然后再重复上面的过程。 - 当所以节点都
doWork完成后,会触发commitRoot方法,React 进入 commit 阶段。 - 在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素。
5. react16的渲染过程
首先, react16采用了一个fiber架构, 整个渲染过程在concurrent模式下的渲染过程
是可中断的, 可调和的, 简单说一下他的核心过程吧
-
用户调用render方法,创建一个virtruldom树,生成一个root节点的fiber
-
模拟了requestIdCallback, 跟浏览器进行调和的方式, 调用workLoop方法
对每个fiber单元进行处理。
-
beginWork方法依次对不同的fiber单元进行处理,
- 跟节点,文字节点, 原生节点, classComponent, FunctionComponent等
-
reconcileChildren,处理当前fiber单元的孩子,生成对应的fiber节点,
如果存在alternate, 复用之前的alternate
-
fiber解析完成, 生成effectList链表
-
commit阶段, 处理所有的effect副作用,比如说新增,删除,更新
react还采用了双缓冲的机制
- 第一次更新会建立current树
- 第二次更新呢, 建立alternate树, 缓存上一次的更新
- 第三次更新,workInprogress就指向了上一次更新的alternate树, 即第一次更新的树
6. 常用的react Hooks
-
useState
-
useEffect --- 用来模拟各个生命周期函数的
- componentDidMount --> 第二个参数为空数组
- componentWillUpdate --> 第二个参数是一个状态数据, 数据改变的时候,会触发更新
- componentWillUnMount --> 第一个参数return一个函数
-
useRef --- 与组件进行关联
-
useContext -- 设置全局状态
使用方法
- 使用
C = createContext(initial)创建上下文 - 使用
<C.provider>圈定作用域 - 在作用域内使用
useContext(C)来使用上下文
- 使用
-
useReducer -- 模拟redux
-
useMemo 缓存复杂的计算值
function App() { const [n, setN] = React.useState(0); const [m, setM] = React.useState(0); const onClick = () => { setN(n + 1); }; const onClickChild = useMemo(() => () => {}, [m]); return ( <div className="App"> <div> <button onClick={onClick}>update n {n}</button> </div> <Child data={m} onClick={onClickChild} /> </div> ); } const Child = React.memo(props => { console.log("child 执行了"); return <div onClick={props.onClick}>child: {props.data}</div>; }); -
useCallback 缓存一个函数传递给子组件
-
useImperativeHandle 组件ref
7. 简单说一下Vue和React的区别
- 渲染方式不同: 数据改变, react需要重上到下重新比对。vue则是把每个渲染单元拆的足够小, 基于响应式的原理,实现了只更新数据变化的那个组件
- 架构不同: Vue核心是composition API和Proxy代理, React基于fiber架构的
- 组件不同: React支持无状态组件
- React的Concurrent模式对动画的支持比Vue更好
- 事件处理方式不同, react基于合成事件,vue是基于snobdom为基础的virtual-dom的绑定事件的方式
- 挂载的区别: Vue会覆盖掉挂载节点重新创建,react是挂载到挂载集诶单的内部
8. 简单介绍一下react的fiber架构
首先, fiber是一种数据结构, fiber架构是指一种可中断,可调和的架构。
react为什么要引入这种架构呢?
因为react16之前,每一次更新都需要从根节点开始,比对所有的虚拟dom树。但是浏览器的js引擎和渲染引擎同在一个线程之内。导致每一次更新都会阻断用户的交互或者渲染。 体验非常不好。
所以就引入了这种可中断,可调和的架构模式。
简单介绍一下这种架构:
这种架构的核心就是fiber这种数据结构结合一个requestIdCallback的api。让浏览器在每一帧渲染结束之后的空余时间内, 将控制权交给js。去完成虚拟dom的比对操作, 即render阶段。
这个过程会从跟节点开始。 给每一个fiber单元设置优先级。 每次会优先处理优先级比较高的任务。然后处理完每个单元, 将控制权交给浏览器。 等待下一帧的空闲时间。
当收集完整个虚拟dom树的时候,会收集到所有的变更,形成一个effect链表。
然后执行所有节点的变更
9. react提交渲染的过程 (effectList)
从根节点开始, 建立两个链表, firstEffect和lastEffect。
firstEffect指向第一个需要更新的单元, 每个单元的nextEffect指向下一个节点。
Lasteffect指向当前单元的lastEffect
形成一个单向链表
8. setState为什么是异步的
Vue相关
1. 简单说一下vue2的渲染过程
-
New Vue创建实例,执行this._init方法, 合并属性,初始化状态,props, methods,
并设置代理
-
$mount方法, 解析template,生成render函数
-
mountComponent函数, 触发组件生命周期
-
创建watcher函数,第一次会创建渲染watcher
2. vue2是如何进行数据更新的
3. vue3是如何进行数据更新的
框架相关
1. 能说一下dom diff的比对过程吗?
Dom diff就是将渲染前旧的virtual dom和即将渲染的新的virtual Dom做比对。(patch方法)
domdiff是一个递归的过程,以节点为单位比对
它主要有以下几个比对过程
-
首先比较新节点和老节点的标签是否相同, 不相同直接替换
-
如果新老节点的标签相同的话, 说明旧节点可以复用, 之后就去比对他们的属性。
进行更新属性操作
-
比对完属性之后, 会比对他们的文本, 更新文本操作
-
最后,会比对他们的孩子。 也是整个dom diff中的重点
比对孩子的时候, 会采用一个双指针的比对方法, 即给新节点和旧节点的孩子设置一个头指针和尾指针。
分别只想第一个孩子和最后一个孩子
-
接下来使用一个while循环去进行比对。 不管是新节点还是旧节点, 只要有一个的头指针小于尾指针,就退出循环
-
比对过程分为四种情况。
第一种正序结尾添加/删除
比对新节点的头指针指向的节点和旧头指针指向的节点一样,依次往后移动新旧头指针, 当某一方的头指针大于尾指针的时候,
退出循环,这时候另一方还有剩余,此时可以进行插入或者删除节点操作
第二种情况开头添加删除
这种情况依次发现新节点的头指针和老节点的头指针指向的节点不一样, 此时会去比对新节点的尾指针和就节点的尾指针
如果一样, 依次让尾指针-1。 最后结束时, 发现剩余。进行新增或删除操作
第三种情况倒叙
这时候会发现新节点的头指针和旧节点的尾指针不一样, 新节点的尾指针和旧节点的尾指针也不一样。
会用旧节点的头指针跟新节点的尾指针做比较, 如果一样, 那么吧旧节点头指针指向的节点移动到尾指针指向的节点后面
旧头指针+1, 新尾指针-1.
第四种情况。 无序。 使用key值找出位置。 进行移动
2. Vue 的computed计算属性是如何实现缓存的
-
创建一个watcher实例,并传入
lazy: true,包装computed的get方法computedGetter。 -
页面template使用fullName,触发fullName的getter方法,即包装好的getter方法computedGetter
-
computedGetter方法中因为
watcher.dirty为true,执行watcher.evaluate方法。- 在evaluate方法中,执行
this.value = this.get(), 并把dirty设为false - 执行了get方法,即执行computedGetter方法,即执行computed的get方法,就会触发firstName和lastName的getter,从而把watcher收集进firstName和lastName的依赖中。
- 在evaluate方法中,执行
-
重新获取computed属性的值,即触发getter方法时,因为dirty已经为false,就从
watcher.value取缓存值。 -
一旦firstName或者lastName发生变化,就会触发
watcher.update方法,设置dirty为true。 -
重新获取computed属性的值,就会执行
watcher.evaluate方法,重新计算,并改变缓存值。
缓存值存在哪里?
watcher.value
首先,computed本质也是一个watcher, 收集到响应式数据的dep属性中,通过Dep.target与属性进行关联。
初始化计算watcher的时候, lazy为true, 使用惰性求值。
之后使用该数据的时候, 调用watcher.evaluate方法触发到get方法, 会把dirty设置为false, 同时将计算出来的值
存到watcher.value中
如果依赖的值发生改变, 触发了setter,那么dirty会设置为false
前端模块化
1. webpack的异步加载如何实现
-
首先,如果在webpack中懒加载了一个模块, webpack会把这个懒加载模块打包成一个匿名函数的样子
这个匿名函数呢,就是把当前的代码块push到window上的webpackJsonP中。
-
需要加载这个模块的时候,webpack使用jsonP的方式对这个js进行加载,并且插入到html中
-
改写数组原生的push方法, 将它指向webpackJsonpCallback这个方法
这个函数的作用就是将已经加载过的模块在全局进行一个缓存。并且递归的调用父模块的这个方法
进行依次缓存
2. webpack的分包策略
- 新的 chunk 是否被共享或者是来自 node_modules 的模块
- 新的 chunk 体积在压缩之前是否大于 30kb
- 按需加载 chunk 的并发请求数量小于等于 5 个
- 页面初始加载时的并发请求数量小于等于 3 个
3. 简单说一下vite的原理
vite主要是一个基于es-module实现的一个web应用开发工具。
它启动一个静态服务器, 然后拦截es-module的请求, 将应用管理和模版代码通过对应的插件返回给客户端。
对比传统的webpack等打包工具, 它只需要变异, 不需要打包。 所以它的更新速度和响应速度都是非常快并且顺滑的。
但是现阶段因为依赖es-module, 所以导致很多require相关的语法并不能使用。
后端相关
1. 简单说一下node的事件循环机制
node事件循环机制跟浏览器的循环机制类似, 都是先执行住县城的程序
遇到一步任务的时候, 会把当前的异步任务放到事件队列中,
当主线程执行结束后, 依次按阶段执行清空异步任务队列。
区别就是异步队列的阶段不一样。
node的异步阶段主要有以下几部分:
-
timers: 主要是执行setTimeout和setInterval的回调函数
-
Pending-callBacks: 上一次事件队列中没有执行结束的i/o操作
-
Idle prepare: 内部系统使用
-
Poll-轮训: i/o操作阶段, 可能会阻塞当前进行, 比如说在i/o操作里面嵌套了一些setTimeout, setImmediate。
-
Check-检测阶段: 执行setImmediate
fs.readFile('./yueqi.txt', () => { setImmediate(() => { }) setTimeout(() => { }, 0) })
备注:
每个宏任务执行结束之后都会清空微任务队列
2.Nginx的正向代理和反向代理
-
正向代理代理的是客户端, 服务端看不到真正的客户端, 例如使用公司内网访问外网
server_name www.yueqi.com; resolver 8.8.8.8; # 谷歌的域名解析地址 server { location / { proxy_pass http://$http_host$request_uri; } } -
反向代理代理的是服务器端,客户端看不到真正的服务端。例如使用nginx代理应用服务器解决跨域
location ~ ^/api { proxy_pass http://localhost:3000; proxy_redirect default; #重定向 proxy_set_header Host $http_host; #向后传递头信息 proxy_set_header X-Real-IP $remote_addr; #把真实IP传给应用服务器 }
算法相关
1. 给定一个字符串,你的任务是计算这个字符串中有多少个回文字符串。(具有不同开始位置或结束位置的字串,即使是由相同的字符组成,也会被视作不同的字串)
leetcode连接: leetcode-cn.com/problems/pa…
function counSubstring(string) {
if(!string) { return 0 }
let arr = string.split('')
let count = 0;
for(let i=0; i < arr.length; i++) {
for(let j=i; j < arr.length; j++) {
if(isPland(s.substring(i, j+1))) {
count ++
}
}
}
return count
}
function isPland(string) {
let i = 0;
let j = string.length - 1;
while(i < j) {
if(string[i] !== string[j]) {
return false
}
i++;
j--
}
return true
}
2. 验证回文字符串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
**说明:**本题中,我们将空字符串定义为有效的回文串。
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function(s) {
// 正则去除非单词字符和下划线,然后转化为小写字符
let str = s.replace(/(\W|_)/g, '').toLocaleLowerCase()
// 下面就是用双指针判断是否是回文字符串的过程了
let left = 0
let right = str.length - 1
while (left < right) {
if (str[left] !== str[right]) {
return false
}
left++
right--
}
return true
}
3. 求时针和分针的夹角
分析:
分针一圈走过的度数:
360 / 12 * 5 = 6度
时针一圈走过的度数:
360 / 12 = 30度
所以如果分针在前, 那么夹角就是
6m - 30h
如果分针在后, 那么夹角就是
30h - 6m
但是这样存在误差, 需要计算一下时针每分钟走过的度数:
360 /( 12 / 60 每分钟走过的度数) = 0.5度
所以最终
6m - (30h+0.5m) 或者 30h+0.5m - 6m
3. 写一个方法计算Url中的参数
// 解析Url中的参数
function parseParams(url) {
if(!url) {
return
}
// 先拿到url参数
let queryString = url.split('?')[1]
// 设置结果集
let params = {};
// 依次拿到每个, 放入到结果集里面
const queryArray = queryString.split('&')
// ["a=1", "b=2"]
queryArray.reduce((obj, kv) => {
const [key, value] = kv.split('=');
// 如果key是一个索引key, 进行处理
depset(params, key.split(/[\[\]]/g).filter(x => x), value)
console.log('path', key.split(/[\[\]]/g).filter(x => x))
return params
}, {})
return params
}
function depset(params, path, value) {
let i = 0;
for(;i<path.length - 1; i++) {
console.log('path', path[i])
if(params[path[i]] === undefined) {
if(path[i+1].match(/^\d+$/)) {
params[path[i]] = []
}else {
params[path[i]] = {}
}
}
params = params[path[i]]
}
params[path[i]] = decodeURIComponent(value)
}
let a = parseParams('www.baidu.com?a[0]=1&b[1]=2')
console.log(a)
4. 大数相加
function add(a, b) {
a = a.split(''),
b = b.split('');
let sum = [];
go = 0;
while(a.length || b.length) {
let num1 = parseInt(a.pop()) || 0;
let num2 = parseInt(b.pop()) || 0;
let temp = num1 + num2 + go
if(temp > 9) {
temp %= 10
go = 1
}else {
go = 0
}
sum.unshift(tmp)
}
if(go) {sum.unshift(1)}
return sum.join('')
}
5. 如何安排最多的会议室
6. 计算奖金最佳分配方式
我们进行了一次黑客马拉松大赛,全公司一共分为了N个组, 每组一个房间排成一排开始比赛, 比赛结束后没有公布成绩,但是每个组能够看到自己相邻的两个组里比自己成绩低的组的成绩, 比赛结束之后要发奖金,以1w为单位,每个组都至少会发1w的奖金,另外,如果一个组发现自己的奖金没有高于比自己成绩低的组发的奖金,就会不满意,作为比赛的组织方,根据成绩计算出至少需要发多少奖金才能 让所有的组满意。
链接:www.nowcoder.com/discuss/509…
7. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
**注意:**给定 n 是一个正整数。
连接: leetcode-cn.com/problems/cl…
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
if(n <= 2) { return n }
let first = 1;
let second = 2;
for(let i = 3; i <= n; i++) {
let third = first + second;
first = second;
second = third
}
return second
};
8. 猴子吃香蕉
珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。
珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
链接:leetcode-cn.com/problems/ko…
/**
* @param {number[]} piles
* @param {number} H
* @return {number}
*/
// 解题思路: 最少每次吃掉1跟, 最多每次吃掉最大堆数的一根。
// 计算每次吃到speed = 1+n,可否立刻吃完, 如果可以, 那么返回speed
var minEatingSpeed = function(piles, H) {
let low = 1;
let max = 1;
for(let i = 0; i < piles.length; i++) {
max = Math.max(piles[i], max)
}
let high = max
// 计算总数
while(low < high) {
let mid = Math.floor((low + high) / 2 )
// 看当前的速度可不可以正好吃完
let time = cucluteTime(piles, mid)
console.log('time', time)
// 说明吃不完,需要提高速度
if(time > H) {
low = mid + 1
}else {
// 说明可以吃完,但不是最小速度
high = mid
}
}
return low
};
// 计算事件与H比较
function cucluteTime(total, speed) {
let time = 0
for(pile of total) {
time += Math.ceil(pile / speed)
}
return time
}
// 速度 = 总数 / H
9. 二分查找
function find(array, target) {
let left = 0;
let right = array.length - 1;
while(left <= right) {
if(array[left] === target) { return left};
if(array[right] === target) { return right };
// 取中间数
let mid = Math.floor((left + right) / 2)
if(array[mid] < target) {
left = mid + 1
}else {
rigth = mid - 1
}
}
return -1
}
10. 快速排序
function quickSort(array) {
if(array.length <= 1) { return arrar }
let midIndex = Math.floor(array.length / 2);
let pivot = array.splice(midIndex, 1)[0];
let left = [];
lef right = [];
for(let i = 0; i < array.length; i++) {
if(arr[i] < pivot) {
left.push(arr[i])
}else {
right.push(arr[i])
}
}
return quickSort(left).concat([pivot], quickSort(right))
}
11. 防抖
function dobuce(fn, time) {
let timer = null;
return function () {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
}, time)
}
}
12. 截流
function throttle(fn, wait) {
let previous = 0;
return function() {
let now = Date.now();
if(now - previous > wait) {
fn();
previous = now
}
}
}
13. 实现一个缓动动画
let target = {x: 0}
tweenAnimation(target, 0, 100, 5000, () => {})
function tweenAnimation(target: any, from: number, to: number, duration: number, callback: () => void) {
// 计算在总时间内一共需要多少帧数来完成动画
let during = Math.ceil(duration / 17);
// 动画开始的时间
let startTime = Date.now(),
lastTime = Date.now();
// 初始帧数
let currentFrame = 0;
let id: number = 0
// 运动函数 - 绘制运动函数里面的一帧
let step = function () {
// 计算上一帧到这一帧内绘制的时间差, 如果大于30帧, 正常走, 如果小于, 计算当前时间内需要移动的内容
const currentTime = Date.now();
let interval = currentTime - lastTime;
let fps = Math.round(1000 / interval)
lastTime = currentTime;
// 计算fps
if(fps >= 30) {
currentFrame ++;
}else {
let _start = Math.floor((currentTime - startTime) / 17);
currentFrame = _start > currentFrame ? _start : currentFrame + 1;
}
// start = 0: value = from
// start = framCoumt: value = to - from
// start = 0 ~ framCoumt: 计算value
// value - 需要移动的距离: 当前帧 * (每一帧需要移动的距离即:总变化距离 / 总帧数 ) + 初始距离
let value = currentFrame * (to - from) / during + from
if(currentFrame <= during) {
target.x = value
// console.log('target', target)
id = requestAnimationFrame(step)
}else {
// 动画结束
cancelAnimationFrame(id)
}
}
step()
}
14 async 调度
class Scheduler {
constructor() {
this.awaitArr = [];
this.count = 0;
}
async add(promiseCreator) {
this.count >= 2 ? await new Promise(resolve => this.awaitArr.push(resolve)) : '';
this.count++;
const res = await promiseCreator();
this.count--;
this.awaitArr.length && this.awaitArr.shift()();
return res;
}
}
const timeout = (time) => new Promise(resolve => {
setTimeout(resolve, time)
})
const scheduler = new Scheduler()
const addTask = (time, order) => {
scheduler.add(() => timeout(time))
.then(() => console.log(order))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
// output: 2 3 1 4
// 一开始,1、2两个任务进入队列
// 500ms时,2完成,输出2,任务3进队
// 800ms时,3完成,输出3,任务4进队
// 1000ms时,1完成,输出1
// 1200ms时,4完成,输出4
15. 深拷贝
const memo = new WeakMap()
const isObject = (object) => {
Object.prototype.toString.call(object) === '[object Object]'
}
function deepClone(target) {
if(target == null) { return target }
if(typeof target !== 'object') { return target }
let instance = new target.constructor()
if(memo.has(target)) {
return memo.get(target)
}
memo.set(target, instance);
for(let key in target) {
instance[key] = deepClone(target[key])
}
return instance
}
var a = {
a: [1, 2, 3],
b: { name: 'ccc' }
}
var b = deepClone(a)
b.a = [1]
b.b.name = 'yueqi'
console.log(a === b, b, a)
16. 扁平数组转树形节点
function toTree(array) {
let tree = [];
let idMap = {}
for(let item of array) {
idMap[item.id] = item
idMap[item.id].children = []
}
array.forEach((item, index) => {
if(item.pid !== 0) {
idMap[item.pid].children.push(item)
}else {
tree.push(item)
}
// delete idMap[item.id]
})
return tree
}
let a = [
{id: 1, name: '部门1', pid: 0},
{id: 2, name: '部门2', pid: 1},
{id: 3, name: '部门3', pid: 1},
{id: 4, name: '部门4', pid: 3},
{id: 5, name: '部门5', pid: 4},
{id: 6, name: '部门6', pid: 0 },
]
const b = toTree(a)
console.log(JSON.stringify(b, null, 2))
/*
扁平数组转Tree
给定一个扁平数组,数组内每个对象的id属性是唯一的。每个对象具有pid属性,pid属性为0表示为根节点(根节点只有一个),其它表示自己的父节点id。
编写一段程序,输入为给定的扁平数组,输出要求为一个树结构,为其中每个对象增加children数组属性(里面存放child对象)。
解法有很多种,性能最优的方案最佳
给定输入:
[
{id: 1, name: '部门1', pid: 0},
{id: 2, name: '部门2', pid: 1},
{id: 3, name: '部门3', pid: 1},
{id: 4, name: '部门4', pid: 3},
{id: 5, name: '部门5', pid: 4},
]
*/
Css相关
1. bfc布局规则
BFC 即 块级格式化上下文(Block Formatting Contexts ()
具备BFC特性的的元素可以看作隔离的容器。容器里面的布局不会影响外面的布局
如何使用BFC:
- body 根元素
- 浮动元素:float 除 none 以外的值
- 绝对定位元素:position (absolute、fixed)
- display 为 inline-block、table-cells、flex
- overflow 除了 visible 以外的值 (hidden、auto、scroll)
BFC作用
- 解决外间距重叠的问题, 块级元素的margin-top和margin-bottom会重叠
- 可以包含浮动元素
- BFC 可以阻止元素被浮动元素覆盖
个人简介
-
画一下微信小程序的登陆时许图
-
ts的构建后的代码如何优化
-
如何提高ts的编译效率
-
实现一个reduce
-
取数组的交集和并集
-
架构如何表达
从使用过程来说
从加载到解析, 到路由, 到缓存, 分别是怎么做的
-
lowcode 架构
协议, 交互,
-
反转双向链表,
-
排序的几种方式
-
项目的难点。
-
缓存的链路
-
重复性建设, 接入 。 业务与业务的隔离。人员培养, 梯队建设。 员工效率提高, 领域的理解
项目和问题的描述太细了。 表达要简短。