问题:
- 数据初始为
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 的铁律。