搜尋文章

页面交互卡顿、掉帧严重怎么排查?从帧预算出发,用 Chrome Performance 定位脚本、布局、绘制与合成瓶颈,再选择任务拆分、虚拟列表和绘制优化,并通过同场景录制验证效果。Vue 面试完整笔记:响应式、组件、路由、Pinia 与渲染机制覆盖《Vue 面试》合集全部 28 集:前言与 27 节专题。逐集整理核心概念、代码思路、面试答法与易错边界,并校正响应式、Pinia、diff 和 nextTick 中的常见误解。Vue 响应式底层原理:Proxy 与视图更新(P6)从手动更新 DOM 出发,理解 Proxy 的 get/set 拦截、与 Vue 2 属性拦截的区别,以及对象、数组和原始对象访问的边界,厘清拦截变化与依赖追踪的关系。前端面试 P081:StrictMode 的检查与版本边界JavaScript 函数传参:值传递与对象引用前端面试 P018:手写柯里化与参数个数边界前端面试 P010:普通函数、箭头函数与 this前端面试 P088:Immer 草稿与结构共享前端面试 P006:对象遍历与属性键的选择前端面试 P009:执行上下文、调用栈与词法作用域前端面试 P094:SSE 分片解码与事件边界前端面试 P097:请求并发池、任务启动与失败释放前端面试 P076:Fiber 的协作调度与不可中断边界前端面试 P013:节流、防抖与取消待执行任务前端面试 P033:Object.create、描述符与共享属性前端面试 P057:Vue Router 配置、历史模式与导航前端面试 P030:构造函数与 new 的返回规则前端面试 P077:key、组件身份与状态重置前端面试 P039:同源策略、CORS 与请求是否发出前端面试 P032:instanceof、原型链与自定义判断前端面试 P047:Proxy 与 Vue 2 属性拦截的区别前端面试 P019:函数组合与同步数据管道前端面试 P004:手写深拷贝、循环引用与实现边界前端面试 P059:导航守卫、顺序与权限边界前端面试 P083:函数式更新、队列与纯度前端面试 P014:call、apply、bind 与模拟实现边界前端面试 P012:高阶函数、柯里化与部分应用前端面试 P041:事件委托、目标匹配与 React 事件前端面试 P048:track、trigger、动态依赖与清理前端面试 P066:自定义指令与 DOM 资源清理前端面试 P075:props 只读、状态归属与派生数据前端面试 P062:Pinia 创建、实例范围与使用前端面试 P073:JSX 编译、React 元素与表达式前端面试 P026:Promise 延迟与顺序任务队列前端面试 P016:纯函数、副作用与参数共享前端面试 P094:流式文本的 SSE 与 WebSocket 取舍前端面试 P064:storeToRefs 与 action 解构纠错前端面试 P044:Composition API 与组合式函数前端面试 P051:副作用清理与过期请求前端面试 P021:事件循环、微任务与渲染机会前端面试 P003:原始值、对象共享与赋值行为前端面试 P098:用户偏好存储、TTL 与命名空间前端面试 P056:插槽作用域与 slot props前端面试 P011:闭包、变量绑定与循环回调前端面试 P027:async 错误处理与 return await前端面试 P036:CSP、nonce 与防护边界前端面试 P007:隐式转换、ToPrimitive 与空数组比较前端面试 P035:继承方式对照与寄生组合式继承前端面试 P025:手写 Promise 的最小状态机前端面试 P079:受控表单与非受控输入前端面试 P072:虚拟 DOM 的价值与性能边界前端面试 P005:数组方法、可变性与回调副作用前端面试 P100:Proxy 沙箱为何不是安全边界前端面试 P040:发布订阅、事件总线与退订前端面试 P053:props、组件事件与 v-model 约定前端面试 P008:相等比较、NaN 与 Object.is前端面试 P065:Pinia 最小变更与对象替换的取舍前端面试 P087:Redux 单向数据流与状态边界前端面试 P002:数据类型与 typeof 的陷阱前端面试 P085:useReducer 与 useState 的实际取舍前端面试 P078:React 合成事件与旧事件池纠错前端面试 P025:Promise 解析过程与实现验收前端面试 P043:Vue 渐进式与声明式 UI前端面试 P091:SWR、TanStack Query 与服务端缓存前端面试 P074:函数组件快照与类实例的输出纠错前端面试 P071:UI 是状态映射与渲染纯度前端面试 P054:provide、inject 与跨层级依赖前端面试 P061:何时引入全局状态管理前端面试 P097:请求去重、缓存键与过期策略前端面试 P086:Context 拆分与订阅更新边界前端面试 P024:Promise 并发组合与空输入边界前端面试 P089:Redux middleware、next 与异步返回值前端面试 P022:Promise 状态、链式调用与错误传播前端面试 P084:状态提升、单一来源与范围选择前端面试 P034:class、super 与语法糖的边界前端面试 P058:路由参数、query 与组件 props前端面试 P020:定时器延迟、轮询重叠与清理前端面试 P029:对象原型、prototype 与属性查找前端面试 P095:AI SDK、编排框架与生成 UI 工具前端面试 P092:Signals、细粒度依赖与 React 集成边界前端面试 P046:Vue 响应式陷阱与 ref 自动解包纠错前端面试 P090:Zustand、Jotai 与状态作用域前端面试 P100:微前端 JS 隔离方案与资源生命周期前端面试课程:学习目录与纠错记录前端面试 P023:async、await、异步恢复与并发前端面试 P068:Vue 2 与 Vue 3 的列表 diff 区别前端面试 P015:IIFE、作用域隔离与现代模块前端面试 P063:Pinia 状态、getter 与 action前端面试 P052:Vue 条件、列表与表单指令前端面试 P050:Vue 生命周期、DOM 时机与 SSR前端面试 P060:路由 meta、合并与界面配置前端面试 P045:ref、reactive、computed 与 watch前端面试 P096:令牌无感刷新、重试与安全边界前端面试 P093:MCP 架构与前端集成场景前端面试 P080:组合、children 与自定义 Hook前端面试 P082:useState 更新请求、快照与批处理前端面试 P067:Vue 模板编译与运行时边界前端面试 P031:手写 new 与 Reflect.construct 的区别前端面试 P069:nextTick 等待 DOM 刷新与绘制的区别前端面试 P055:默认插槽、具名插槽与内容接口前端面试 P038:嵌套定时器的最小延迟与调度选择前端面试 P099:倒计时截止时间、漂移与完成事件前端面试 P019:记忆化缓存与递归调用的纠错前端面试 P049:Vue 与 React 的更新范围和批处理前端面试 P004:浅拷贝、深拷贝与 structuredClone前端面试 P042:Vue 面试导学与学习顺序前端面试 P096:共享刷新 Promise 与并发失败恢复前端面试 P070:React 面试导学与复习主线前端面试 P028:异步输出顺序的逐步推演前端面试 P001:var、let、const 与暂时性死区前端面试 P037:内存泄漏的快照与引用链排查前端面试 P017:手写 bind、重复绑定与构造调用你好,這裡是 Henry 的自留地給想法找一個落腳的地方,也給那些解決過的問題留下一點記錄。
Esc 關閉
全部文章

Vue 响应式底层原理:Proxy 与视图更新(P6)

目前顯示中文(中國)原文

来源:程序员方格《Vue 面试》P6:Vue 响应式底层原理——Proxy,约 10 分 17 秒。根据本集课件画面与代码演示整理;技术补充另行标明。

核心结论

Proxy 解决“如何拦截对象操作”,依赖追踪解决“数据变化后通知谁”,视图更新由相关副作用和调度机制完成。

本集从手动更新 DOM 开始,对比 Proxy 与 Object.defineProperty,演示读取、赋值、数组操作,最后把 set 拦截与计数器的 DOM 更新连接起来。下一集继续讲 track、trigger 与依赖追踪。

1. 为什么需要响应式

手动更新页面时,开发者需要在每个改变数据的位置安排 DOM 更新。数据与视图的联系分散在事件处理函数中,容易出现“数据改了,某处显示没有更新”的问题。

响应式系统把这层联系统一管理:读取数据时建立依赖,数据变化时通知依赖它的工作,再执行相应更新。开发者主要描述状态和视图之间的关系。

不过,JavaScript 提供的 Proxy 只是实现这套机制的一项工具。单独创建一个代理对象,并不会让浏览器自动更新 DOM。

2. Proxy 拦截的是对象操作

const raw = { count: 0 };
const state = new Proxy(raw, {
  get(target, key, receiver) {
    console.log("get", key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log("set", key, value);
    return Reflect.set(target, key, value, receiver);
  },
});

state.count;    // get count
state.count++;  // get count,然后 set count 1

new Proxy(target, handler) 返回一个新的代理对象。handler 中的方法称为 trap,用于拦截对应的内部操作。

操作 对应的拦截入口
state.count get
state.count = 1 set
state.count++ 先读取,再写入,因此会涉及 get 和 set
delete state.count deleteProperty
"count" in state has
Object.keys(state) 等枚举操作 涉及 ownKeys 和属性描述符查询

补充:只实现 get、set,并不能记录删除或枚举操作。 没有提供的 trap 通常会按目标对象的默认行为转发;要建立相应的响应式依赖,还需要实现对应逻辑。

为什么示例使用 Reflect

视频中的基础示例用 target[key] 读写目标。上面的整理版改用 Reflect.get、Reflect.set,以保留常规属性访问语义,并传递 receiver。

receiver 与访问器中的 this、原型链上的读写有关。在存在 getter、setter 或继承关系时,直接访问 target[key] 与通过代理访问,行为可能不同。

set trap 应返回表示操作是否成功的布尔值。返回 Reflect.set(...) 的结果,可以保留写入失败的语义;不能把无条件 return true 当成通用实现。

3. 与 Object.defineProperty 的区别

视频用“Proxy 拦截行为,defineProperty 拦截属性”帮助记忆。更准确地说,Object.defineProperty 用来定义或修改某一个属性的描述符,而 Proxy 在对象外层提供操作拦截入口。

比较项 Vue 2 常见的属性访问器方案 Vue 3 的 Proxy 对象代理方案
拦截方式 遍历属性,逐个设置 getter、setter 创建代理,通过 trap 拦截相关操作
普通属性读写 经过已转换属性的 getter、setter 通过代理访问时进入 get、set
新增属性 初始化时没有对应访问器,通常需要 Vue.set 等补充机制 通过代理赋值可进入 set,再由框架识别新增与更新
删除属性 需要 Vue.delete 等补充机制通知依赖 可在 deleteProperty 中处理
数组变化 Vue 2 对部分变更方法做了包装,直接改下标或长度存在限制 可拦截索引和显式长度赋值,框架仍需处理相关依赖
是否自动更新视图 还需要依赖管理与更新机制 同样需要依赖管理与更新机制

技术校正: Object.defineProperty 本身可以定义新属性,也可以为数组的某个索引定义访问器。“不能新增属性”“不能监听数组下标”不是这项 JavaScript API 的绝对能力限制;面试中应说明,讨论的是 Vue 2 既有观察方案的限制。

4. 数组:看清显式操作与隐式变化

视频演示通过 Proxy 观察数组下标赋值和 push:

const list = new Proxy([0], {
  set(target, key, value, receiver) {
    console.log("set", key, value);
    return Reflect.set(target, key, value, receiver);
  },
});

list[0] = 1; // set "0" 1
list.push(2); // set "1" 2,然后 set "length" 2

需要补充一个重要边界:

list[5] = 9; // set "5" 9
console.log(list.length); // 6

写入越界索引会让数组长度随之增长,但这个隐式变化不会额外产生一次 set "length" 调用。因此,不能仅靠“有没有打印 length”判断数组长度是否改变。

完整的响应式系统需要结合索引写入、旧长度和新长度,正确通知依赖数组长度、索引或遍历结果的副作用。

5. 把拦截与视图更新连起来

下面沿用视频的计数器思路,将状态写入与 DOM 更新连接起来。这是一个平面状态、单一视图的演示:

<p id="count">0</p>
<button id="inc">+1</button>
<script>
  const output = document.querySelector("#count");
  const raw = { count: 0 };
  const state = new Proxy(raw, {
    get(target, key, receiver) {
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const previous = Reflect.get(target, key, receiver);
      const success = Reflect.set(target, key, value, receiver);
      if (success && key === "count" && !Object.is(previous, value)) {
        output.textContent = String(state.count);
      }
      return success;
    },
  });

  output.textContent = String(state.count);
  document.querySelector("#inc").addEventListener("click", () => {
    state.count++;
  });
</script>

点击按钮后,count++ 先读取再写入,写入时执行我们安排的 DOM 更新。页面改变的原因,是开发者在 set 中写了更新逻辑。

这个例子没有实现依赖收集、多视图订阅、任务调度、深层代理和依赖清理,不能当作 Vue 响应式源码。它用于说明:拦截变化以后,还必须决定执行什么更新。

6. 从演示走向 Vue 响应式,还要补齐什么

以下是结合 Vue 文档的补充,用来明确本集知识的适用范围:

  • 依赖追踪: 在副作用执行并读取属性时,记录“目标对象、属性与当前副作用”的关系;写入时找到相关依赖,再按更新策略调度。
  • 深层对象: new Proxy(raw, handler) 不会自动把所有嵌套对象都代理。Vue 的深层响应式还需要在读取嵌套对象时返回相应代理,并通过缓存保持代理身份稳定。
  • 访问入口: 直接执行 raw.count = 2 会绕过上面代理的 set。使用 Vue 响应式对象时,应通过相应的代理或响应式 API 修改状态。
  • ref: Vue 3 的响应式机制不只有 Proxy。ref 的 .value 使用 getter、setter 参与依赖追踪与触发;不能把“Vue 3 响应式”全部等同于 Proxy。

面试回答示例

Vue 3 使用 Proxy 代理对象,通过 get、set,以及删除、枚举等相关拦截入口感知对象操作。相比 Vue 2 逐个属性转换 getter、setter 的方案,它更便于覆盖新增属性、删除属性和数组索引变化。但 Proxy 本身不负责自动更新页面。Vue 还需要在读取时收集依赖,在写入时找到并调度相关副作用,最终完成视图更新。还要注意,直接修改原始对象会绕过代理,普通 Proxy 也不会自动产生深层响应式。

关联笔记

参考资料

看看其他文章 →