1. 居中元素(未知宽高)
- 方法一:flex
.parent {
display: flex;
justify-content: center;
align-items: center;
}
- 方法二:absolute + transform
.child{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
- 方法三:grid布局,place-items
.parent {
display: grid;
place-items: center; /* 等价于 align-items + justify-items */
height: 100vh; /* 示例,撑满屏幕 */
}
// 或者
.child{
place-self : center;/* 等价于 align-self + justify-self */
}
- 方法四: grid布局,justify-items和align-items
.grid {
display: grid;
justify-items: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
/* 或单个元素:*/
.item {
justify-self: center;
align-self: center;
}
2.数组常用方法:
- 查找类:
find(callback):返回数组中第一个满足条件的元素findIndex(callback):返回数组中第一个满足条件元素的下标includes(value):判断数组中是否包含某个值(返回true/false)indexOf(value):返回某个值第一次出现的位置lastIndexOf(value):返回某个值最后一次出现的位置
- 遍历类:
forEach(callback):遍历数组(没有返回值)map(callback):对每个元素进行处理,返回新数组filter(callback):筛选满足条件的元素,返回新数组some(callback):判断是否至少有一个元素满足条件every(callback):判断是否所有元素都满足条件
- 修改类:(会改变原数组)
push(...items):尾部添加元素,返回新长度pop():删除尾部元素,返回删除的值shift():删除头部元素,返回删除的值unshift(...items):头部添加元素,返回新长度splice(start, deleteCount, ...items):删除/替换/插入元素sort(compareFn):对数组排序(默认字典序,会改变原数组)reverse():反转数组顺序
- 归并/组合类
concat(...arrays):合并数组,返回新数组reduce(callback, initialValue):累计处理,返回单个值(常用于求和、统计)flat(depth=1):扁平化数组flatMap(callback):先map再flat(1)
- 复制/截取类(不改变原数组)
slice(start, end):返回数组的一个片段join(separator):把数组转换成字符串
map vs forEach
- map:
- 返回新的数组,长度与原数组下相同
- 回调函数的return值会场成为新的数组项
- forEach:
- 没有返回值(返回undefined)
- 不能通过 return/break中断
- 如何中断forEach?
- for...of 可以用break
- forEach 硬中断只能通过 throw + try/catch
try{ [1,2,3].forEach(x=>{ if(x===2) throw Error('break') console.log(x) }) }catch(err){} // 所以实际开发中想要中断就不要用forEach ,该用for...of
3.遍历对象的方法
遍历键
1. for...in
- 遍历对象可枚举属性(包括继承的)
- 一般搭配hasOwnProperty使用,避免遍历原型上的属性
const obj = { a: 1, b: 2 };
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key, obj[key]);
}
}
// a 1
// b 2
2. Object.keys(obj)
- 返回对象自身可枚举属性键的数组
- 常配合 forEach/for...of遍历这个数组
Object.keys(obj).forEach(key=>{
console.log(key)
})
遍历值
1.Object.values(obj)
- 返回对象自身可枚举属性值的数组
Object.values(obj).forEach(value => {
console.log(value);
});
// 1, 2
遍历键值对([key,value])
1.obj.entries(obj)
- 返回对象的[key,value]数组
for (let [key, value] of Object.entries(obj)) {
console.log(key, value);
}
其他方法
1.Reflect.ownKeys(obj)
- 返回对象的所有键(包括不可枚举和Symbol键)
Reflect.ownkeys(obj).forEach(key=>{
console.log(key,obj[key])
})
2. for...of 配合 Object.entries / Object.keys / Object.values
for (let [k, v] of Object.entries(obj)) {
console.log(k, v);
}
4. typeof
-
基本类型:
number→ number(包括 NaN)string→ stringboolean→ booleanundefined→ undefinedsymbol→ symbolbigint→ bigint
-
特殊:
null→ object(历史遗留 bug)function→ function- 其他对象 → object
-
👉 记住:
typeof NaN === 'number' -
👉 记住:
typeof null === 'object'
5.判断是否数组的方法
Array.isArray(obj)obj instanceof ArrayObject.prototype.toString.call(obj) === "[object Array]"- 详解下👆:
- js中所有的对象都最终继承自Object.prototype。
- Object.prototype 上有一个toString方法,用来
把一个值转成字符串 - Object.prototype.toString 是 Object原型上的toString方法
- 用 .call(obj) 调用,是把函数内部调用的this绑定为obj
使用Object.prototype.toString()这个最底层的toString来识别类型。如果直接调用obj.toString()那就有可能导致调用的是obj身上的toString()
6.call / bind / apply
- call:fn.call(thisArg,arg1,arg2...) 立即执行
- apply : fn.apply(thisArg,[arg1,arg2...]) 立即执行
- bind:
fn.bind(thisArg, arg1, arg2...)→ 返回新函数,不立即执行
7.Promise API
- .then 、 .catch 、 .finally
- Promise.all() : 所有成功才成功。有一个reject就失败
- Promise.race() : 第一个返回的结果决定
- Priomise.allSettled() : 无论成功失败都返回 结果数组
- Promise.any() : - 第一个成功就返回,若都失败则抛
AggregateError
8.async/await 优势
-
语法糖(基于 Promise)
-
优势:
- 代码可读性更强,避免
.then回调地狱 - 处理异步更接近同步写法
- 支持
try/catch捕获错误(比 Promise 链方便) - 更适合与
for of结合执行串行任务
- 代码可读性更强,避免
9. 实现防抖(debounce)函数时,为什么建议用 apply 而不是直接调用函数?
- 防抖: 把高频的事件合并成最后一次执行
function debounce(fn,wait=200){
let timer = null;
return function(...args){
const ctx = this; // 保持调用者的this
clearTimerout(timer);
timer = setTimeout(()=>{
fn.apply(ctx,args) // 用apply穿透this和参数
},wait)
}
}
关键点
setTimeout回调里的this默认会丢失。- 防抖的封装里要手动保存外层函数的
this(ctx = this)。 - 最终用
fn.apply(ctx, args)来确保 被包装的函数拿到和原来一样的 this 和参数。 - 如果fn是箭头函数,或者不依赖于this,就不影响,但是习惯上还是使用 .apply() 强制绑定函数执行时的this和fn一致
10.React Hooks中 useMemo 和 useCallback 本质区别是什么
useMeno : 缓存【计算结果】(值)
const sum = useMemo(()=>a+b,[a,b])
// 作用:缓存计算结果,只有依赖项发声变化的时候才重新计算
// 返回的就是 a+b的值
// 避免因为组件重渲染而重复做开销大的计算
useCallback : 缓存【函数定义】
const handleClick = useCallback(()=>{
console.log("clicked", value);
},[value]);
// 作用:缓存一个函数,只有依赖变化的时候才会返回新的函数
// 返回的函数是原函数本身
// 避免子组件收到“新的函数引用”导致不必要的重新渲染【在把父组件的函数作为参数传递给子组件的场景】
本质区别
useMemo: 缓存一个【值】。(执行函数->得到结果->缓存结果)useCallback:缓存一个【函数本身】,不缓存函数的执行结果
11.手写 Promise.allSettled
- 要求 : 返回一个Promise, 在所有输入Promise都最终完成后resolve,结果为每一个promise的结果
{ status: 'fulfilled'|'rejected', value|reason: ... }。
function allSettled(promises) {
return Promise.all(
promises.map(p =>
Promise.resolve(p)
.then(value => ({ status: 'fulfilled', value }))
.catch(reason => ({ status: 'rejected', reason }))
)
);
}
- Promise.all + Promise.resolve 保证了Promise一定会执行所有的promise,并且返回结果
- `Promise.resolve(p)` 保证 `p` 即使不是 Promise 也能被统一处理。
- `Promise.all` 会在所有映射后的 Promise 完成后 resolve(因为映射后的 promise 永远 resolve,不会 reject)。
12.浏览器事件循环中,requestAnimationFrame 的执行时机
前言:js执行模型【同步代码➡️宏任务➡️微任务】
- 单线程,同一时间只能执行一段代码
- 为了能同时处理【同步】和【异步】任务,js引入了【事件循环】
- 事件循环中有两个主要的“任务队列”
- macrotask queue(宏任务队列)
- microtask queue(微任务队列)
宏任务:每次事件循环会从宏任务队列里取出一个任务来执行。
setTimeoutsetIntervalsetImmediate(Node.js)requestAnimationFrame(浏览器)- 整个脚本
script本身
微任务:当宏任务执行完毕后,下一个宏任务开始之前,立刻执行的小任务
Promise.then/catch/finallyprocess.nextTick(Node.js)MutationObserver(浏览器)
执行顺序:
- 执行一个宏任务(比如一段
script、或者一个setTimeout回调) - 执行所有产生的微任务(清空 microtask queue)
- 再取下一个宏任务
- 如此循环
console.log(1);
setTimeout(() => {
console.log("setTimeout"); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log("promise1"); // 微任务
}).then(() => {
console.log("promise2"); // 微任务
});
console.log(2);
- 宏任务:执行整个script
- 输出1
- 注册setTimeout
- 注册promise.then
- 输出2
- 清空微任务队列
- 输出promise1
- 输出promise2
- 下一个宏任务
- 输出setTimeout
requestAnimationFrame(rAF)的执行时机:介于【宏任务/微任务】和 【渲染阶段】之间
1. 浏览器事件循环大体顺序
每一帧(frame)浏览器要做这些事:
-
执行一个宏任务
- 比如整段
script,或者setTimeout的回调。
- 比如整段
-
清空微任务队列(microtask)
- 比如
Promise.then、MutationObserver。
- 比如
-
更新渲染前的准备阶段
- 执行
requestAnimationFrame回调(如果当前帧要渲染)。
- 执行
-
布局 + 绘制(渲染)
- 浏览器把 DOM 更新到屏幕上。
-
下一轮事件循环
- 进入下一个宏任务。
2. requestAnimationFrame 的特点
- 它的回调 在每次浏览器重绘之前执行。
- 浏览器通常是 每秒 60 次刷新(16.6ms 一帧),如果电脑性能高、显示器高刷新率,也可能是 120Hz/144Hz。
- 与
setTimeout(fn, 16)不同,rAF 是 由浏览器决定时机,不会因为 Tab 切到后台而浪费 CPU,它会暂停。
3. 举个例子
console.log("script start");
setTimeout(() => {
console.log("timeout"); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log("promise"); // 微任务
});
requestAnimationFrame(() => {
console.log("rAF"); // 渲染前执行
});
console.log("script end");
执行顺序:
-
宏任务(
script本身):- 打印
script start - 注册
setTimeout - 注册
Promise.then - 注册
rAF - 打印
script end
- 打印
-
清空微任务队列:
- 打印
promise
- 打印
-
渲染前阶段:
- 打印
rAF
- 打印
-
下一个宏任务:
- 打印
timeout
- 打印
最终输出:
script start
script end
promise
rAF
timeout
✅ 总结
- 宏任务:
script、setTimeout、setInterval等。 - 微任务:
Promise.then、MutationObserver。 - requestAnimationFrame:在 微任务执行完,渲染前 执行。
👉 所以 rAF 的位置可以理解为:
宏任务 → 微任务 → rAF → 渲染 → 下一轮宏任务
13.如何用 CSS 实现宽度自适应且保持宽高比 1:1 的容器?
// 现代做法:
.squre{
width:100%;
aspect-ratio :1/1;
background: #eee;
}
// 兼容做法
<div class="square">
<div class="content">...</div>
</div>
.square {
position: relative;
width: 100%;
padding-top: 100%; /* 高度 = 宽度 * 100% -> 1:1 */
}
.square .content {
position: absolute;
inset: 0;// 相当于top/bottom/left/right : 0
}
14.## 实现虚拟列表(virtual list)时,如何计算可视区域外的缓冲区(buffer)?
1.为什么需要缓冲区(buffer)
- 如果只渲染【可视区域】,滚动的时候会频繁销毁/创建节点,用户可能看到【白屏闪烁】
- 为了平滑体验,需要在可视区域 上下多渲染一部分额外内容,即 缓冲区 buffer。
2. 如何计算?
已知:
- scrollTop : 滚动条滚动的距离
- viewPortHeight: 容器可视区域高度
- itemHeight:单个元素高度 则可视区域覆盖的元素下标范围:
startIndex = Math.floor(scrollTop / itemHeight)
endIndex = Math.floor((scrollTop + viewportHeight) / itemHeight)
3. 加入缓冲区
设定一个bufferSize(缓冲区大小【单位:元素个数】)
- 往上扩展
bufferSize个 - 往下扩展
bufferSize个
最终渲染范围:
renderStart = Math.max(0, startIndex - bufferSize)
renderEnd = Math.min(totalCount-1,endIndex + bufferSize )
4. 如何选择buffer大小
-
(1) 固定数量buffer
- 例如
bufferSize = 5,总是上下额外渲染 5 个元素。 - 优点:实现简单,适合固定高度列表。
- 例如
-
(2) 根据视窗比例buffer
- 例如缓冲区高度 = 0.5 * viewportHeight
- 转换成元素数量:
bufferSize = Math.ceil((viewportHeight * 0.5) / itemHeight) - 优点:在不同屏幕高度下表现更一致。
-
(3). 动态调节 buffer
- 根据滚动速度调整 buffer(滚动越快,buffer 越大),减少白屏风险。
15.Webpack 的 tree-shaking 原理及 ES Module 限制
1. tree-shaking(摇树优化):
- 把代码中没有用到的模块/函数/变量删掉,减少打包体积
- 核心依赖:静态分析,只有能在编译阶段确定未被使用的代码,才能被删除。
2. webpack Tree-shaking 原理
-
Webpack 本身 不直接做 Tree-shaking,它依赖 Terser(压缩阶段的 DCE, Dead Code Elimination) ,
-
主要流程:
1. ESM 静态依赖分析
- Webpack 解析 **import/export**, 构建依赖模块图 - 标记出哪些到处知被使用 (used export)2. 标记未使用的导出
- 对于未被使用的export,Webpack会在bundle里打上注释3. 交给压缩工具(Terser)删除
- 👉 所以:webpack tree-shaking 就等于 “标记 + 压缩工具删除”
-
为什么必须是ES Modules
-
因为 Tree-shaking 是静态结构,而common.js是动态的
-
ESM的特点:
- import/export 是静态的、编译时确定的
- 不能写条件导入
if (cond) { import { foo } from './lib' // ❌ 不合法 }- 所以打包工具能确定依赖关系,安全移除未使用代码
-
-
commonjs特点
- require()是运行时调用,可以动态拼接
const m = require('./'+name) // 动态路径- 工具无法在编译阶段静态确定依赖,所以不能安全使用tree-shake
-
4. ES Module限制
为了支持 Tree-shaking,ESM 有一些限制/规则:
-
1). 静态导入/导出
- import/export 必须在顶层,不能写在函数或条件语句里。
-
2). 只导入不使用,也不会报错
- 因为工具会自动删除未使用部分。
-
3). 副作用(Side Effects)限制
-
如果一个模块有副作用(比如改全局变量、执行函数),就算没有使用它的 export,也可能不能删。
-
可以在
package.json里声明:{ "sideEffects": false }告诉 Webpack 这个包没有副作用,可以安全移除。
-
5. 局限性:
-
动态场景不支持
- 比如动态 import 名称、对象属性方式访问 export。
-
有副作用的代码不敢删
- 例如模块内执行的语句:
console.log("I will run even if not imported")
总结:
- Webpack的 Tree-shaking 本质:
- 利用ES Module的静态结构,标记未使用的export→交给压缩工具删除
- 限制:必须使用ES Module,且import/export 静态化;副作用模块要小心处理。
- 👉 所以:webpack tree-shaking 就等于 “标记 + 压缩工具删除”
16.从输入 URL 到页面展示的全链路性能优化方案(要点清单)
用户输入URL后发生了什么?
1. 用户输入URL
比如用户在浏览器地址栏输入:
https://www.example.com/page
浏览器首先要解析出这个 URL 的各个部分:
- 协议:
https - 域名:
www.example.com - 端口:默认 443(HTTPS)
- 路径:
/page - 查询参数(如果有):
?a=1&b=2 - 哈希(如果有):
#section1
2. 浏览器检查缓存
浏览器会检查是否有缓存资源可以直接使用
- DNS 缓存:是否已经知道
www.example.com的 IP - HTTP 缓存(Cache-Control / ETag / Last-Modified)
- Service Worker 缓存(如果网站有 PWA 支持)
如果缓存命中,就可能直接返回,不用走网络。
3. DNS解析
-
域名 → IP 地址
当你在浏览器输入www.google.com时,DNS 会帮你找到对应的 IP 地址,然后浏览器才能请求到服务器。 -
隐藏复杂性
用户只需要记住域名,不用记 IP。 -
分布式查询
DNS 是分布式的,不是单台服务器完成解析,全球有层级结构。
如果浏览器没缓存 IP,需要将域名转换为 IP 地址:
-
浏览器查本地 hosts 文件。
-
查操作系统的 DNS 缓存。
-
向本地 DNS 服务器请求(通常是运营商提供或自定义的 DNS)。
-
DNS 服务器递归查找:
- 根域名服务器 → 顶级域名服务器 → 权威域名服务器
-
最终返回
www.example.com对应的 IP(比如93.184.216.34)。
✅ 结果:浏览器得到了目标服务器的 IP。
4. 建立TCP链接
- SYN:客户端发送同步请求包
- SYN-ACK:服务器回应确认
- ACK:客户端确认,连接建立
TCP 连接保证可靠、有序、完整的数据传输。
5. 建立TLS/SSL(HTTPS情况)
如果是HTTPS,需要进行TLS握手加密
- 浏览器验证服务器证书(CA 签发)
- 协商加密算法(对称加密 + 非对称加密)
- 生成共享密钥
- 后续通信都使用加密通道(HTTPS)
现在 TCP + TLS 完成,可以安全发送 HTTP 请求。
6. 发送HTTP请求
浏览器构建HTTP请求头:
GET /page HTTP/1.1
Host: www.example.com
User-Agent: Chrome/xxx
Accept: text/html,application/xhtml+xml
Cookie: xxx
然后通过TCP连接发给服务器
7. 服务器处理请求
服务器接收到请求后
- 解析 URL 和方法(GET / POST 等)
- 路由到对应的处理逻辑或静态资源
- 生成响应(HTML / JSON / 图片等)
- 设置响应头(Content-Type, Set-Cookie, Cache-Control 等)
- 发送响应给客户端
8.浏览器接收到响应
浏览器收到 HTTP 响应:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 1024
...
<html>...</html>
- 如果有重定向(3xx),浏览器会自动重新发请求
- 如果有压缩(gzip / br),浏览器解压
9.浏览器渲染页面
-
解析 HTML → 构建 DOM 树
-
解析 CSS → 构建 CSSOM 树
-
生成 Render Tree(DOM + CSSOM 结合)
-
布局(Layout / Reflow) → 计算每个元素的位置和大小
-
绘制(Paint) → 填充像素到屏幕
-
JavaScript 执行:
- JS 可能修改 DOM/CSSOM(触发 Reflow / Repaint)
- 异步请求(AJAX / Fetch / WebSocket)进一步更新页面内容
-
资源加载:
- JS、CSS、图片、字体、视频等异步加载
- 浏览器会开启多个连接并发请求资源
10 浏览器优化行为
浏览器可能会:
- 预加载(Preload / Prefetch)
- 懒加载(Lazy Loading)
- Service Worker 缓存资源
- HTTP/2 或 HTTP/3 多路复用
这些都会加速用户看到页面的时间。
总结流程
- 用户输入URL
- 浏览器检查缓存
- DNS解析→得到服务器IP
- TCP三次握手
- TLS握手(https)
- 浏览器发送HTTP请求
- 服务器处理并返回响应
- 浏览器接收并解析HTML/CSS/JS
- 构建 DOM/CSSOM → Render Tree → Layout → Paint
- 异步加载其他资源,执行JS,交互就绪
17.手写:带并发限制的异步调度器(最多同时运行 2 个任务)
你可以把这个 Scheduler 想象成:
- Scheduler = 电影院检票员
- limit = 电影院的座位数(最多能同时容纳多少人看片)
- queue = 等候区(没座位时,观众只能在这里排队)
- running = 目前正在看片的观众人数
- add(fn) = 来了一个观众(任务),他说“我想看电影(fn)”
- _drain() = 检票员:一旦发现有空座,就喊等候区的人进场
- onIdle() = 等所有观众都看完了,电影院彻底空了
/**
* 带并发限制的异步任务调度器
* - 最多允许 limit 个任务同时执行
* - 超过的任务会进入等待队列,等有空闲时再启动
*/
class Scheduler {
constructor(limit = 2) {
this.limit = limit; // 最大并发数(电影院的座位数)
this.running = 0; // 当前正在执行的任务数(正在看片的人)
this.queue = []; // 等待队列(排队等候的观众)
this._idleResolvers = []; // 监听器(有人在外面等通知,等所有人看完再告诉他)
}
/**
* 添加一个任务
* @param {Function} fn - 必须是一个函数,执行后返回 Promise(或者同步返回值也行)
* @returns {Promise} - 返回一个 Promise,表示这个任务的执行结果
*/
add(fn) {
return new Promise((resolve, reject) => {
// 把任务包装成一个可执行函数,方便排队
const run = () => {
// 1. 占用一个并发名额
this.running++;
// 2. 用 Promise.resolve 包裹,兼容:
// - fn 返回 Promise(正常异步任务)
// - fn 返回普通值(同步任务)
// - fn 抛出错误(同步异常)
Promise.resolve()
.then(fn) // 开始放电影(执行任务)
.then(resolve) // 电影看完 -> 门票上写“成功”
.catch(reject) // 如果电影坏了 -> 门票上写“失败”
.finally(() => {
this.running--; // 看完离场,空出一个座位
this._drain(); // 检票员喊:下一个进来!
});
};
this.queue.push(run); // 新观众先去等候区
this._drain(); // 检票员马上检查:有空位就让他进
});
}
/**
* 当所有任务都执行完毕(队列清空 & 没有运行中的任务)时 resolve
* @returns {Promise<void>}
* “等所有观众都看完电影时,再告诉我。”
*/
onIdle() {
if (this.running === 0 && this.queue.length === 0) {
// 如果当前已经没任务了,直接返回 resolved Promise
return Promise.resolve();
}
// 否则存一个 resolver,等空闲时再触发
return new Promise(res => this._idleResolvers.push(res));
}
/**
* 内部调度函数:
* - 如果并发还没达到上限,就取队列中的任务执行
* - 如果所有任务都执行完,触发 idle 回调
*/
_drain() {
// 不断从队列取任务,直到达到并发上限
while (this.running < this.limit && this.queue.length > 0) {
const task = this.queue.shift(); // 从队头取一个任务
task(); // 执行任务
}
// 如果没有运行中的任务 && 队列也空了,说明彻底完成了
if (this.running === 0 && this.queue.length === 0 && this._idleResolvers.length > 0) {
// 把之前 onIdle 等待的 Promise 全部 resolve 掉
const resolvers = this._idleResolvers;
this._idleResolvers = []; // 清空,避免重复触发
resolvers.forEach(r => r());
}
}
}
18.如何用 IntersectionObserver 实现图片懒加载?
定义:
- IntersectionObserver 是浏览器提供的一个API接口,用来观察一个元素和它的父容器或视口的
交叉状态。【也就是告诉你这个元素什么时候进入或者离开可视窗口(或者父元素指定区域)】
作用:
- 在以前,如果你想知道一个元素什么时候出现在屏幕上,通常要用 scroll 事件 + getBoundingClientRect 去计算,既复杂又性能差。
IntersectionObserver可以 浏览器内部优化,直接告诉你元素是否可见,效率高很多。
使用场景
- 图片懒加载:只有图片进入视口才去加载资源。
- 无限滚动 / 下拉加载:滚动到底部时自动加载更多内容。
- 广告或统计曝光:判断广告是否真正出现在用户屏幕上。
- 元素进入/离开视口时触发动画。
// 假设有一个 div
const box = document.querySelector('.box');
// 创建 IntersectionObserver
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素进入可视区域');
} else {
console.log('元素离开可视区域');
}
});
}, {
root: null, // 观察的基准容器,null 表示视口
threshold: 0.5 // 元素至少 50% 可见时触发
});
// 开始观察
observer.observe(box);
19. Vue3 响应式原理中,为什么用 Proxy替代defineProperty?
1️⃣ Vue2 的做法:Object.defineProperty
在 Vue2 中,响应式是这样实现的:
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`访问 ${key}`);
return val;
},
set(newVal) {
console.log(`设置 ${key} = ${newVal}`);
val = newVal;
}
});
}
缺点:
-
- 只能劫持已有属性
- 新增属性/删除属性 无法监听
- 必须用 Vue.set/Vue.delete 来解决
const obj = {a:1} defineReactive(obj,"a",obj.a) obj.b=2 // 无法被监听 -
- 数组监听有缺陷
- defineProperty 不饿能拦截数组的下标访问和修改
- Vue2 只能“改写数组方法”(push/pop/shift/unshift/splice/sort/reverse),很麻烦而且有限制
-
- 深层嵌套对象性能差
- 需要层层遍历对象的所有属性,一层一层用defineProperty 劫持→ 初始化开销大
2️⃣ Vue3 的做法:Proxy
在Vue3中是这样实现的
const obj = { a: 1, b: { c: 2 } };
const proxy = new Proxy(obj, {
get(target, key) {
console.log(`访问 ${key}`);
return Reflect.get(target, key);
},
set(target, key, value) {
console.log(`设置 ${key} = ${value}`);
return Reflect.set(target, key, value);
}
});
// `Reflect.get` 和 `Reflect.set` 是和 `Proxy` 经常一起用的工具方法。
Reflect 是 ES6新增的内置对象
1. 提供了更加规范、语义化的 对象操作方法,代替obj[a]这种写法
2. 和 `Proxy` 搭配使用,保证拦截操作后还能正确调用原本的行为。
## 为什么 Proxy 里常用 Reflect?
因为在 `Proxy` 里拦截操作时,我们通常想在做一些额外逻辑后,**继续执行原本的默认行为**。
这时候用 `Reflect` 最安全,也能避免无限递归。
优点:
-
可以监听整个对象,不局限于某个属性
- 新增 / 删除属性都能监听到
proxy.c = 3; // ✅ 能监听 delete proxy.a; // ✅ 也能监听 -
数组下标和 length 修改也能监听
proxy[0] = 100; // ✅ 能监听 proxy.length = 0; // ✅ 能监听 -
按需监听,不用递归遍历所有属性
- 访问时再“懒代理”(lazy proxy),性能更高
- 避免 Vue2 初始化时大规模递归带来的性能问题
-
功能更强大
-
Proxy能拦截 13 种操作(get/set/deleteProperty/has/ownKeys 等),而defineProperty只能拦截 get/set -
Vue3 可以更灵活地扩展能力(比如
readonly、shallowReactive等)
-
20.设计一个前端灰度发布(灰度/灰度发布)系统
🔧 什么是灰度发布?
灰度发布(canary release)= 让 部分用户 先体验新功能/新版本,收集反馈,验证稳定性 → 再逐步扩大范围,最后全量上线。
它的目标是 降低风险,避免“一次性全量上线导致线上事故”。
🎯 系统目标
- 按 比例 控制:比如先 5%,再 20%,最后 100%。
- 按 用户维度 控制:可以指定某些用户、某个地区、某个渠道。
- 按 功能维度 控制:支持 A/B Test,不同用户看到不同的前端功能。
- 动态可控:不用重新发版就能调整灰度策略。
- 可回滚:如果灰度有问题,能立即关闭或回滚。
🏗️ 系统设计思路
1. 用户分流
关键点:如何把用户划分到“灰度组” or “老版本组”?
-
按用户 ID hash 分流(最常用)
- 算法:
hash(userId) % 100 < 灰度比例 - 稳定性:同一个用户始终落在同一组,不会抖动
- 算法:
-
按地区/渠道分流
- 比如先在“北京” 或 “iOS 用户” 测试
-
按流量比例随机分流
- 无法保证用户稳定归属,但适合短期测试
👉 分流逻辑要放在 前端运行前(最好在网关/中间层),避免用户先看到老版本再跳新版本。
2. 版本管理方案
前端有两种灰度模式:
A. 整包灰度(整个前端应用)
-
服务器 / CDN 上有两个版本:
- 老版本(稳定版)
- 新版本(灰度版)
-
网关 / Nginx 根据分流策略,把请求分配到不同版本入口文件(index.html)。
B. 功能级灰度(Feature Flag)
-
前端只发一个版本,但内部有 开关系统:
if (featureFlags.newUI) { renderNewUI() } else { renderOldUI() } -
开关配置由 配置中心 / 后端接口 下发,支持动态修改。
-
适合做 A/B 测试、多版本并存。
3. 配置中心(核心)
需要一个 灰度配置中心(可以是后端接口 + 数据库 + 管理后台):
-
存放灰度策略:
- 哪些用户 / 区域 / 渠道走灰度?
- 当前灰度比例是多少?(5%、10%、50%...)
- 哪些功能开启?哪些功能关闭?
-
管理员可以通过后台 UI 修改策略,前端会定时拉取配置,或者登录时下发配置。
4. 前端 SDK
为了让前端接入简单,需要一个 SDK:
import { featureFlags } from './gray-sdk';
// 判断用户是否进入灰度组
if (featureFlags.isGrayUser) {
loadGrayVersion();
} else {
loadStableVersion();
}
// 判断功能开关
if (featureFlags.enable('newUI')) {
renderNewUI();
} else {
renderOldUI();
}
SDK 内部逻辑:
- 从服务端获取用户的灰度配置
- 计算是否落入灰度组
- 提供
isGrayUser、enable(key)等 API 给业务代码使用
5. 监控 & 回滚
灰度系统不能只是“分流”,还必须要有 监控 + 回滚:
-
埋点 & 数据监控
- 灰度组 vs 老版本组 → 对比报错率、转化率、性能指标
- 如果灰度组异常明显 → 自动触发报警
-
一键回滚
- 管理员在后台关闭灰度配置
- 所有用户重新走老版本 / 老功能
- 前端立即生效(下次刷新即可恢复)
✅ 总结方案
- 入口层(网关/CDN) :负责版本分流(整包灰度)
- 配置中心:存放灰度策略(比例 / 用户 / 功能开关)
- 前端 SDK:拉取配置,控制功能开关 or 判断版本归属
- 监控系统:实时对比灰度组和老版本的指标,提供回滚