Skip to content

Vue 2:响应式原理、data 设计、丢失响应式与 props

一、为何组件里 data 必须是函数

现象

  • 根实例data 可以是对象。
  • 组件data 必须是返回对象的函数,否则运行时会报错。

原因:避免状态共享

组件会被多次实例化(列表、重复标签)。若 data同一个对象引用,所有实例共享这份数据,一改全改。

写成 data() { return { ... } } 时,每次创建实例都会执行函数并得到新对象,实例间数据隔离。

export、引用的关系

  • 模块里 export 普通对象:多处 import 可能共享同一引用。
  • 函数每次返回新对象字面量:引用不同,状态不串联。
  • Vue 3 Composable 同理:函数内创建的状态每次调用独立。

表述纠正

旧稿问答中有「钓鱼这个函数」应为笔误,语义指 调用函数返回新对象


二、响应式原理精讲(保留结构与代码骨架)

简述

初始化时递归遍历 data,用 Object.defineProperty 把属性改成访问器;getter 做依赖收集,setter 通知 Dep → Watcher 更新视图。

阶段归纳

  1. 初始化 observe:遍历 data,为属性建立 getter/setter,并为属性挂 Dep
  2. 首次渲染 / Watcher:渲染函数读取数据 → 触发 getter → 把当前 Watcher 记到对应 Dep。
  3. 修改数据:setter → dep.notify() → Watcher 异步队列批量刷新(与 $nextTick 机制衔接)。
  4. 再次渲染:新 VNode 与旧 VNode patch(详见虚拟 DOM 章节)。

代码示例

文档中原有一段简化 Dep / Watcher / defineReactive / observe 示例,用于理解 getter/setter 与依赖队列;其核心与 Vue 真实实现一致的是「访问收集、修改通知」,队列调度与对象嵌套处理真实源码会更复杂。

局限性(再次归纳)

  • 监听不到 新增/删除属性(需 $set/$delete 或整体替换)。
  • 数组下标直接赋值等场景需借助封装过的数组方法或 $set

getter/setter 命名

Object.defineProperty 的配置项固定为 get / set,不能随意改名;这与 ES 访问器属性语义一致。


三、丢失响应式:场景与处理

  1. 对象新增属性this.$set(obj, key, val)this.obj = Object.assign({}, this.obj, { ... })
  2. 对象删除属性this.$delete(obj, key)
  3. 数组按下标改this.$set(arr, i, val)splice
  4. 改数组长度:如 splice 清空等。
  5. 未在 data 声明就使用:应先在 data 里声明字段。

四、获取 data 初始快照

  1. const initialData = this.$options.data.call(this)
  2. 或在模块级保存常量模板,data()JSON.parse(JSON.stringify(originalData))(注意结构化克隆局限)。

五、父改 props 子组件「不更新」?

场景特征

常为 对象 props,只改内部字段而未替换引用;Vue 2 对 props 的检测方式可能导致子组件侧未按预期触发更新。

处理思路

  • 父:this.$set(this.obj, 'key', val),或展开运算符生成新对象引用赋回。
  • 子:用 computed 派生展示值;或对 props deep watch

说明

Vue 3 中 props 与响应式实现不同,此类现象较少以同样形式出现,但仍应遵循单向数据流


六、能否在子组件里改 props?

不推荐直接改 props,违背单向数据流;若必须「本地可编辑副本」:

  1. 拷贝到 data 作初始值(与父后续变更是否同步需自行 watch)。
  2. computed + getter/setter,setter 里 $emit('input', val)(Vue 2)实现受控组件。
  3. 引用类型 props:改嵌套属性技术上可能影响父对象,但不利于追溯,团队规范通常禁止。

文档中 Vue 3 defineProps 片段属跨版本示例,若仍在 Vue 2 项目请用选项式 API 写法。


七、小结

  • 响应式Object.defineProperty + Dep/Watcher + 异步更新队列。
  • data 函数:保证多实例隔离
  • 边界:增删属性、数组下标、props 深层变更 —— 用 API 或模式规避。

下一章:模板与指令