今天团队内部分享和讨论了 Vue 数据是如何驱动视图的,本次主要讨论 Object 的变化侦测。
- 监听数据变化
- 收集依赖
- 通知依赖更新
监听数据变化
需要监听哪些数据的变化?
根组件data
子组件data 和 props 也就是说只有在 data 和 props 中定义的数据,才能被监听。
什么时候监听数据的变化
创建 Vue 实例的时候,initData阶段, 调用 observe 方法进行劫持数据变化。
function initData (vm: Component) {
let data = vm.$options.data
// observe data
observe(data, true /* asRootData */)
}
如何追踪数据变化
把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property
在 initData 阶段, Vue 会调用 observe 方法,内部会 new Observer(), 返回一个 ob对象, ob 对象是一个被 Object.defineProperty 方法劫持的对象。
Observer构造器中主要是缓存 ob 对象, 把实例缓存在 value._ob_ 中, 如果存在 value._ob_ 属性, 则不会 new Observer ,而是从缓存中获取 ob 实例, 减少重复创建。 这里 vue 用了一个 if 判断, 如果 observe 方法的参数是一个数组类型,则会遍历数组,让数组中的每一项都被 observe 方法调用,这样就可以劫数组中对象了。
observe(data, true /* asRootData */)
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
class Observer {
constructor (value) {
value.__ob__ = this
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
// Vue 将遍历此对象所有的 property, 并绑定 defineReactive
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
使用 Object.defineProperty 把这些 property 全部转为 getter/setter, Vue 内部会通过 getter/setter 进行追踪依赖, 在 property 被修改时和被访问时通知变更。
调用 defineReactive 时, 还会递归遍历此对象下所有的 property, 绑定 defineReactive
function defineReactive (obj,property,val,customSetter,shallow) {
observe(val)
Object.defineProperty(obj, property, {
get() {
return val
},
set(newVal) {
val = newVal
}
})
}
收集依赖
依赖是什么?
谁用到了数据谁就是依赖,数据就是上文 Object.defineProperty 响应式劫持的 data, 依赖就是使用数据的地方。比如 template 模板里面使用的 data 变量。
为什么要收集依赖
数据变化了要更新视图,提前把跟这个数据有关的依赖收集起来,等到数据改变时, 可以从刚才收集里取出依赖,方便更新。
在何时收集依赖?
Object.defineProperty 的 getter 方法中收集依赖, 这里的依赖并不直接是一个视图, 而是一个 Watch 实例, 这个 watch 实例可以去通知视图更新的。
function defineReactive (
obj,property,val
) {
const dep = new Dep()
Object.defineProperty(obj, property, {
get() {
if (Dep.target) {
dep.depend()
}
return value
},
set: function reactiveSetter (newVal) {
val = newVal
dep.notify()
}
})
}
class Dep {
static target;
constructor () {
this.subs = []
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
class Watcher {
constructor (vm,expOrFn){
this.getter = expOrFn
this.newDeps = []
this.get()
}
get() {
Dep.target = this
this.getter.call(vm, vm)
}
addDep (dep) {
this.newDeps.push(dep)
}
}
在 defineReactive 绑定每一个 property 之前, 会创建一个 Dep 实例, subs 里面是订阅的多个 watch, subs是一个数组, 一个 dep 可以订阅多个 watch 实例, 就是说一个数据改变了, 可能会影响多个视图更新。
在 getter 方法里, 有一个 Dep.target 参数, 这个 target 其实就是 watch 实例, 那么 target 从哪里来的呢。 在 beforeMount钩子 和 mounted 钩子初始化之间,会实例化 Watch 类, Watch 构造函数中会把 watch 实例保存在 Dep.target 上, 随后会触发所有数据的访问,也就是上面的 getter 方法,dep.depend() 会把 watch 保存起来, 这个过程就是收集依赖。