问题:
- 数据初始为
undefined或null,v-if="data"为false,DOM 不渲染。 - 数据后来通过非响应式方式 被赋值(如直接
this.data = xxx但data不在data()中声明)。
1.示例:
vue
<template>
<div>
<!-- 第一次渲染时 list 是 undefined,v-if 为 false,p 标签不会进入 DOM -->
<p v-if="list">第一项:{{ list[0] }}</p>
</div>
</template>
<script>
export default {
// 1. 没有声明 list → 非响应式
data() {
return {}; // 这里缺了 list
},
mounted() {
// 2. 异步拿到数据
setTimeout(() => {
this.list = ['a', 'b']; // 3. 直接挂一个新属性
}, 1000);
},
};
</script>
2. Vue2 响应式核心(Object.defineProperty)
- 初始化阶段
在new Vue()→_init()→initState()→initData()时,会遍历data()返回对象的 key ,用Object.defineProperty做 getter/setter 劫持 。
只有此刻已存在的属性 才能被追踪;后续动态新增的属性默认不会被侦测。 - 依赖收集
当渲染函数执行到v-if="list"时,如果list已经是响应式属性,会触发getter,把当前的 渲染 watcher 收集到Dep中;将来list变化时触发setter→dep.notify()→ 重新渲染。
如果list压根儿就不是响应式,渲染 watcher 不会被收集 ,后续你哪怕改成this.list = xxx也不会触发任何更新。 - 数组/对象新增属性
Vue2 对数组的 索引 和 长度 以及 对象新增属性 都无能为力 (性能权衡)。
官方提供Vue.set/vm.$set作为补丁。
3. 生命周期与挂载时机(Vue2)
kotlin
beforeCreate → initState 已执行完,data 已完成响应式转换
created → 此时可访问响应式数据,但 DOM 尚未生成
beforeMount → 模板已编译成 render 函数
mounted → 真实 DOM 已插入页面,且 第一次渲染 watcher 已执行完毕
关键点:
- mounted 之后,渲染 watcher 已经执行过一次;
- 如果第一次执行时
list不存在,那么v-if="list"计算为false,不会把<p>加入 VNode 树 ,也不会对list做依赖收集; - 后续你再加
this.list = xxx,由于 没有收集到依赖 ,Vue 认为"没人关心它",于是不会触发重新渲染。
4. 常见"踩坑"场景(Vue2)
| 场景 | 触发条件 | 结果 |
|---|---|---|
| 1. data 里漏声明 | data(){ return {} } |
新增属性非响应式 |
| 2. 异步赋值 | axios 回调里 this.list = res |
同上 |
| 3. 对象嵌套新增 | this.obj.newKey = x |
需要 Vue.set |
| 4. 数组索引赋值 | this.arr[0] = x |
需要 Vue.set 或 splice |
| 5. JSON 解析后直接替换 | this.data = JSON.parse(str) |
只要原 key 已声明就安全,否则一样失效 |
| 6. v-if 条件里拼错字段名 | v-if="lis" |
始终 false,且不会报错 |
5. 源码级:为什么不会触发
- 渲染 watcher 的执行栈
mountComponent()→new Watcher(vm, updateComponent, noop, {}, true /* isRenderWatcher */)
updateComponent = () => vm._update(vm._render(), hydrating)
第一次_render()执行时,访问list为undefined,不会进入 getter ,因此 Dep.target(当前渲染 watcher)不会被收集。 - 后续赋值
你执行this.list = ['a','b']时,只是普通属性赋值,没有 setter 通知 ,dep.notify()永远不会调用,组件不会重新进入 patch 阶段。
6. 现场定位技巧(Vue2)
- Vue-DevTools
打开组件面板,看list字段是否带 Getter/Setter 图标;如果图标缺失 → 非响应式。 - 控制台打印
console.log(this.$data)看是否包含list字段;没有 → 漏声明。 - 强制刷新
在赋值后手动this.$forceUpdate(),如果页面立刻变化 → 100% 非响应式问题。
7. 正确写法(Vue2 最佳实践)
vue
<template>
<div>
<p v-if="list && list.length">第一项:{{ list[0] }}</p>
<button @click="load">加载</button>
</div>
</template>
<script>
export default {
data() {
return {
// 1. 先声明,哪怕是空数组/null
list: null,
};
},
methods: {
load() {
// 2. 异步拿到数据
setTimeout(() => {
// 3. 直接替换即可,响应式已建立
this.list = ['a', 'b'];
}, 500);
},
},
};
</script>
补充:
- 如果一开始不知道结构 ,可用
Vue.set动态加顶级属性,但更推荐先声明。 - 对于对象嵌套 使用
this.$set(this.obj, 'newKey', value)。 - 对于数组 使用
this.$set(this.arr, index, newVal)或this.arr.splice(index, 1, newVal)。
8. 进阶:万一已经踩坑,如何补救
| 场景 | 补救方案 |
|---|---|
| 1. 漏声明 | 先 this.$set(this, 'list', value) 把顶级属性补成响应式;后续就能正常追踪 |
| 2. 大量动态字段 | 用 Object.assign 前先 this.data = Object.assign({}, this.data, newObj),只要原对象 key 已声明即可 |
| 3. 必须强制刷新 | this.$forceUpdate()(暴力,不推荐长期使用) |
| 4. 监听异步数据 | 用 watch 或 computed 把异步结果映射到已声明的字段 |
9. 总结
Vue2 的响应式只在 初始化 data() 时存在的属性 上生效;
第一次渲染时如果字段不存在,渲染 watcher 不会收集依赖 ;
后续你再怎么 this.xxx = ... 都不会触发更新 ,看起来就像 v-if 失效。
先声明、后赋值,是 Vue2 的铁律。