面经-03

86 阅读21分钟

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):先 mapflat(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 → string
    • boolean → boolean
    • undefined → undefined
    • symbol → symbol
    • bigint → bigint
  • 特殊:

    • null → object(历史遗留 bug)
    • function → function
    • 其他对象 → object
  • 👉 记住:typeof NaN === 'number'

  • 👉 记住: typeof null === 'object'

5.判断是否数组的方法

  • Array.isArray(obj)
  • obj instanceof Array
  • Object.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...]) 立即执行
  • bindfn.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(微任务队列)
宏任务:每次事件循环会从宏任务队列里取出一个任务来执行。
  • setTimeout
  • setInterval
  • setImmediate(Node.js)
  • requestAnimationFrame(浏览器)
  • 整个脚本 script 本身
微任务:当宏任务执行完毕后,下一个宏任务开始之前,立刻执行的小任务
  • Promise.then/catch/finally
  • process.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)浏览器要做这些事:

  1. 执行一个宏任务

    • 比如整段 script,或者 setTimeout 的回调。
  2. 清空微任务队列(microtask)

    • 比如 Promise.thenMutationObserver
  3. 更新渲染前的准备阶段

    • 执行 requestAnimationFrame 回调(如果当前帧要渲染)。
  4. 布局 + 绘制(渲染)

    • 浏览器把 DOM 更新到屏幕上。
  5. 下一轮事件循环

    • 进入下一个宏任务。

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");

执行顺序:

  1. 宏任务(script 本身):

    • 打印 script start
    • 注册 setTimeout
    • 注册 Promise.then
    • 注册 rAF
    • 打印 script end
  2. 清空微任务队列:

    • 打印 promise
  3. 渲染前阶段:

    • 打印 rAF
  4. 下一个宏任务:

    • 打印 timeout

最终输出:

script start
script end
promise
rAF
timeout

✅ 总结

  • 宏任务scriptsetTimeoutsetInterval 等。
  • 微任务Promise.thenMutationObserver
  • 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 静态化;副作用模块要小心处理。

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 地址:
  1. 浏览器查本地 hosts 文件。

  2. 查操作系统的 DNS 缓存。

  3. 向本地 DNS 服务器请求(通常是运营商提供或自定义的 DNS)。

  4. DNS 服务器递归查找:

    • 根域名服务器 → 顶级域名服务器 → 权威域名服务器
  5. 最终返回 www.example.com 对应的 IP(比如 93.184.216.34)。

✅ 结果:浏览器得到了目标服务器的 IP。

4. 建立TCP链接
  1. SYN:客户端发送同步请求包
  2. SYN-ACK:服务器回应确认
  3. ACK:客户端确认,连接建立

TCP 连接保证可靠、有序、完整的数据传输。

5. 建立TLS/SSL(HTTPS情况)

如果是HTTPS,需要进行TLS握手加密

  1. 浏览器验证服务器证书(CA 签发)
  2. 协商加密算法(对称加密 + 非对称加密)
  3. 生成共享密钥
  4. 后续通信都使用加密通道(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. 服务器处理请求

服务器接收到请求后

  1. 解析 URL 和方法(GET / POST 等)
  2. 路由到对应的处理逻辑或静态资源
  3. 生成响应(HTML / JSON / 图片等)
  4. 设置响应头(Content-Type, Set-Cookie, Cache-Control 等)
  5. 发送响应给客户端
8.浏览器接收到响应

浏览器收到 HTTP 响应:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 1024
...
<html>...</html>
  • 如果有重定向(3xx),浏览器会自动重新发请求
  • 如果有压缩(gzip / br),浏览器解压
9.浏览器渲染页面
  1. 解析 HTML → 构建 DOM 树

  2. 解析 CSS → 构建 CSSOM 树

  3. 生成 Render Tree(DOM + CSSOM 结合)

  4. 布局(Layout / Reflow) → 计算每个元素的位置和大小

  5. 绘制(Paint) → 填充像素到屏幕

  6. JavaScript 执行

    • JS 可能修改 DOM/CSSOM(触发 Reflow / Repaint)
    • 异步请求(AJAX / Fetch / WebSocket)进一步更新页面内容
  • 资源加载

    • JS、CSS、图片、字体、视频等异步加载
    • 浏览器会开启多个连接并发请求资源
10 浏览器优化行为

浏览器可能会:

  • 预加载(Preload / Prefetch)
  • 懒加载(Lazy Loading)
  • Service Worker 缓存资源
  • HTTP/2 或 HTTP/3 多路复用

这些都会加速用户看到页面的时间。

总结流程

  1. 用户输入URL
  2. 浏览器检查缓存
  3. DNS解析→得到服务器IP
  4. TCP三次握手
  5. TLS握手(https)
  6. 浏览器发送HTTP请求
  7. 服务器处理并返回响应
  8. 浏览器接收并解析HTML/CSS/JS
  9. 构建 DOM/CSSOM → Render Tree → Layout → Paint
  10. 异步加载其他资源,执行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;
    }
  });
}
缺点:
    1. 只能劫持已有属性
    • 新增属性/删除属性 无法监听
    • 必须用 Vue.set/Vue.delete 来解决
    const obj = {a:1}
    defineReactive(obj,"a",obj.a)
    obj.b=2 // 无法被监听
    
    1. 数组监听有缺陷
    • defineProperty 不饿能拦截数组的下标访问和修改
    • Vue2 只能“改写数组方法”(push/pop/shift/unshift/splice/sort/reverse),很麻烦而且有限制
    1. 深层嵌套对象性能差
    • 需要层层遍历对象的所有属性,一层一层用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` 经常一起用的工具方法。
  ReflectES6新增的内置对象
 1. 提供了更加规范、语义化的 对象操作方法,代替obj[a]这种写法
 2.`Proxy` 搭配使用,保证拦截操作后还能正确调用原本的行为。


## 为什么 Proxy 里常用 Reflect?

因为在 `Proxy` 里拦截操作时,我们通常想在做一些额外逻辑后,**继续执行原本的默认行为**。  
这时候用 `Reflect` 最安全,也能避免无限递归。

优点:

  1. 可以监听整个对象,不局限于某个属性

    • 新增 / 删除属性都能监听到
    proxy.c = 3;   // ✅ 能监听
    delete proxy.a; // ✅ 也能监听
    
  2. 数组下标和 length 修改也能监听

    proxy[0] = 100;   // ✅ 能监听
    proxy.length = 0; // ✅ 能监听
    
  3. 按需监听,不用递归遍历所有属性

    • 访问时再“懒代理”(lazy proxy),性能更高
    • 避免 Vue2 初始化时大规模递归带来的性能问题
  4. 功能更强大

    • Proxy 能拦截 13 种操作(get/set/deleteProperty/has/ownKeys 等),而 defineProperty 只能拦截 get/set

    • Vue3 可以更灵活地扩展能力(比如 readonlyshallowReactive 等)

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 内部逻辑:

  • 从服务端获取用户的灰度配置
  • 计算是否落入灰度组
  • 提供 isGrayUserenable(key) 等 API 给业务代码使用

5. 监控 & 回滚

灰度系统不能只是“分流”,还必须要有 监控 + 回滚

  1. 埋点 & 数据监控

    • 灰度组 vs 老版本组 → 对比报错率、转化率、性能指标
    • 如果灰度组异常明显 → 自动触发报警
  2. 一键回滚

    • 管理员在后台关闭灰度配置
    • 所有用户重新走老版本 / 老功能
    • 前端立即生效(下次刷新即可恢复)

✅ 总结方案

  • 入口层(网关/CDN) :负责版本分流(整包灰度)
  • 配置中心:存放灰度策略(比例 / 用户 / 功能开关)
  • 前端 SDK:拉取配置,控制功能开关 or 判断版本归属
  • 监控系统:实时对比灰度组和老版本的指标,提供回滚