跳到主要内容

前端底层原理

参考: https://github.com/KieSun/all-of-frontend

浏览器渲染原理及流程

解析 html 构建 dom 树->构建 render 树->布局 render 树->绘制 render Dom Tree + css Tree -> render Tree

输入 url 到页面展示的过程

输入网址->DNS 解析->建立 tcp 连接(三次握手,四次挥手)->客户端发送 http 请求(http,https)->服务处理、响应处理->浏览器展示 html(再细分的话就是浏览器渲染原理及流程)

前端性能优化(reflow 和 repaint)

reflow(回流): 布局改变

reflow:几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显 示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲 染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。

下面情况会导致 reflow 发生:

  1. 改变窗口大小
  2. 改变文字大小
  3. 内容的改变,如用户在输入框中敲字
  4. 激活伪类,如:hover
  5. 操作 class 属性
  6. 脚本操作 DOM
  7. 计算 offsetWidth 和 offsetHeight
  8. 设置 style 属性

repaint(重绘):css 样式改变

repaint:如果只是改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性,将只会引起浏览器 repaint(重绘)。repaint 的速度明显快于 reflow

那么为了减少回流要注意哪些方式呢?

1:不要通过父级来改变子元素样式,最好直接改变子元素样式,改变子元素样式尽可能不要影响父元素和兄弟元素的大小和尺寸

2:尽量通过 class 来设计元素样式,切忌用 style

axios 的底层原理

ajax 利用 XMLHttpRequest 的异步请求完成,通过 open 设置相应的请求信息,绑定 onreadystatechange 事件,然后根据服务器返回状态的不同来做不同处理

axios 的拦截:

axios.interceptors.request.use 请求拦截

axios.interceptors.response.use 响应拦截

jq 链式调用原理

利用 prototype 原型,当实例在调用内部方法时,返回当前实例对象的 this,继续访问自己的原型,形成 jq 链,

例如,$('class').addClass('desd').delay().css()

js 原型链和原型

原型:任何对象都有一个原型对象,这个原型对象由对象的内置属性_proto_指向它的构造函数的 prototype 指向的对象,即任何对象都是由一个构造函数创建的,
但是不是每一个对象都有 prototype,只有方法才有 prototype。

原型链:每个对象都有原型,当访问这个变量时,如果找不到会一直访问它的原型_proto_,一直循环下去,就产生了原型链

prototype 如何产生的

当我们声明一个函数时,这个属性就被自动创建了


实例对象的 _proto_ 如何产生的

当我们使用 new 操作符时,生成的实例对象拥有了_proto_属性

总结

Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它

Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它

Function.prototypeObject.prototype 是两个特殊的对象,他们由引擎来创建

除了以上两个特殊对象,其他对象都是通过构造器 new 出来的

函数的 prototype 是一个对象,也就是原型

对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链

new 的过程

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象

什么是回调地狱

在 js 中,我们经常通过回调实现异步逻辑,一旦嵌套层级多了,就会形成回调地狱

promise

promise 的状态:pending,resolved,rejected

pending: 等待中,或者进行中,表示还没有得到结果

resolved(Fulfilled): 已经完成,表示得到了我们想要的结果,可以继续往下执行

rejected: 也表示得到结果,但是由于结果并非我们所愿,因此拒绝执行

状态不受外界影响,而且状态只能从 pending 改变为 resolved 或者 rejected,并且不可逆

缺点:

  1. 一旦建立就会立即执行,中途无法取消
  2. 不设置回调函数,promise 内部抛出错误,不会反应到外部

因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回的是 promise,所以它们可以被链式调用

Promise 静态方法

Promise.all(iterable)

这个方法返回一个新的 promise 对象,该 promise 对象在 iterable 参数对象里所有的 promise 对象都成功的时候才会触发成功,一旦有任何一个 iterable 里面的 promise 对象失败则立即触发该 promise 对象的失败。

常被用于处理多个 promise 对象的状态集合

Promise.allSettled(iterable)

等到所有 promises 都已敲定(settled)(每个 promise 都已兑现(fulfilled)或已拒绝(rejected))。

返回一个 promise,该 promise 在所有 promise 完成后完成。并带有一个对象数组,每个对象对应每个 promise 的结果。

Promise.any(iterable)

接收一个 Promise 对象的集合,当其中的一个 promise 成功,就返回那个成功的 promise 的值。

Promise.race(iterable)

当 iterable 参数里的任意一个子 promise 被成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应句柄,并返回该 promise 对象。

Promise.reject(reason)

返回一个状态为失败的 Promise 对象,并将给定的失败信息传递给对应的处理方法

Promise.resolve(value)

返回一个状态由给定 value 决定的 Promise 对象。如果该值是 thenable(即,带有 then 方法的对象),返回的 Promise 对象的最终状态由 then 方法执行决定;否则的话(该 value 为空,基本类型或者不带 then 方法的对象),返回的 Promise 对象状态为 fulfilled,并且将该 value 传递给对应的 then 方法。通常而言,如果您不知道一个值是否是 Promise 对象,使用 Promise.resolve(value) 来返回一个 Promise 对象,这样就能将该 value 以 Promise 对象形式使用。

Promise 基础实例

let myFirstPromise = new Promise(function (resolve, reject) {
//当异步代码执行成功时,我们才会调用resolve(...), 当异步代码失败时就会调用reject(...)
//在本例中,我们使用setTimeout(...)来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法.
setTimeout(function () {
resolve('成功!'); //代码正常执行!
}, 250);
});

myFirstPromise.then(function (successMessage) {
//successMessage的值是上面调用resolve(...)方法传入的值.
//successMessage参数不一定非要是字符串类型,这里只是举个例子
console.log('Yay! ' + successMessage);
});

Promise 链式调用

new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 500);
})
.then((res) => {
console.log(res);
return new Promise((resolve) => {
setTimeout(() => {
resolve(2);
}, 500);
});
})
.then((res) => {
console.log('dosomething', res);
});

vue 生命周期

创建:

beforeCreate:组件实例刚被创建,el 和 data 都未初始化

created: 组件创建完成,属性已绑定,但 DOM 还未生成,el 还没有初始化,data 初始化

挂载:

beforeMount:完成了 el 和 data 的初始化

mounted:完成挂载

更新:

beforeUpdate

updated

卸载:

beforeDestory

destoryed

react 生命周期

挂载:

componentWillMount 在渲染前调用,在客户端也在服务端。

componentDidMount 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的 DOM 结构,可以通过 this.getDOMNode()来进行访问。 如果你想和其他 JavaScript 框架一起使用,可以在这个方法中调用 setTimeout, setInterval 或者发送 AJAX 请求等操作(防止异步操作阻塞 UI)。

更新:

componentWillReceiveProps 在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化 render 时不会被调用。

shouldComponentUpdate 返回一个布尔值。在组件接收到新的 props 或者 state 时被调用。在初始化时或者使用 forceUpdate 时不被调用。 可以在你确认不需要更新组件时使用。

componentWillUpdate 在组件接收到新的 props 或者 state 但还没有 render 时被调用。在初始化时不会被调用。

componentDidUpdate 在组件完成更新后立即调用。在初始化时不会被调用。

卸载:

componentWillUnmount 在组件从 DOM 中移除之前立刻被调用。

时间复杂度

常见的时间复杂度 最常见的时间复杂度有常数阶 O(1),对数阶 O(logn),线性阶 O(n),线性对数阶 O(nlogn),平方阶 O(n²) 从下图可以清晰的看出常见时间复杂度的对比: 20200917170826106

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2)

O(1)

传说中的常数阶的复杂度,这种复杂度无论数据规模 n 如何增长,计算时间是不变的。

const increment = (n) => n++;

举个简单的例: 不管 n 如何增长,都不会影响到这个函数的计算时间,因此这个代码的时间复杂度都是 O(1)。

O(n)

线性复杂度,随着数据规模 n 的增长,计算时间也会随着 n 线性增长。典型的 O(n)的例就是线性查找。

const linearSearch = (arr, target) => {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i;
}
}
return -1;
};

线性查找的时间消化与输入的数组数量 n 成个线性例,随着 n 规模的增大,时间也会线性增长。

O(logn)

对数复杂度,随着问题规模 n 的增长,计算时间也会随着 n 对数级增长。典型的例是二分查找法。

function binarySearch(arr: any[], target: any) {
let left = 0, right = arr.length - 1;

while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid; // 找到目标,返回索引
} else if (arr[mid] < target) {
left = mid + 1; // 在右半区继续查找
} else {
right = mid - 1; // 在左半区继续查找
}
}

return -1; // 未找到目标,返回-1
}

在二分查找法的代码中,通过 while 循环,成 2 倍数的缩减搜索范围,也就是说需要经过 log2^n 次即可跳出循环。

事实上在实际项目中, O(logn) 是个非常好的时间复杂度,比如当 n=100 的数据规模时,二分查找只需要 7 次,线性查找需要 100 次,这对于计算机而言差距不大,但是当有 10 亿的数据规模的时候,二分查找依然只需要 30 次,而线性查找需要惊人的 10 亿次, O(logn) 时间复杂度的算法随着数据规模的增大,它的优势就越明显。

O(nlogn)

线性对数复杂度,随着数据规模 n 的增长,计算时间也会随着 n 呈线性对数级增长。

const mergeSort = (array) => {
const len = array.length
if (len <2) {
return len
}
const mid = Math.floor(len / 2)
const first = array.slice(0, mid)
const last = array.slice(mid)
return merge(mergeSort(fist), mergeSort(last))
}

function merge(left, right){
var result = [];
while (left.length && right.length){
// 这其中典型代表就是归并排序
result.push(left.shift());
} else {
result.push(right.shift());
}
while (left.length){
result.push(left.shift());
}
while (right.length){
result.push(right.shift());
}
return result;
}

O(n²)

平方级复杂度,典型情况是当存在双重循环的时候,即把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是O(n²) 了,代表应用是冒泡排序算法。

function bubleSort(arra){
var temp;
for(var i=0;i<arra.length;i++){
for(var j=0;j<arra.length-i-1;j++){
if(arra[j]>arra[j+1]){
temp=arra[j];
arra[j]=arra[j+1];
arra[j+1]=temp;
}
}
};

前端项目工程化

  1. 项目架构设计
  2. 目录结构定义
  3. 制定项目开发规范(ESLint 规范)
  4. 模块化、组件化
  5. 前后端接口规范
  6. 性能优化、自动化部署(压缩、合并、打包)

模块化

将 javascript 程序拆分为可按需导入的单独模块的机制

就是像搭积木一样一块一块的,利用 import 和 export 或 requeire 以及 webpack

webpack

webpack 的一些知识的概括

entry

output

端口

css loader

img loader

性能优化

如何提升项目的性能

(1)代码包优化

屏蔽打包 sourceMap

对项目代码中的 JS/CSS/SVG(*.ico) 文件进行 gzip 压缩

(2)对路由组件进行懒加载

(3)源码优化 vue

v-if 和 v-show 选择调用

为 item 设置唯一 key 值

细分 vuejs 组件

减少 watch 的数据

内容类系统的图片资源使用懒加载

SSR(服务端渲染)

(4)用户体验优化 better-click 防止 iphone 点击延迟(在开发移动端 vuejs 项目时,手指触摸时会出现 300ms 的延迟效果,可以采用 better-click 对 ipone 系列的兼容体验优化。)

菊花 loading

骨架屏加载

(5)用 cdn 加载资源文件

什么是BFC(Block Formatting Context)

BFC 的定义

BFC(Block Formatting Context,块级格式化上下文)是 Web 页面中盒模型布局的 CSS 渲染模式,指一个独立的渲染区域或者说是一个隔离的独立容器。

BFC 的形成条件

满足以下任一条件的元素就会创建 BFC:

  1. 根元素html 标签
  2. 浮动元素float 值不为 none
  3. 绝对定位元素position 值为 absolutefixed
  4. display 为特定值display 值为 inline-blocktable-celltable-captionflexinline-flexgridinline-grid
  5. overflow 不为 visibleoverflow 值为 hiddenautoscroll
  6. contain 属性contain 值为 layoutcontentpaintstrictall
/* 以下元素都会形成 BFC */
.container {
overflow: hidden; /* overflow 不为 visible */
}

.float-box {
float: left; /* 浮动元素 */
}

.absolute-box {
position: absolute; /* 绝对定位 */
}

.inline-block-box {
display: inline-block; /* display 为 inline-block */
}

.flex-container {
display: flex; /* flex 布局 */
}

BFC 的特性

  1. 内部的 Box 会在垂直方向上一个接一个放置

    • 即使存在浮动元素,普通块级元素也会从上到下排列
  2. BFC 的区域不会与 float box 重叠

    • BFC 会避开浮动元素,形成独立的布局区域
  3. 计算 BFC 的高度时,浮动元素也参与计算

    • 这是清除浮动的原理之一
  4. BFC 是页面上的一个独立容器,子元素不会影响外部

    • 内外的 margin 不会重叠
    • 子元素的浮动不会影响到父元素的外部布局

BFC 的应用场景

1. 解决 margin 重叠问题

/* ❌ margin 重叠问题 */
.box1 {
margin-bottom: 20px;
}
.box2 {
margin-top: 20px;
}
/* 实际间距是 20px,而不是 40px */

/* ✅ 解决方案:创建 BFC */
.box2 {
overflow: hidden; /* 触发 BFC */
margin-top: 20px;
}
/* 这样两个 box 之间会有真正的 40px 间距 */

2. 清除浮动(自适应布局)

/* ❌ 父元素高度塌陷 */
.parent {
/* 没有设置高度 */
}
.child {
float: left;
width: 200px;
height: 100px;
}
/* 父元素高度为 0,因为子元素浮动了 */

/* ✅ 方案 1:父元素触发 BFC */
.parent {
overflow: hidden; /* 触发 BFC,包含浮动元素 */
}

/* ✅ 方案 2:使用 clearfix */
.clearfix::after {
content: '';
display: table;
clear: both;
}
.parent::after {
@extend .clearfix;
}

3. 两栏自适应布局

<div class="container">
<div class="left">左侧固定宽度</div>
<div class="right">右侧自适应</div>
</div>

<style>
.container {
overflow: hidden; /* 触发 BFC */
}
.left {
float: left;
width: 200px;
height: 300px;
background: lightblue;
}
.right {
overflow: hidden; /* 触发 BFC,不会与 left 重叠 */
height: 300px;
background: lightgreen;
}
</style>

4. 防止元素被浮动覆盖

/* 当有浮动元素时,普通元素可能会被覆盖 */
.float-element {
float: left;
width: 200px;
}

/* ✅ 让普通元素触发 BFC,避免被覆盖 */
.normal-element {
overflow: hidden; /* 或其他触发 BFC 的属性 */
}

BFC 与 IFCC、GFC、FFC 的区别

  • BFC(Block Formatting Context):块级格式化上下文,用于块级布局
  • IFC(Inline Formatting Context):行内格式化上下文,用于行内元素布局
  • GFC(Grid Formatting Context):网格格式化上下文,用于 Grid 布局
  • FFC(Flex Formatting Context):弹性格式化上下文,用于 Flex 布局

总结

BFC 是 CSS 布局中的重要概念,理解它有助于解决:

  • margin 重叠问题
  • 浮动引起的高度塌陷
  • 自适应多栏布局
  • 防止元素被浮动覆盖

掌握 BFC 的触发条件和特性,可以更灵活地控制页面布局。

vue 和 react 的区别

最主要为前 2 个点的区别

数据是不是可变的。

react 是函数式的思想,是单向数据流,使用 setState 来重新渲染

vue 是响应式的,基于数据可变的,通过对每一个属性建立监听,当属性变化时,响应式的更新对应的虚拟 dom

通过 js 来操作一切,还是用各自的处理方式。

react 的思路是 all in js,通过 js 来生成 html,所以设计了 jsx,还有通过 js 来操作 css,社区的 styled-component、jss 等

vue 是把 html,css,js 组合到一起,用各自的处理方式,vue 有单文件组件,可以把 html、css、js 写到一个文件中,html 提供了模板引擎来处理

类式的组件写法,还是声明式的写法(vue3.0 之前)

react 是类式的写法,api 很少

而 vue 是声明式的写法,通过传入各种 options,api 和参数都很多。所以 react 结合 typescript 更容易一起写,vue 稍微复杂

什么功能内置,什么交给社区去做

react 做的事情很少,很多都交给社区去做,vue 很多东西都是内置的,写起来确实方便一些

比如 redux 的 combineReducer 就对应 vuex 的 modules

比如 reselect 就对应 vuex 的 getter 和 vue 组件的 computed

vuex 的 mutation 是直接改变的原始数据,而 redux 的 reducer 是返回一个全新的 state,所以 redux 结合 immutable 来优化性能,vue 不需要

闭包

一个内层函数中访问到其外层函数的作用域

存在自由变量的函数就是闭包。

例子:

let a = 1;
let b = function () {
console.log(a);
};
b(); //a=1 自由变量,b函数为闭包

闭包是在函数创建的时候,让函数打包带走的根据函数内的外部引用来过滤作用域链剩下的链。它是在函数创建的时候生成的作用域链的子集,是打包的外部环境。eval 因为没法分析内容,所以直接调用会把整个作用域打包(所以尽量不要用 eval,容易在闭包保存过多的无用变量),而不直接调用则没有闭包。

js 函数和变量的优先级哪个高?

函数提升优先级比变量提升要高,且不会被变量声明覆盖,但是会被变量赋值覆盖。

JS 变量提升和函数提升

例子:

// 函数提升优先级高于变量提升,所以foo会为foo
console.log(foo); // function foo() {}
foo = 3;
console.log(foo); // 3
function foo() {}

在 ES6 中,let 声明的变量可以修改,而 const 声明的变量不可更改,使用 const 声明必须指定初始值。

  • var 命令在变量的定义被执行之前就初始化变量,并拥有一个默认的 undefined 值。
  • letconst 命令会形成暂时性死区,在变量的定义被执行之前都不会初始化变量,避免在声明语句之前的不正确调用。
  • 如果定义时没有给定值的话,let 声明的变量会赋值为 undefined,而 const 声明的变量会报错。
  • ES6 明确规定,如果区块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。 总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
const test = 1;
test = 2 错误

const ddd = { title: '' };
ddd.title = '12345' 正确
ddd.title = 1 错误

call、apply、bind

文档

Function.prototype.call() - JavaScript | MDN (mozilla.org)

Function.prototype.apply() - JavaScript | MDN (mozilla.org)

Function.prototype.bind() - JavaScript | MDN (mozilla.org)

JS 中的 call、apply、bind 方法详解 - SegmentFault 思否

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

注意:该方法的语法和作用与 apply() 方法类似,只有一个区别,就是:

call() 方法接受的是一个参数列表

apply() 方法接受的是一个包含多个参数的数组

apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;

apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;

apply 、 call 、bind 三者都可以利用后续参数传参;

bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

例子:

var obj = {
x: 81,
};

var foo = {
getX: function() {
return this.x;
}
}

console.log(foo.getX.bind(obj)()); //81
console.log(foo.getX.call(obj)); //81
console.log(foo.getX.apply(obj)); //81

什么是http 协议

超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP 是万维网的数据通信的基础。

三次握手和四次挥手

c75c10385343fbf2f97f3e3decd7c58964388f8b

第一次握手:建立连接时,客户端发送 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 连接成功)状态,完成三次握手。

四次握手过程理解

64380cd7912397dd5170b7330b2bbdbed0a2872a 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 连接的时间要比客户端早一些。

vue 的通信都有哪些?

props
$emit(常用)
$attrs和$listeners
中央事件总线(非父子组件间通信)
v-model
provide和inject
$parent 和$children
vuex

防抖和节流

防抖(debounce):在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时,重新发出定时器。

节流(throttle):规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

防抖(debounce)

search 搜索联想,用户在不断输入值时,用防抖来节约请求资源。 window 触发 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

节流(throttle)

鼠标不断点击触发,mousedown(单位时间内只触发一次) 监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 来判断

引用类型和数据类型

引用类型:Object、Array、RegExp、Date、Function

区别:引用类型值可添加属性和方法,而基本类型值则不可以。

数据类型:Number、String、Boolean、Null、Undefined、object、symbol、bigInt。

那我们都知道,在 ES6 前,JavaScript 共六种数据类型,分别是:

Undefined、Null、Boolean、Number、String、Object

然而当我们使用 typeof 对这些数据类型的值进行操作的时候,返回的结果却不是一一对应,分别是:

undefined、object、boolean、number、string、object

注意以上都是小写的字符串。Null 和 Object 类型都返回了 object 字符串。

Class 及继承

Class

// es6语法
class Person {
constructor(name) {
this.name = name;
}

sayHello() {
console.log(`我的名字:${this.name}`);
}
}

let data = new Person('深海如梦');
data.sayHello();
// es5语法
function Person(name) {
this.name = name;
}

Person.prototype.sayHello = function () {
console.log(`我的名字:${this.name}`);
};

let data = new Person('深海如梦');
data.sayHello();

继承

原型链继承

function Parent() {
this.name = 'kevin';
}

Parent.prototype.getName = function () {
console.log(this.name);
};

function Child() {}

Child.prototype = new Parent();

var child1 = new Child();

console.log(child1.getName()); // kevin

借用构造函数(经典继承)

// 缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Parent() {
this.names = ['kevin', 'daisy'];
}

function Child() {
Parent.call(this);
}

var child1 = new Child();

child1.names.push('yayu');
console.log(child1.names);

var child2 = new Child();
console.log(child2.names);

/* ------------------------------------------------ */
function Parent(name) {
this.name = name;
}

function Child(name) {
Parent.call(this, name);
}

var child1 = new Child('kevin');

console.log(child1.name); // kevin

var child2 = new Child('daisy');

console.log(child2.name); // daisy

组合继承

// 原型链继承和经典继承双剑合璧。
// 优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name);
};

function Child(name, age) {
Parent.call(this, name);
this.age = age;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

寄生组合式继承

为了方便大家阅读,在这里重复一下组合继承的代码:

function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name);
};

function Child(name, age) {
Parent.call(this, name);
this.age = age;
}

Child.prototype = new Parent();

var child1 = new Child('kevin', '18');

console.log(child1);

组合继承最大的缺点是会调用两次父构造函数。

// 一次是设置子类型实例的原型的时候:
Child.prototype = new Parent();
// 一次在创建子类型实例的时候:
var child1 = new Child('kevin', '18');
// 回想下 new 的模拟实现,其实在这句中,我们会执行:
Parent.call(this, name);

在这里,我们又会调用了一次 Parent 构造函数。

所以,在这个例子中,如果我们打印 child1 对象,我们会发现 Child.prototype 和 child1 都有一个属性为 colors,属性值为['red', 'blue', 'green']

那么我们该如何精益求精,避免这一次重复调用呢?

如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢?

看看如何实现:

function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name);
};

function Child(name, age) {
Parent.call(this, name);
this.age = age;
}

// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();

var child1 = new Child('kevin', '18');

console.log(child1);

最后我们封装一下这个继承方法:

function object(o) {
function F() {}
F.prototype = o;
return new F();
}

function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}

// 当我们使用的时候:
prototype(Child, Parent);

引用《JavaScript 高级程序设计》中对寄生组合式继承的夸赞就是:

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

async/await

async 函数是使用 async 关键字声明的函数。 async 函数是 AsyncFunction 构造函数的实例, 并且其中允许使用 await 关键字。 async 和 await 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise。 await 操作符用于等待一个 Promise 对象。它只能在异步函数 async function 中使用。

例子

const getData = () => new Promise((resolve) => setTimeout(() => resolve('data'), 1000));

async function test() {
const data = await getData();
console.log('data: ', data);
const data2 = await getData();
console.log('data2: ', data2);
return 'success';
}

// 这样的一个函数 应该再1秒后打印data 再过一秒打印data2 最后打印success
test().then((res) => console.log(res));

并发模型与事件循环

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其它语言中的模型截然不同,比如 C 和 Java。

栈:函数调用形成了一个由若干帧组成的栈。

function foo(b) {
let a = 10;
return a + b + 11;
}

function bar(x) {
let y = 3;
return foo(x * y);
}

console.log(bar(7)); // 返回 42

当调用 bar 时,第一个帧被创建并压入栈中,帧中包含了 bar 的参数和局部变量。 当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。

对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。

队列

一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。

javascript 永不阻塞

垃圾回收

JavaScript 在变量被创建时分配内存,并在对象不再使用时自动释放内存,这个过程被称为垃圾回收。另外我们主要学习 V8 引擎下的垃圾回收机制。

数组去重的方法

一、利用 ES6 Set 去重(ES6 中最常用)

function unique (arr) {
return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。

二、利用 for 嵌套 for,然后 splice 去重(ES5 中最常用)

function unique(arr){
for(var i=0; i<arr.length; i++){
for(var j=i+1; j<arr.length; j++){
if(arr[i]==arr[j]){ //第一个等同于第二个,splice方法删除第二个
arr.splice(j,1);
j--;
}
}
}
return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}] //NaN和{}没有去重,两个null直接消失了

双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。 想快速学习更多常用的 ES6 语法,可以看我之前的文章《学习 ES6 笔记 ── 工作中常用到的 ES6 语法》

三、利用 indexOf 去重

function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var array = [];
for (var i = 0; i < arr.length; i++) {
if (array .indexOf(arr[i]) === -1) {
array .push(arr[i])
}
}
return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //NaN、{}没有去重

新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则 push 进数组。

四、利用 sort()

function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return;
}
arr = arr.sort()
var arrry= [arr[0]];
for (var i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i-1]) {
arrry.push(arr[i]);
}
}
return arrry;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined] //NaN、{}没有去重

利用 sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。

五、利用对象的属性不能相同的特点进行去重(这种数组去重的方法有问题,不建议用,有待改进)

function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var arrry= [];
var obj = {};
for (var i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
arrry.push(arr[i])
obj[arr[i]] = 1
} else {
obj[arr[i]]++
}
}
return arrry;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", 15, false, undefined, null, NaN, 0, "a", {…}] //两个true直接去掉了,NaN和{}去重

六、利用 includes

function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var array =[];
for(var i = 0; i < arr.length; i++) {
if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
array.push(arr[i]);
}
}
return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] //{}没有去重

七、利用 hasOwnProperty

function unique(arr) {
var obj = {};
return arr.filter(function(item, index, arr){
return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
})
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}] //所有的都去重了

利用 hasOwnProperty 判断是否存在对象属性

八、利用 filter

function unique(arr) {
return arr.filter(function(item, index, arr) {
//当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
return arr.indexOf(item, 0) === index;
});
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]

九、利用递归去重

function unique(arr) {
var array= arr;
var len = array.length;

array.sort(function(a,b){ //排序后更加方便去重
return a - b;
})

function loop(index){
if(index >= 1){
if(array[index] === array[index-1]){
array.splice(index,1);
}
loop(index - 1); //递归loop,然后数组去重
}
}
loop(len-1);
return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

十、利用 Map 数据结构去重

function arrayNonRepeatfy(arr) {
let map = new Map();
let array = new Array(); // 数组用于返回结果
for (let i = 0; i < arr.length; i++) {
if(map .has(arr[i])) { // 如果有该key值
map .set(arr[i], true);
} else {
map .set(arr[i], false); // 如果没有该key值
array .push(arr[i]);
}
}
return array ;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

创建一个空 Map 数据结构,遍历需要去重的数组,把数组的每一个元素作为 key 存到 Map 中。由于 Map 中不会出现相同的 key 值,所以最终得到的就是去重后的结果。

十一、利用 reduce+includes

function unique(arr){
return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

十二、[...new Set(arr)]

[...new Set(arr)]
//代码就是这么少----(其实,严格来说并不算是一种,相对于第一种方法来说只是简化了代码)

javascript是单线程还是多线程?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,JavaScript为什么是单线程,而不是多线程?明明这样能提高效率啊。

JavaScript为什么是单线程?

JavaScript的单线程,与它的用途有关。JavaScript最初被设计用在浏览器中,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题。

比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以为了避免复杂性,JavaScript从诞生起就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。

2.png

只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制。这个过程会不断重复。

什么是单页面应用

单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。

MVVM和MVC

MVVM与MVC的区别有:

1、mvvm各部分的通信是双向的,而mvc各部分通信是单向的;

2、mvvm是真正将页面与数据逻辑分离放到js里去实现,而mvc里面未分离。

MVC(例子:Java+jsp)

MVC是包括view视图层、controller控制层、model数据层。各部分之间的通信都是单向的。

View 传送指令到 ControllerController 完成业务逻辑后,要求 Model 改变状态Model 将新的数据发送到 View,用户得到反馈

img

MVVM(例子:vue/react)

MVVM包括view视图层、model数据层、viewmodel层。各部分通信都是双向的。采用双向数据绑定,View的变动,自动反映在 ViewModel,反之亦然。其中ViewModel层,就是View和Model层的粘合剂,他是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他各种各样的代码的极好的地方。说白了,就是把原来ViewController层的业务逻辑和页面逻辑等剥离出来放到ViewModel层

img

MVC与MVVM的具体区别

在MVC里,View是可以直接访问Model的,所以View里会包含Model信息以及一些业务逻辑。 MVC模型关注的是Model的不变,所以在MVC模型里,Model不依赖于View,但是 View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。

MVVM在概念上是真正将页面与数据逻辑分离的模式,它把数据绑定工作放到一个JS里去实现,而这个JS文件的主要功能是完成数据的绑定,即把model绑定到UI的元素上。此外MVVM另一个重要特性双向绑定,它更方便你去同时维护页面上都依赖于某个字段的N个区域,而不用手动更新它们。

foreach和map的区别

一、相同点: 都是循环遍历数组中的每一项 forEach和map方法里每次执行匿名函数都支持3个参数,参数分别是item(当前每一项)、index(索引值)、arr(原数组)

匿名函数中的this都是指向window

只能遍历数组

array.map(function(item,index,arr){},this)

Array.forEach(function(item,index,arr){},this)

二、区别:

1.forEach() 没有返回值。

2.map() 有返回值,可以return 出来。

高阶函数

map/reduce

filter

sort排序算法

react高阶组件

什么是高阶组件?

一个高阶组件只是一个包装了另外一个 React 组件的 React 组件

这种形式通常实现为一个函数,本质上是一个类工厂(class factory),它下方的函数标签伪代码启发自 Haskell

hocFactory:: W: React.Component => E: React.Component

这里 W(WrappedComponent) 指被包装的 React.Component,E(Enhanced Component) 指返回的新的高阶 React 组件。

定义中的『包装』一词故意被定义的比较模糊,因为它可以指两件事情:

  1. 属性代理(Props Proxy):高阶组件操控传递给 WrappedComponent 的 props,
  2. 反向继承(Inheritance Inversion):高阶组件继承(extends)WrappedComponent。

我们将讨论这两种形式的更多细节。

我可以使用高阶组件做什么呢?

概括的讲,高阶组件允许你做:

  • 代码复用,逻辑抽象,抽离底层准备(bootstrap)代码
  • 渲染劫持
  • State 抽象和更改
  • Props 更改

history和hash路由的区别

history和hash路由的区别主要在于它们的工作原理、URL结构、浏览器兼容性以及与后端交互的方式。以下是详细介绍:

1.URL结构:hash路由在URL中包含“#”,而history路由的URL中不包含“#”。
2.浏览器兼容性:hash路由支持较老版本的浏览器,而history路由是HTML5中新增的API,因此对浏览器的要求更高。
3.与后端交互的方式:hash路由不会将“#”后的部分发送到服务器,因此对后端没有影响,改变hash值不会导致页面重新加载。而history路由需要后端配合,如果后端不配合,刷新页面可能会出现404错误。
4.工作原理:hash路由通过监听“hashchange”事件来更新路由,而history路由通过使用HTML5的“pushState”和“replaceState”方法来修改浏览器历史记录栈中的URL,但这些修改不会立即发送到服务器。
5.URL显示:hash路由在地址栏中显示为带有“#”的完整URL,而history路由显示为没有“#”的更精简的URL

前端SPA(Single Page Application)的原理

前端SPA(Single Page Application)的原理主要涉及到以下几个方面:

单页面设计:SPA采用单页面设计,这意味着整个应用的所有功能都在同一个网页上展现,不需要频繁地打开和关闭不同的页面。这种设计方式使得应用的导航更加简单,用户体验更佳。

异步加载:SPA通过异步加载技术如Ajax来实现,这样可以避免每次操作都要完全加载新的页面。当用户在应用中进行某些操作时,只需要更新部分页面内容,而不是整个页面。这样不仅加快了响应速度,也减少了数据传输的开销。

历史记录和Hash:SPA还利用浏览器的历史记录功能和Hash值来实现在不刷新页面的情况下改变界面的行为。这允许用户在不离开当前页面的情况下执行多个操作,从而提高了用户体验。

动态内容生成:在SPA中,通常使用JavaScript来动态生成和修改页面内容。这种方式可以根据用户的操作动态调整显示的信息,使得应用能够实时反映用户的输入。

模块化架构:SPA往往具有模块化的架构,这意味着不同功能的组件可以独立开发和维护,然后在需要时组合在一起。这样的结构有助于提高开发的效率和质量。

服务端渲染:有些SPA可能会采用服务端渲染的方式,即将初始页面和服务端的逻辑结合起来,以减少客户端的处理负担。这种方式可以在服务器端完成一些复杂的计算和数据处理工作,然后再将结果传递给浏览器。

综上所述,前端SPA的核心原理在于结合了单页面设计和异步加载技术,以及动态内容的生成和使用,同时保持了良好的用户体验和高性能的特点。

WebSocket 相关面试题

1. 什么是 WebSocket?与 HTTP 有什么区别?

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,使得客户端和服务器之间的数据交换变得更加简单。

WebSocket vs HTTP 对比:

特性WebSocketHTTP
连接方式持久连接无状态、短连接
通信方向双向通信(全双工)单向请求 - 响应
数据传输二进制帧文本/二进制
头部开销小(握手后仅 2-10 字节)大(每次请求都有完整 header)
实时性高,服务器可主动推送低,需轮询
兼容性需要浏览器支持所有浏览器支持

适用场景:

  • WebSocket:实时聊天、在线游戏、股票行情、协同编辑
  • HTTP:普通网页请求、RESTful API、文件上传下载

2. WebSocket 的连接过程是怎样的?

连接建立流程:

// 1. 创建 WebSocket 实例
const ws = new WebSocket('ws://example.com/socket')

// 2. WebSocket 握手(HTTP Upgrade)
// 客户端发送:
GET /socket HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

// 服务器响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

// 3. 连接建立,进入 WebSocket 通信阶段

生命周期事件:

const ws = new WebSocket('ws://localhost:8080')

// 连接打开
ws.onopen = () => {
console.log('连接已建立')
ws.send('Hello Server!') // 发送消息
}

// 接收消息
ws.onmessage = (event) => {
console.log('收到消息:', event.data)
}

// 连接关闭
ws.onclose = (event) => {
console.log('连接已关闭', event.code, event.reason)
}

// 发生错误
ws.onerror = (error) => {
console.error('发生错误:', error)
}

3. WebSocket 如何实现心跳检测?

心跳检测目的:

  • 保持连接活跃,防止被防火墙或代理服务器断开
  • 检测连接是否仍然有效
  • 及时重连断开的连接

实现方案:

class WebSocketClient {
constructor(url) {
this.url = url
this.ws = null
this.pingInterval = null
this.pongTimeout = null
this.HEARTBEAT_INTERVAL = 30000 // 30 秒
this.PONG_TIMEOUT = 5000 // 5 秒超时
}

connect() {
this.ws = new WebSocket(this.url)

this.ws.onopen = () => {
console.log('连接成功')
this.startHeartbeat()
}

this.ws.onmessage = (event) => {
// 收到 pong 响应,清除超时定时器
if (event.data === 'pong') {
clearTimeout(this.pongTimeout)
} else {
// 处理其他消息
this.handleMessage(event.data)
}
}

this.ws.onclose = () => {
this.stopHeartbeat()
// 自动重连
setTimeout(() => this.connect(), 3000)
}

this.ws.onerror = (error) => {
console.error('连接错误', error)
this.ws.close()
}
}

// 发送心跳
startHeartbeat() {
this.pingInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send('ping')

// 设置 pong 超时,如果超时未收到 pong,认为连接断开
this.pongTimeout = setTimeout(() => {
console.warn('心跳超时,关闭连接')
this.ws.close()
}, this.PONG_TIMEOUT)
}
}, this.HEARTBEAT_INTERVAL)
}

stopHeartbeat() {
clearInterval(this.pingInterval)
clearTimeout(this.pongTimeout)
}

sendMessage(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data))
} else {
console.warn('连接未打开')
}
}

handleMessage(data) {
// 处理业务逻辑
console.log('收到消息:', data)
}
}

// 使用
const client = new WebSocketClient('ws://localhost:8080')
client.connect()

4. WebSocket 断线重连如何实现?

指数退避重连策略:

class WebSocketWithReconnect {
constructor(url, options = {}) {
this.url = url
this.reconnectInterval = options.reconnectInterval || 1000
this.maxReconnectInterval = options.maxReconnectInterval || 30000
this.reconnectAttempts = 0
this.ws = null
this.manualClose = false
}

connect() {
this.manualClose = false
this.ws = new WebSocket(this.url)

this.ws.onopen = () => {
console.log('连接成功')
this.reconnectAttempts = 0 // 重置重连次数
}

this.ws.onclose = (event) => {
console.log('连接关闭', event.code, event.reason)

// 如果不是手动关闭,则尝试重连
if (!this.manualClose) {
this.scheduleReconnect()
}
}

this.ws.onerror = (error) => {
console.error('连接错误', error)
// 错误时也会触发 onclose,不需要在这里重连
}
}

// 调度重连
scheduleReconnect() {
// 指数退避:1s, 2s, 4s, 8s, 16s, 30s...
const delay = Math.min(
this.reconnectInterval * Math.pow(2, this.reconnectAttempts),
this.maxReconnectInterval
)

console.log(`将在 ${delay}ms 后重连 (${this.reconnectAttempts + 1})`)

setTimeout(() => {
this.reconnectAttempts++
this.connect()
}, delay)
}

// 手动关闭
close() {
this.manualClose = true
if (this.ws) {
this.ws.close()
}
}

send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data)
} else {
console.warn('连接不可用')
}
}
}

// 使用
const ws = new WebSocketWithReconnect('ws://localhost:8080', {
reconnectInterval: 1000,
maxReconnectInterval: 30000
})
ws.connect()

5. WebSocket 和 Socket.IO 有什么区别?

Socket.IO 是一个基于 WebSocket 的库,提供了额外的功能。

特性WebSocketSocket.IO
协议标准协议自定义协议(基于 WebSocket)
降级支持❌ 无✅ 支持轮询降级
房间/命名空间❌ 需自己实现✅ 内置支持
自动重连❌ 需自己实现✅ 内置支持
二进制支持✅ 支持✅ 支持
ACK 机制❌ 无✅ 支持回调确认
体积小(原生 API)较大(~30KB gzipped)
性能更高略低(有额外封装)

Socket.IO 示例:

// 客户端
import io from 'socket.io-client'

const socket = io('http://localhost:3000', {
transports: ['websocket', 'polling'], // 优先 WebSocket,失败则轮询
reconnection: true, // 自动重连
reconnectionAttempts: 5,
timeout: 10000
})

// 加入房间
socket.emit('join-room', 'room1')

// 发送消息带回调
socket.emit('message', { text: 'Hello' }, (ack) => {
console.log('服务器已确认', ack)
})

// 接收消息
socket.on('message', (data) => {
console.log('收到消息:', data)
})

// 命名空间
const adminSocket = io('/admin')

// 服务端(Node.js)
const io = require('socket.io')(server)

io.on('connection', (socket) => {
console.log('用户连接:', socket.id)

socket.join('room1') // 加入房间

socket.on('message', (data, callback) => {
// 广播给其他人
socket.broadcast.emit('message', data)

// 发送确认
callback({ status: 'ok' })
})

socket.on('disconnect', () => {
console.log('用户断开连接')
})
})

选择建议:

  • 简单实时应用:使用原生 WebSocket
  • 复杂应用(需要房间、重连、降级):使用 Socket.IO
  • 追求极致性能:使用原生 WebSocket

6. WebSocket 的安全问题有哪些?

常见安全问题:

  1. XSS 攻击
// ❌ 危险:直接发送用户输入
ws.send(userInput)

// ✅ 安全:先转义
function escapeHtml(str) {
return str.replace(/[&<>"']/g, (char) => {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]
})
}
ws.send(escapeHtml(userInput))
  1. CSRF 攻击
// ✅ 使用 Token 验证
const token = localStorage.getItem('authToken')
const ws = new WebSocket(`ws://example.com/socket?token=${token}`)

// 服务端验证 token 后才允许连接
  1. 数据验证
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)

// 验证数据结构
if (!data.type || !data.payload) {
throw new Error('无效数据')
}

handleMessage(data)
} catch (error) {
console.error('数据解析失败', error)
}
}
  1. 使用 WSS 加密
// ❌ 不加密
const ws = new WebSocket('ws://example.com/socket')

// ✅ 使用加密连接
const ws = new WebSocket('wss://example.com/socket')
  1. 频率限制
class RateLimiter {
constructor(maxRequests, interval) {
this.maxRequests = maxRequests
this.interval = interval
this.requests = []
}

canSend() {
const now = Date.now()
this.requests = this.requests.filter(time => now - time < this.interval)

if (this.requests.length < this.maxRequests) {
this.requests.push(now)
return true
}
return false
}
}

const limiter = new RateLimiter(10, 1000) // 每秒最多 10 条

function sendMessage(data) {
if (limiter.canSend()) {
ws.send(JSON.stringify(data))
} else {
console.warn('发送频率过高')
}
}

7. WebSocket 在实际项目中的应用场景?

场景 1:实时聊天室

class ChatClient {
constructor(userId) {
this.userId = userId
this.ws = null
this.messageQueue = []
}

connect() {
this.ws = new WebSocket(`wss://chat.example.com?userId=${this.userId}`)

this.ws.onopen = () => {
console.log('连接成功')
// 发送队列中的消息
this.messageQueue.forEach(msg => this.ws.send(JSON.stringify(msg)))
this.messageQueue = []
}

this.ws.onmessage = (event) => {
const message = JSON.parse(event.data)

switch (message.type) {
case 'chat':
this.displayMessage(message.data)
break
case 'user-join':
this.notifyUserJoin(message.data)
break
case 'user-leave':
this.notifyUserLeave(message.data)
break
}
}
}

sendMessage(content) {
const message = {
type: 'chat',
data: {
userId: this.userId,
content,
timestamp: Date.now()
}
}

if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message))
} else {
this.messageQueue.push(message)
}
}

displayMessage(data) {
// 显示消息到 UI
console.log(`${data.userId}: ${data.content}`)
}
}

场景 2:股票行情实时更新

class StockTicker {
constructor(symbols) {
this.symbols = symbols
this.ws = null
this.subscribers = new Map()
}

connect() {
this.ws = new WebSocket('wss://ticker.example.com')

this.ws.onopen = () => {
// 订阅股票
this.ws.send(JSON.stringify({
action: 'subscribe',
symbols: this.symbols
}))
}

this.ws.onmessage = (event) => {
const ticker = JSON.parse(event.data)
this.notifySubscribers(ticker.symbol, ticker)
}
}

subscribe(symbol, callback) {
if (!this.subscribers.has(symbol)) {
this.subscribers.set(symbol, [])
}
this.subscribers.get(symbol).push(callback)
}

notifySubscribers(symbol, data) {
const callbacks = this.subscribers.get(symbol) || []
callbacks.forEach(cb => cb(data))
}
}

// 使用
const ticker = new StockTicker(['AAPL', 'GOOGL', 'MSFT'])
ticker.connect()

ticker.subscribe('AAPL', (data) => {
console.log(`AAPL 当前价格:$${data.price}`)
})

场景 3:在线协作编辑

class CollaborativeEditor {
constructor(docId, userId) {
this.docId = docId
this.userId = userId
this.ws = null
this.editor = null // CodeMirror/Monaco Editor 实例
}

connect() {
this.ws = new WebSocket(`wss://collab.example.com/doc/${this.docId}`)

this.ws.onopen = () => {
// 加入文档
this.ws.send(JSON.stringify({
type: 'join',
userId: this.userId
}))
}

this.ws.onmessage = (event) => {
const operation = JSON.parse(event.data)

if (operation.type === 'operation') {
// 应用远程操作
this.applyRemoteOperation(operation)
} else if (operation.type === 'cursor') {
// 更新其他用户的游标位置
this.updateRemoteCursor(operation.userId, operation.position)
}
}

// 监听本地编辑器变化
this.editor.on('change', (change) => {
this.sendOperation(change)
})
}

sendOperation(change) {
this.ws.send(JSON.stringify({
type: 'operation',
userId: this.userId,
change: change,
timestamp: Date.now()
}))
}

applyRemoteOperation(operation) {
// 使用 OT 算法或 CRDT 算法处理冲突
// 这里简化处理
if (operation.userId !== this.userId) {
this.editor.applyChange(operation.change)
}
}
}

8. WebSocket 的性能优化方法?

1. 消息压缩

// 使用 pako 进行 gzip 压缩
import pako from 'pako'

function sendCompressed(data) {
const compressed = pako.gzip(JSON.stringify(data))
ws.send(compressed) // WebSocket 支持发送 ArrayBuffer
}

ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const decompressed = pako.inflate(event.data, { to: 'string' })
const data = JSON.parse(decompressed)
handleMessage(data)
}
}

2. 消息合并(批量发送)

class BatchSender {
constructor(ws, interval = 100) {
this.ws = ws
this.interval = interval
this.queue = []
this.timer = null
}

send(data) {
this.queue.push(data)

if (!this.timer) {
this.timer = setTimeout(() => {
this.flush()
}, this.interval)
}
}

flush() {
if (this.queue.length > 0) {
const batch = {
type: 'batch',
data: this.queue,
timestamp: Date.now()
}
this.ws.send(JSON.stringify(batch))
this.queue = []
}
this.timer = null
}
}

3. 二进制协议

// 使用 Protocol Buffers 或 MessagePack
import * as msgpack from 'msgpack-lite'

// 编码
const encoded = msgpack.encode({ type: 'message', data: 'hello' })
ws.send(encoded) // 发送 Buffer

// 解码
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const decoded = msgpack.decode(new Uint8Array(event.data))
handleMessage(decoded)
}
}

4. 连接复用

// 单例模式,避免重复创建连接
class WebSocketManager {
static instance = null

static getInstance(url) {
if (!WebSocketManager.instance) {
WebSocketManager.instance = new WebSocket(url)
}
return WebSocketManager.instance
}
}

// 多个模块共用一个连接
const ws = WebSocketManager.getInstance('wss://example.com')

http 常见的状态码

1xx

代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束

常见的有:

  • 100(客户端继续发送请求,这是临时响应):这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应
  • 101:服务器根据客户端的请求切换协议,主要用于websocket或http2升级

2xx

代表请求已成功被服务器接收、理解、并接受

常见的有:

  • 200(成功):请求已成功,请求所希望的响应头或数据体将随此响应返回
  • 201(已创建):请求成功并且服务器创建了新的资源
  • 202(已创建):服务器已经接收请求,但尚未处理
  • 203(非授权信息):服务器已成功处理请求,但返回的信息可能来自另一来源
  • 204(无内容):服务器成功处理请求,但没有返回任何内容
  • 205(重置内容):服务器成功处理请求,但没有返回任何内容
  • 206(部分内容):服务器成功处理了部分请求

3xx

表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向

常见的有:

  • 300(多种选择):针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择
  • 301(永久移动):请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置
  • 302(临时移动): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
  • 303(查看其他位置):请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码
  • 305 (使用代理): 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理
  • 307 (临时重定向): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求

4xx

代表了客户端看起来可能发生了错误,妨碍了服务器的处理

常见的有:

  • 400(错误请求): 服务器不理解请求的语法
  • 401(未授权): 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
  • 403(禁止): 服务器拒绝请求
  • 404(未找到): 服务器找不到请求的网页
  • 405(方法禁用): 禁用请求中指定的方法
  • 406(不接受): 无法使用请求的内容特性响应请求的网页
  • 407(需要代理授权): 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理
  • 408(请求超时): 服务器等候请求时发生超时

5xx

表示服务器无法完成明显有效的请求。这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生

常见的有:

  • 500(服务器内部错误):服务器遇到错误,无法完成请求
  • 501(尚未实施):服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码
  • 502(错误网关): 服务器作为网关或代理,从上游服务器收到无效响应
  • 503(服务不可用): 服务器目前无法使用(由于超载或停机维护)
  • 504(网关超时): 服务器作为网关或代理,但是没有及时从上游服务器收到请求
  • 505(HTTP 版本不受支持): 服务器不支持请求中所用的 HTTP 协议版本

举例说明

下面给出一些状态码的适用场景:

  • 100:客户端在发送POST数据给服务器前,征询服务器情况,看服务器是否处理POST的数据,如果不处理,客户端则不上传POST数据,如果处理,则POST上传数据。常用于POST大数据传输
  • 206:一般用来做断点续传,或者是视频文件等大文件的加载
  • 301:永久重定向会缓存。新域名替换旧域名,旧的域名不再使用时,用户访问旧域名时用301就重定向到新的域名
  • 302:临时重定向不会缓存,常用 于未登陆的用户访问用户中心重定向到登录页面
  • 304:协商缓存,告诉客户端有缓存,直接使用缓存中的数据,返回页面的只有头部信息,是没有内容部分
  • 400:参数有误,请求无法被服务器识别
  • 403:告诉客户端进制访问该站点或者资源,如在外网环境下,然后访问只有内网IP才能访问的时候则返回
  • 404:服务器找不到资源时,或者服务器拒绝请求又不想说明理由时
  • 503:服务器停机维护时,主动用503响应请求或 nginx 设置限速,超过限速,会返回503
  • 504:网关超时

js的宏任务和微任务

在JavaScript中,任务被分为宏任务微任务两种类型。

宏任务通常包括以下几类:

  1. 事件回调函数,例如clickloadajax等。
  2. setTimeoutsetInterval定时器。
  3. requestAnimationFrame动画操作,例如文件读写、网络通信等。
  4. 整体代码script
  5. postMessageMessageChannel、setImmediate`(在Node.js环境中)。 宏任务在Event Loop中的表现为一个个的Task,Event Loop会依次从宏任务队列中取出Task执行,直到队列为空

微任务通常包括以下几类:

  1. Promise的回调函数。

  2. MutationObserver的回调函数。

  3. process.nextTick(在Node.js环境中)

微任务在Event Loop中的表现为一个个的Job,它们会在当前宏任务执行完成后立即执行。也就是说,当一个宏任务执行完成后,如果在它的执行期间产生了微任务,那么这些微任务会被立即执行,而不是等待下一个宏任务执行。在同一个宏任务中,微任务的执行顺序是优先于宏任务的,也就是说,当宏任务执行到一半时,如果产生了微任务,那么这些微任务会先执行完毕,然后再继续执行剩余的宏任务。

执行顺序是先执行一个宏任务,把微任务放在一个队列里执行完成后,执行下一个宏任务,查看是否有需要执行的宏任务依次执行,以此循环直到所有任务执行完毕。

设计原则和设计模式

设计原则

单一职责原则(SRP)

一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

应该把对象或方法划分成较小的粒度

最少知识原则(LKP)

一个软件实体应当 尽可能少地与其他实体发生相互作用

应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的 相互联系,可以转交给第三方进行处理

开放-封闭原则(OCP)

软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定

什么是设计模式?

假设有一个空房间,我们要日复一日地往里 面放一些东西。最简单的办法当然是把这些东西 直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东西的位置也不容易。所以在房间里做一些柜子也许是个更好的选择,虽然柜子会增加我们的成本,但它可以在维护阶段为我们带来好处。使用这些柜子存放东西的规则,或许就是一种模式

学习设计模式,有助于写出可复用和可维护性高的程序

设计模式的原则是“找出程序中变化的地方,并将变化封装起来”,它的关键是意图,而不是结构。

不过要注意,使用不当的话,可能会事倍功半。

一、单例模式

  1. 定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点

  1. 核心

确保只有一个实例,并提供全局访问

  1. 实现

假设要设置一个管理员,多次调用也仅设置一次,我们可以使用闭包缓存一个内部变量来实现这个单例

二、策略模式

  1. 定义

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

  1. 核心

将算法的使用和算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成:

第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。

第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明Context中要维持对某个策略对象的引用

  1. 实现

策略模式可以用于组合一系列算法,也可用于组合一系列业务规则

假设需要通过成绩等级来计算学生的最终得分,每个成绩等级有对应的加权值。我们可以利用对象字面量的形式直接定义这个组策略

  1. 优缺点

优点

可以有效地避免多重条件语句,将一系列方法封装起来也更直观,利于维护

缺点

往往策略集会比较多,我们需要事先就了解定义好所有的情况

三、代理模式

  1. 定义

为一个对象提供一个代用品或占位符,以便控制对它的访问

  1. 核心

当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。

替身对象对请求做出一些处理之后,再把请求转交给本体对象

代理和本体的接口具有一致性,本体定义了关键功能,而代理是提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情

  1. 实现

代理模式主要有三种:保护代理、虚拟代理、缓存代理

保护代理主要实现了访问主体的限制行为,以过滤字符作为简单的例子

四、迭代器模式

  1. 定义

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

  1. 核心

在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素

  1. 实现

JS中数组的map forEach 已经内置了迭代器

五、发布—订阅模式

  1. 定义

也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知

  1. 核心

取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅

  1. 实现

JS中的事件就是经典的发布-订阅模式的实现

  1. 优缺点

优点

一为时间上的解耦,二为对象之间的解耦。可以用在异步编程中与MVC框架中

缺点

创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销

弱化了对象之间的联系,复杂的情况下可能会导致程序难以跟踪维护和理解

六、命令模式

  1. 定义

用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系

命令(command)指的是一个执行某些特定事情的指令

  1. 核心

命令中带有execute执行、undo撤销、redo重做等相关命令方法,建议显示地指示这些方法名

  1. 实现

简单的命令模式实现可以直接使用对象字面量的形式定义一个命令

七、组合模式

  1. 定义

是用小的子对象来构建更大的 对象,而这些小的子对象本身也许是由更小 的“孙对象”构成的。

  1. 核心

可以用树形结构来表示这种“部分-整体”的层次结构。

调用组合对象的execute方法,程序会递归调用组合对象下面的叶对象的execute方法

20200917170826106

但要注意的是,组合模式不是父子关系,它是一种HAS-A(聚合)的关系,将请求委托给它所包含的所有叶对象。基于这种委托,就需要保证组合对象和叶对象拥有相同的接口

此外,也要保证用一致的方式对待列表中的每个叶对象,即叶对象属于同一类,不需要过多特殊的额外操作

  1. 实现

使用组合模式来实现扫描文件夹中的文件

  1. 优缺点

优点

可以方便地构造一棵树来表示对象的部分-整体结构。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一一致的操作。

缺点

创建出来的对象长得都差不多,可能会使代码不好理解,创建太多的对象对性能也会有一些影响

八、模板方法模式

  1. 定义

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。

  1. 核心

在抽象父类中封装子类的算法框架,它的 init方法可作为一个算法的模板,指导子类以何种顺序去执行哪些方法。

由父类分离出公共部分,要求子类重写某些父类的(易变化的)抽象方法

  1. 实现

模板方法模式一般的实现方式为继承

以运动作为例子,运动有比较通用的一些处理,这部分可以抽离开来,在父类中实现。具体某项运动的特殊性则有自类来重写实现。

最终子类直接调用父类的模板函数来执行

九、享元模式

十、职责链模式

十一、中介者模式

十二、装饰者模式

  1. 定义

以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。

是一种“即用即付”的方式,能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责 2. 核心

是为对象动态加入行为,经过多重包装,可以形成一条装饰链

  1. 实现

最简单的装饰者,就是重写对象的属性

十三、状态模式

十四、适配器模式

  1. 定义

是解决两个软件实体间的接口不兼容的问题,对不兼容的部分进行适配

  1. 核心

解决两个已有接口之间不匹配的问题

  1. 实现

比如一个简单的数据格式转换的适配器

十五、外观模式

  1. 定义

为子系统中的一组接口提供一个一致的界面,定义一个高层接口,这个接口使子系统更加容易使用 2. 核心

可以通过请求外观接口来达到访问子系统,也可以选择越过外观来直接访问子系统

  1. 实现

外观模式在JS中,可以认为是一组函数的集合

react合成事件的原理

React合成事件的原理基本上可以分为三个主要步骤:

  1. 事件绑定:当你在React组件中定义一个事件处理函数时,比如onClick或者onChange,React会将这些事件处理函数绑定到DOM元素上。这通常是在组件挂载阶段完成的。
  2. 事件委托:React利用事件委托的方式来处理这些事件。它会在最外层的DOM元素上注册事件监听器,然后根据事件的冒泡机制,在DOM树中逐级查找触发事件的元素和对应的事件处理函数。这样就避免了在每个子元素上都注册事件监听器的开销。
  3. 合成事件对象:当事件发生时,React会创建一个合成事件对象,这个对象包含了该事件的所有信息,比如事件类型、目标元素、鼠标位置等等。这个合成事件对象会被传递给你定义的事件处理函数。这使得你可以在事件处理函数中访问到所有与事件相关的信息,而不必担心浏览器之间的差异。

简而言之,React合成事件的原理就是利用事件委托和合成事件对象来统一处理各种浏览器事件,提供了一种更加方便、高效和一致的方式来处理用户交互。

react 路由版本差异的区别

React 路由版本之间的主要差异主要涉及到 API 的变化、功能的增强以及性能的优化。以下是一些常见版本之间的区别:

  1. React Router v4 和 v5
    • 从 v4 到 v5,最显著的变化之一是 API 的统一性。v5 引入了新的 <Router> 组件,代替了之前的 <BrowserRouter><HashRouter><MemoryRouter>。这样做的目的是为了让代码更一致、更易于理解。
    • 另一个重要的变化是在 history 包的更新。v5 引入了新的 history 版本,解决了一些在 v4 中存在的问题,并提供了更好的浏览器支持。
  2. React Router v5 和 v6
    • 从 v5 到 v6,最显著的变化之一是引入了新的 <Routes><Route> 组件。这些新组件取代了之前的 <Switch><Route>,使得路由配置更加清晰和简洁。
    • 另一个重要的变化是在路由匹配和嵌套路由方面的改进。v6 提供了更灵活的路由匹配规则,并改进了嵌套路由的处理方式。
  3. 功能增强
    • 每个新版本都引入了一些新的功能和改进,比如针对动态路由的更好支持、更灵活的路由配置、更好的导航控制等等。
  4. 性能优化
    • 随着版本的更新,React Router 也在不断优化性能,比如减少了不必要的重新渲染、更高效的路由匹配算法等。

总的来说,React 路由版本差异的主要区别在于API的改变、功能的增强和性能的优化。每个新版本都致力于提供更好的开发体验和更清晰的路由管理方式。

vue2和vue3的区别,以及跟react的区别

Vue 2 和 Vue 3 以及 React 之间的主要区别包括 API 设计、性能优化、响应式系统等方面:

Vue 2 和 Vue 3 的区别:

  1. 响应式系统的改进
    • Vue 3 中的响应式系统经过了重构和优化,使用了基于 Proxy 的实现,相比 Vue 2 的基于 Object.defineProperty 的实现,具有更高的性能和更好的类型推断。
    • Vue 3 的响应式系统支持嵌套数组的监听,对于动态添加属性和删除属性也更加友好。
  2. Composition API
    • Vue 3 引入了 Composition API,这是一种基于函数的 API 设计,使得组件内部逻辑更加清晰、可复用性更强。这种 API 设计更加灵活,有助于更好地组织代码。
    • Composition API 使得组件的逻辑可以按照功能进行组织,而不是像 Vue 2 中的 Options API 那样按照生命周期钩子。
  3. 性能优化
    • Vue 3 在渲染性能上有很大的提升,引入了虚拟 DOM 的静态提升(Static Hoisting)和事件侦听器的缓存等优化。
    • 新的编译器将模板编译成更小、更快的代码。
  4. Tree-Shaking
    • Vue 3 的代码更容易进行 Tree-Shaking,减少了打包后的体积,提高了应用的性能。
  5. 更好的 TypeScript 支持
    • Vue 3 对 TypeScript 的支持更加友好,具有更好的类型推断和类型提示。

Vue 3 和 React 的区别:

  1. 模板 vs JSX
    • Vue 使用模板语法,可以更快地创建简单的视图结构,而 React 使用 JSX,它更接近 JavaScript 本身,使得组件和逻辑更紧密地结合在一起。
  2. 组件 API
    • Vue 3 的 Composition API 和 React Hooks 在设计上有一些相似之处,但语法和使用方式略有不同。
    • Vue 3 的 Composition API 是可选的,而 React Hooks 是 React 中唯一的 API。
  3. 响应式系统
    • Vue 的响应式系统是自带的,不需要额外的库,而 React 需要使用 useStateuseEffect 等钩子来实现组件的状态管理和副作用。
  4. 生态系统
    • React 生态系统非常庞大,有大量的第三方库和组件可供选择。
    • Vue 生态系统也在不断壮大,但相比 React 还是稍显不足。
  5. 学习曲线
    • Vue 的学习曲线相对较低,特别适合初学者入门。
    • React 在使用 JSX 和 Hooks 时可能需要一些时间适应,但这也使得它更加灵活和强大。

总的来说,Vue 3 在性能、响应式系统和开发体验上有很多优势,特别是引入了 Composition API 和对 TypeScript 的更好支持。而与 React 相比,Vue 的语法更加简洁、易懂,适合快速开发中小型应用;React 则更加灵活、可扩展,适合大型应用和更复杂的逻辑。选择 Vue 还是 React 主要取决于项目需求、团队经验和个人喜好。

redux 的面试题

https://www.cnblogs.com/gqx-html/p/17368876.html

vuex常见面试题

https://blog.csdn.net/weixin_52834435/article/details/124522483

Vuex 和 Pinia 的区别

Vuex(Vue2/Vue3)

Vuex 是 Vue 的官方状态管理库,采用集中式存储模式。

核心概念:

  • state:存储应用的状态
  • getters:从 state 中派生出一些状态(类似 computed)
  • mutations:更改 state 的唯一方法(同步)
  • actions:提交 mutation,可包含异步操作
  • modules:将 store 分割成模块

使用方法:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 0,
userInfo: null
},
getters: {
doubleCount: state => state.count * 2
},
mutations: {
increment(state, payload) {
state.count += payload
},
setUserInfo(state, info) {
state.userInfo = info
}
},
actions: {
async fetchUserInfo({ commit }, userId) {
const res = await fetch(`/api/user/${userId}`)
const data = await res.json()
commit('setUserInfo', data)
}
}
})

// main.js
import store from './store'
new Vue({
store,
render: h => h(App)
}).$mount('#app')

// 组件中使用
export default {
methods: {
add() {
this.$store.commit('increment', 1)
},
loadUser() {
this.$store.dispatch('fetchUserInfo', 123)
}
},
computed: {
count() {
return this.$store.state.count
},
doubleCount() {
return this.$store.getters.doubleCount
}
}
}

Pinia(Vue3 推荐)

Pinia 是 Vue 3 的官方状态管理库,更简洁、类型友好。

核心特点:

  • 没有 mutations,只有 state、getters、actions
  • 支持 Composition API 和 Options API
  • 完整的 TypeScript 支持
  • 体积更小(约 1KB)
  • 更好的 DevTools 集成

使用方法:

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Evan'
}),
getters: {
doubleCount: (state) => state.count * 2,
upperName: (state) => state.name.toUpperCase()
},
actions: {
increment(payload = 1) {
this.count += payload
},
async fetchData() {
const res = await fetch('/api/data')
const data = await res.json()
this.count = data.count
}
}
})

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')

// 组件中使用(Composition API)
<script setup>
import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'

const counterStore = useCounterStore()

const doubleCount = computed(() => counterStore.doubleCount)

const add = () => {
counterStore.increment(1)
}

const loadData = async () => {
await counterStore.fetchData()
}
</script>

// 组件中使用(Options API)
export default {
methods: {
add() {
const store = useCounterStore()
store.increment(1)
}
},
computed: {
count() {
const store = useCounterStore()
return store.count
}
}
}

Vuex vs Pinia 对比

特性VuexPinia
适用版本Vue 2/Vue 3Vue 3
核心概念state/getters/mutations/actions/modulesstate/getters/actions
Mutations必需已移除
TypeScript 支持一般完善
包大小~2.2KB~1KB
模块化modules 嵌套扁平化设计
DevTools支持更好的集成
代码量较多更简洁

使用建议:

  • 新项目(Vue 3):优先选择 Pinia
  • 老项目(Vue 2):继续使用 Vuex
  • 需要迁移:Pinia 提供了 Vuex 迁移指南

React 状态管理:Redux、MobX、Zustand 对比

Redux

Redux 是 React 最常用的状态管理库,遵循单向数据流。

核心原则:

  • 单一数据源(Single Source of Truth)
  • State 只读,只能通过 action 修改
  • 使用纯 reducer 函数处理状态变更

使用方法(Redux Toolkit - 推荐):

// store/counterSlice.js
import { createSlice, configureStore } from '@reduxjs/toolkit'

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0, user: null },
reducers: {
increment: (state, action) => {
state.value += action.payload
},
decrement: (state, action) => {
state.value -= action.payload
},
setUser: (state, action) => {
state.user = action.payload
}
},
extraReducers: (builder) => {
builder.addCase('user/fetch/fulfilled', (state, action) => {
state.user = action.payload
})
}
})

export const { increment, decrement, setUser } = counterSlice.actions

export const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
})

// index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './store/store'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)

// 组件中使用
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from '../store/counterSlice'

function Counter() {
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()

return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment(1))}>+</button>
<button onClick={() => dispatch(decrement(1))}>-</button>
</div>
)
}

// 异步 action(配合 Redux Thunk)
export const fetchUser = (userId) => async (dispatch) => {
const res = await fetch(`/api/user/${userId}`)
const data = await res.json()
dispatch(setUser(data))
}

优点:

  • 可预测的状态变化
  • 强大的 DevTools
  • 中间件生态丰富
  • 适合大型复杂应用

缺点:

  • 样板代码多(Redux Toolkit 已改善)
  • 学习曲线较陡
  • 配置相对复杂

MobX

MobX 是基于响应式编程的状态管理库,更简单灵活。

核心概念:

  • observable:可观察的状态
  • action:修改状态的方法
  • computed:计算属性
  • observer:使组件响应状态变化

使用方法:

// stores/counterStore.js
import { makeAutoObservable, runInAction } from 'mobx'

class CounterStore {
count = 0
user = null

constructor() {
makeAutoObservable(this)
}

get doubleCount() {
return this.count * 2
}

increment = (payload = 1) => {
this.count += payload
}

decrement = (payload = 1) => {
this.count -= payload
}

async fetchUser(userId) {
const res = await fetch(`/api/user/${userId}`)
const data = await res.json()
runInAction(() => {
this.user = data
})
}
}

export const counterStore = new CounterStore()

// 或者使用 makeAutoObservable 简化
import { makeAutoObservable } from 'mobx'

function createCounterStore() {
return makeAutoObservable({
count: 0,
user: null,
get doubleCount() {
return this.count * 2
},
increment(payload = 1) {
this.count += payload
},
async fetchUser(userId) {
const res = await fetch(`/api/user/${userId}`)
const data = await res.json()
this.user = data
}
})
}

export const counterStore = createCounterStore()

// 组件中使用
import React from 'react'
import { observer } from 'mobx-react-lite'
import { counterStore } from '../stores/counterStore'

const Counter = observer(() => {
return (
<div>
<p>Count: {counterStore.count}</p>
<p>Double: {counterStore.doubleCount}</p>
<button onClick={() => counterStore.increment(1)}>+</button>
<button onClick={() => counterStore.decrement(1)}>-</button>
<button onClick={() => counterStore.fetchUser(123)}>Load User</button>
</div>
)
})

export default Counter

// 使用 Provider(可选)
import { Provider } from 'mobx-react-lite'

function App() {
return (
<Provider counterStore={counterStore}>
<Counter />
</Provider>
)
}

优点:

  • 代码简洁,样板代码少
  • 响应式更新,自动追踪依赖
  • 学习曲线平缓
  • 面向对象风格

缺点:

  • "魔法"较多,不够透明
  • 调试相对困难
  • 社区规模小于 Redux

Zustand

Zustand 是轻量级状态管理库,API 简洁,无样板代码。

核心特点:

  • 极简 API(~1KB)
  • 无需 Provider
  • 支持中间件
  • 兼容 React 18 并发模式

使用方法:

// stores/useCounterStore.js
import { create } from 'zustand'

export const useCounterStore = create((set, get) => ({
count: 0,
user: null,
doubleCount: () => get().count * 2,
increment: (payload = 1) => set((state) => ({ count: state.count + payload })),
decrement: (payload = 1) => set((state) => ({ count: state.count - payload })),
fetchUser: async (userId) => {
const res = await fetch(`/api/user/${userId}`)
const data = await res.json()
set({ user: data })
}
}))

// 组件中使用
import React from 'react'
import { useCounterStore } from '../stores/useCounterStore'

function Counter() {
const count = useCounterStore((state) => state.count)
const doubleCount = useCounterStore((state) => state.doubleCount())
const increment = useCounterStore((state) => state.increment)
const decrement = useCounterStore((state) => state.decrement)
const fetchUser = useCounterStore((state) => state.fetchUser)

// 或者直接使用整个 store(不推荐,可能导致不必要的渲染)
// const { count, increment, decrement } = useCounterStore()

return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => increment(1)}>+</button>
<button onClick={() => decrement(1)}>-</button>
<button onClick={() => fetchUser(123)}>Load User</button>
</div>
)
}

// 在组件外部使用
const count = useCounterStore.getState().count
useCounterStore.setState({ count: 10 })

// 使用中间件(持久化)
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export const useCounterStore = create(
persist(
(set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}),
{
name: 'counter-storage' // localStorage 中的 key
}
)
)

优点:

  • 极简 API,易于上手
  • 无需 Provider,使用灵活
  • 体积小,性能好
  • 支持中间件(持久化、immer 等)

缺点:

  • 相对年轻,生态不如 Redux
  • 大型项目经验较少
  • 社区资源相对有限

Redux vs MobX vs Zustand 对比

特性ReduxMobXZustand
学习曲线陡峭平缓极低
代码量较多中等极少
响应式手动订阅自动响应手动订阅
Provider必需可选不需要
TypeScript良好良好优秀
DevTools强大一般基础
包大小~2.5KB~16KB~1KB
适用场景大型复杂应用中小型应用轻中到中型应用
社区生态非常成熟成熟快速增长

选择建议

选择 Redux 的情况:

  • 大型复杂应用
  • 团队熟悉 Redux
  • 需要强大的 DevTools 和调试能力
  • 需要丰富的中间件生态

选择 MobX 的情况:

  • 偏好面向对象编程
  • 希望减少样板代码
  • 项目规模中等
  • 喜欢响应式编程风格

选择 Zustand 的情况:

  • 追求简洁和轻量
  • 快速开发原型
  • 中小型项目
  • 不想配置 Provider

综合推荐

  • 新手入门:Zustand(最简单)
  • 企业级应用:Redux Toolkit(最稳定)
  • 快速开发:Zustand 或 MobX
  • Vue 项目:Pinia(官方推荐)

React Diff 算法详解

什么是 Diff 算法

Diff 算法是虚拟 DOM 技术的核心,用于比较两棵虚拟 DOM 树的差异,找出最小更新路径,避免不必要的 DOM 操作。

时间复杂度:

  • 完全 Diff 算法:O(n³) - 不可接受
  • React/Vue 优化后:O(n) - 线性时间复杂度

React Diff 策略

React 基于两点假设实现 O(n) 复杂度:

  1. 不同组件产生不同树:如果根节点类型不同,直接销毁旧树,创建新树
  2. key 值唯一性:开发者通过 key 提示稳定性,相同 key 视为同一节点

1. Tree Diff 策略

// ❌ 类型不同,直接替换
<div> <h1>
<Child /> <Child />
</div> diff </h1>
// 直接销毁整个 div 树,创建 h1 树

// ✅ 类型相同,继续比较子节点
<div className="old"> <div className="new">
<p>Hello</p> diff <p>Hello</p>
</div> </div>
// 只更新 className 属性

2. Component Diff 策略

// 同类型组件,继续比较
<App> <App>
<Header /> diff <Header />
</App> </App>
// 比较 Header 组件的内部差异

// 不同类型组件,替换
<OldComponent> <NewComponent>
... diff ...
</OldComponent> </NewComponent>
// 销毁 OldComponent,创建 NewComponent

3. Element Diff 策略 (核心)

对同一层级的子节点进行循环比较:

const oldChildren = [...]; // 旧节点集合
const newChildren = [...]; // 新节点集合

// React 的循环比较逻辑
for (let i = 0; i < newChildren.length; i++) {
const oldNode = oldChildren[i];
const newNode = newChildren[i];

if (oldNode === newNode) {
continue; // 相同节点,跳过
} else if (isSameType(oldNode, newNode)) {
updateElement(oldNode, newNode); // 更新
} else {
replaceElement(oldNode, newNode); // 替换
}
}

三种操作:

  1. INSERT(插入):新增节点
  2. UPDATE(更新):节点类型相同,属性不同
  3. DELETE(删除):移除节点
  4. REORDER(移动):节点位置变化 (需要 key)

Key 的作用

// ❌ 不使用 key,按索引比较
<ul>
<li>A</li> // index 0
<li>B</li> // index 1
<li>C</li> // index 2
</ul>

// 删除第一个元素后
<ul>
<li>B</li> // index 0 - 被误认为 A,更新内容
<li>C</li> // index 1 - 被误认为 B,更新内容
<li></li> // index 2 - 被删除
// 导致不必要的 DOM 操作和状态丢失

// ✅ 使用 key,精确匹配
<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>

// 删除后
<ul>
{/* key="a" 找不到,删除 */}
<li key="b">B</li> // 精确匹配,保持不变
<li key="c">C</li> // 精确匹配,保持不变
// 只执行一次删除操作,性能最优

React Fiber 架构改进

React 15 及之前:

  • 递归 Diff,无法中断
  • 大量节点时阻塞主线程,导致页面卡顿

React 16+ Fiber:

  • 将递归改为链表遍历
  • 支持中断和恢复 (时间分片)
  • 优先级调度 (紧急任务优先)
// Fiber 节点结构
const fiberNode = {
type: 'div',
props: { className: 'container' },
child: ChildFiber, // 第一个子节点
sibling: SiblingFiber, // 兄弟节点
return: ParentFiber, // 父节点
effectTag: 'UPDATE', // 副作用标记
nextEffect: NextFiber // 下一个有副作用的节点
};

Diff 算法性能优化

1. 稳定的 key 值

// ❌ 错误示范
{items.map((item, index) => (
<Item key={index} data={item} /> // 索引作为 key
))}

// ✅ 正确示范
{items.map((item) => (
<Item key={item.id} data={item} /> // 唯一 ID 作为 key
))}

2. 减少层级嵌套

// ❌ 过深的嵌套
<div>
<div>
<div>
<Content />
</div>
</div>
</div>

// ✅ 扁平化结构
<div>
<Content />
</div>

3. 条件渲染优化

// ❌ 可能引起整树替换
{isLoggedIn ? <LoggedIn /> : <LoggedOut />}

// ✅ 保持组件稳定性
<div>
{isLoggedIn && <LoggedIn />}
{!isLoggedIn && <LoggedOut />}
</div>

Vue Diff 算法详解

Vue 2.x Diff 策略

Vue 2.x 的 Diff 算法同样采用 O(n) 复杂度,但实现细节与 React 有所不同。

核心流程

function patch(oldVnode, newVnode) {
// 1. 判断是否是同一个节点
if (!isSameVnode(oldVnode, newVnode)) {
createElm(newVnode); // 创建新节点
destroyElm(oldVnode); // 销毁旧节点
return;
}

// 2. 更新节点属性
updateProperties(oldVnode, newVnode);

// 3. 比较子节点
updateChildren(oldVnode.children, newVnode.children);
}

双端比较算法 (Vue 2.4+)

Vue 2.4+ 引入了双端比较优化,从前后两端同时向中间比较:

function updateChildren(oldCh, newCh) {
let oldStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let newStartIdx = 0;
let newEndIdx = newCh.length - 1;

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 四种比较情况:

// 1. 旧头 vs 新头
if (isSameVnode(oldCh[oldStartIdx], newCh[newStartIdx])) {
patch(oldCh[oldStartIdx++], newCh[newStartIdx++]);
}
// 2. 旧尾 vs 新尾
else if (isSameVnode(oldCh[oldEndIdx], newCh[newEndIdx])) {
patch(oldCh[oldEndIdx--], newCh[newEndIdx--]);
}
// 3. 旧头 vs 新尾 (节点移动)
else if (isSameVnode(oldCh[oldStartIdx], newCh[newEndIdx])) {
moveElement(oldCh[oldStartIdx++], oldEndIdx + 1);
patch(oldCh[oldStartIdx], newCh[newEndIdx--]);
}
// 4. 旧尾 vs 新头 (节点移动)
else if (isSameVnode(oldCh[oldEndIdx], newCh[newStartIdx])) {
moveElement(oldCh[oldEndIdx--], oldStartIdx);
patch(oldCh[oldEndIdx], newCh[newStartIdx++]);
}
// 5. 使用 key 查找
else {
const idxInOld = findKeyIndex(oldCh, newCh[newStartIdx].key);
if (idxInOld !== undefined) {
moveElement(oldCh[idxInOld], oldStartIdx);
patch(oldCh[idxInOld], newCh[newStartIdx]);
} else {
createElm(newCh[newStartIdx]); // 新增节点
}
newStartIdx++;
}
}
}

双端比较的优势:

// 场景:列表反转 [1, 2, 3, 4, 5] -> [5, 4, 3, 2, 1]

// ❌ React 单端比较 (需要多次移动)
// 1. 删除头部 1
// 2. 在尾部插入 1
// 3. 删除头部 2
// 4. 在尾部插入 2
// ... 共需 8 次操作

// ✅ Vue 双端比较 (只需交换指针)
// 1. 旧头 (1) vs 新头 (5) - 不匹配
// 2. 旧尾 (5) vs 新尾 (1) - 不匹配
// 3. 旧头 (1) vs 新尾 (1) - 匹配!移动指针
// 4. 旧尾 (5) vs 新头 (5) - 匹配!移动指针
// ... 共需 4 次操作

Vue 3.x Diff 算法优化

Vue 3 在 Vue 2 的基础上进行了多项优化:

1. 最长递增子序列 (LIS)

用于确定最少移动次数:

// 场景:[1, 2, 3, 4, 5] -> [1, 3, 4, 2, 5]

// 1. 构建映射关系
const sourceToIndex = { 1: 0, 3: 1, 4: 2, 2: 3, 5: 4 };

// 2. 提取索引数组
const indices = [0, 1, 2, 3, 4]; // 对应 [1, 3, 4, 2, 5]

// 3. 计算 LIS (最长递增子序列)
const lis = getLIS(indices); // [0, 1, 2, 4] - 对应 [1, 3, 4, 5]

// 4. 只移动不在 LIS 中的元素
// 只需要移动 2,其他元素保持不动

2. 静态提升 (Static Hoisting)

<!-- Vue 3 编译优化 -->
<template>
<div>
<div class="static">静态内容</div> <!-- 提升到 render 函数外 -->
<div>{{ dynamic }}</div> <!-- 需要动态更新 -->
</div>
</template>

<script>
// 编译后
const _hoisted = /*#__PURE__*/ createStaticVNode(
'<div class="static">静态内容</div>',
1
);

export function render(ctx) {
return openBlock(), createBlock("div", null, [
_hoisted, // 直接复用,跳过 Diff
createTextVNode(toDisplayString(ctx.dynamic)) // 需要 Diff
]);
}
</script>

3. Patch Flags 标记

<template>
<!-- 编译时添加标记 -->
<div class="static" :class="dynamicClass"> <!-- TEXT: 只比较文本内容 -->
{{ text }}
</div>
<button @click="handleClick">点击</button> <!-- PROPS: 只比较 click 事件 -->
</template>

<script>
// 编译后
import { TEXT, PROPS } from 'vue';

export function render() {
return createElementVNode("div", {
class: _normalizeClass(["static", dynamicClass])
}, toDisplayString(text), TEXT); // 只 Diff 文本部分

return createElementVNode("button", {
onClick: handleClick
}, "点击", PROPS, ['onClick']); // 只 Diff onClick 属性
}
</script>

Patch Flags 类型:

enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态属性 (非 class/style)
FULL_PROPS = 1 << 4,// 完整 props 对比
NEED_HYDRATION = 1 << 5,
STABLE_FRAGMENT = 1 << 6,
KEYED_FRAGMENT = 1 << 7,
UNKEYED_FRAGMENT = 1 << 8,
NEED_PATCH = 1 << 9,
DYNAMIC_SLOTS = 1 << 10,
HOISTED = -1, // 静态节点
BAIL = -2 // 禁用优化
}

React vs Vue Diff 对比

特性ReactVue 2Vue 3
时间复杂度O(n)O(n)O(n)
比较方式单端循环双端比较双端 + LIS
Key 要求必须推荐推荐
节点移动依赖 key双端优化LIS 最优解
静态优化useMemo 手动自动识别编译时标记
跳过 DiffReact.memo自动Patch Flags
更新粒度组件级节点级属性级

实际应用场景

1. 长列表渲染

// ✅ React: 使用 React.memo + key
const ListItem = React.memo(({ item }) => (
<div key={item.id}>{item.name}</div>
));

function List({ items }) {
return (
<div>
{items.map(item => <ListItem key={item.id} item={item} />)}
</div>
);
}

// ✅ Vue: 自动优化,配合 key
<template>
<div>
<ListItem v-for="item in items" :key="item.id" :item="item" />
</div>
</template>

2. 动态表格列

<!-- ✅ Vue 3: Patch Flags 自动优化 -->
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.title }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
{{ row[col.key] }}
</td>
</tr>
</tbody>
</table>
</template>

<script setup>
// columns 变化时,只更新 th,不影响 tbody
// row 数据变化时,只更新对应行的 td
</script>

3. 条件切换优化

// ✅ React: 使用 key 强制重建
function Tabs({ activeTab }) {
return (
<div>
{tabs.map(tab => (
<TabPanel
key={tab.id}
isActive={activeTab === tab.id}
/>
))}
</div>
);
}

// ✅ Vue: v-show 避免 Diff
<template>
<div>
<TabPanel
v-for="tab in tabs"
:key="tab.id"
v-show="activeTab === tab.id"
/>
</div>
</template>

性能优化建议

通用原则:

  1. 稳定的 key: 始终使用唯一且稳定的 key
  2. 减少层级: 保持虚拟 DOM 树扁平
  3. 避免频繁创建: 使用 v-show/条件缓存
  4. 拆分组件: 缩小 Diff 范围

React 特有:

// 1. 使用 React.memo 避免不必要渲染
const Memoized = React.memo(Component);

// 2. 使用 useMemo/useCallback 稳定引用
const memoizedValue = useMemo(() => compute(a, b), [a, b]);
const memoizedFn = useCallback(() => doSomething(a), [a]);

// 3. 使用 shouldComponentUpdate 自定义比较
class Component extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.id !== this.props.id;
}
}

Vue 特有:

<script setup>
// 1. 使用 Object.freeze 冻结大对象
const largeData = Object.freeze({ /* ... */ });

// 2. 使用 v-once 渲染静态内容
<div v-once>{{ staticContent }}</div>

// 3. 使用 markRaw 避免响应式转换
import { markRaw } from 'vue';
const chartInstance = markRaw(new Chart());
</script>