开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
背景
Vue 我们在日常生活中已经使用非常多了,和传统开发不同的是,Vue 是数据驱动视图的。只要我们修改了 data中的数据,相应的视图就会自动更新,而不用我们手动更新了,这也就是我们常说的响应式。下面我们就用几分钟的时间来看看 Vue2 中实现响应式的基本原理。
如何实现响应式更新
Vue 中实现响应式更新(追踪数据变化,再进行对应视图的更新)主要分为2步:
- 将普通对象变为可追踪的数据
- 组件实例记录使用到的数据,数据变更时再通知组件进行更新
一、将普通对象变为可追踪的数据
在Vue中比较常见的响应式数据有2种:
data中初始化的数据- 通过
vue.set()动态添加的数据。
这2种方式在内部实现时都是调用的 Vue.observable()函数。这个函数通过遍历数据对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。让一个普通对象变为可追踪的数据。
二、组件实例绑定数据
在 Vue 中,每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把 "接触" 过(getter)的数据 property 记录为依赖(依赖收集)。之后当依赖项数据改变时,数据对应的 setter 触发(数据变更),会通知 watcher,从而使它关联的组件重新渲染(派发更新)。
大概流程如下图:
可以看到,先通过将数据 getter/setter化,让数据的使用和赋值都变得可追踪;然后再通过 Vue 实例对数据的使用, 依赖收集-数据变更-派发更新 的过程来实现数据变更时,更新对应视图的效果。
这里我们只介绍了大方向上的原理,所以细节并没有涉及到。如果大家想了解更多关于Vue开发相关的问题,可以看看这篇文章。
检测变化的注意事项
对象
Vue 无法检测到对象属性的添加或删除。因为 Vue 会在实例初始化时对 data 对象属性执行 getter/setter 转化,所以初始化时 data 对象中存在数据才为响应式数据。那如果我们想要给对象添加新的属性并更新视图,就需要使用 Vue.set。
- 直接通过赋值添加新属性不生效(数据变更时不会更新视图) ❌
this.someObject.b = 2
- 向嵌套对象添加响应式 property ✅
Vue.set(vm.someObject, 'b', 2)
this.$set(this.someObject,'b',2)
- 为已有对象赋值多个新 property ✅
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
数组
- 不能检测以下数组的变动
-
利用索引直接设置一个数组项时。例如:
直接赋值,非响应式的 ❌
vm.items[index] = newValue响应式赋值 ✅
// 1.Vue.set Vue.set(vm.items, index, newValue) // 2.Array.prototype.splice vm.items.splice(index, 1, newValue) -
修改数组的长度时,例如:
// 非响应式的 ❌ vm.items.length = newLength // 响应式的 ✅ vm.items.splice(newLength)
不允许动态添加根级响应式数据
必须在初始化实例前声明所有根级响应式数据,哪怕只是一个空值。
在部分比较特殊的场景中,通过
vue.set()设置数据,可能也会出现失效的情况。vue.set 失败场景
异步更新队列
Vue 在更新 DOM 时是 异步执行 的。
- 只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个
watcher被多次触发,只会被推入到队列中一次。 - 然后,在下一个的事件循环
tick中, Vue 刷新队列并执行实际 (已去重的) 工作。
Vue 在内部对异步队列尝试使用原生的
Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替
例如,当你设置 this.someData = 'new value',该组件不会立即重新渲染。如果想要基于更新后的 DOM 状态做些什么,可以使用 Vue.nextTick(callback)
这也是为什么如果要获取更新后的 DOM,通常在 Vue.nextTick() 回调中处理。
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: '未更新'
}
},
methods: {
updateMessage: function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新'
})
}
}
})
总结
通过这篇文章我们了解到了 Vue2 实现数据响应式的基本原理,以及实现数据响应式的常见问题。因为只是介绍了大方向上的原理,所以细节并没有涉及到。如果大家想了解更多关于Vue开发相关的问题,可以看看这篇文章。