这篇文章并不会一行一行的分析 vue 的源码,已经有很多文章都写过了,我会一步一步的讲清楚 vue 数据绑定的原理。
defineProperty
基本上每个人都知道 vue 是使用了 defineProperty 将属性转化为 getter/setter 来监听数据的改变。
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
console.log('invoke getter:' + val)
return val
},
set: function(newVal) {
if (newVal === val) return
console.log('invoke setter:' + newVal)
val = newVal
},
})
}
var a = {b: 1}
defineReactive(a,'b',1)
a.b // invoke getter:1
a.b = 2 // invoke setter:2
可以看到,当获取 b 的值时,会触发 getter,给 b 赋值时,会触发 setter,这样,每次当我们写 this.b = 3 时,在 setter 里写上需要处理的动作,就可以达到响应式的目的。
通过这种方式,将 props 和 data 变成响应式的,但是这只是这两者的值变了,依赖它们的地方如何跟着改变呢?
为了达到这个目的,需要使用 Dep 类,在该类上定义了两个原型方法,用于向 subs 增加订阅者和通知订阅者更新。
function Dep() {
this.subs = []
}
Dep.prototype.depend = function () {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
Dep.prototype.notify = function () {
// 先备份,避免 subs 改变
const subs = this.subs.slice()
subs.forEach(sub => {
sub.update()
})
}
这时,修改一下上面的 defineReactive 方法,给每个属性里增加一个 dep 实例,用于收集依赖,当有其它的变量调用了该属性的值时,就会在 subs 里增加一个订阅者,当该属性的值发生改变时,就会在 setter 里调用 notify,通知订阅者更新。
function defineReactive(data, key, val) {
let dep = new Dep() // 新增
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
console.log('invoke getter:' + val)
dep.depend() // 增加订阅者
return val
},
set: function(newVal) {
if (newVal === val) return
console.log('invoke setter:' + newVal)
dep.notify() // 通知更新
val = newVal
},
})
}
那么问题来了,谁是订阅者,如何加订阅者添加到 subs 中?这时我们需要一个 Watcher 类
function Watcher(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
this.value = this.get()
}
Watcher.prototype.update = function() {
let oldVal = this.value
let newVal = this.get()
if (oldVal !== newVal) {
this.value = newVal
this.cb.call(this.vm, newVal, oldVal)
}
}
Watcher.prototype.get = function () {
Dep.target = this
let value = this.vm[this.exp]
Dep.target = null
return value
}
在 Vue 里,有三处实例化了这个类,分别是使用 computed、watch和模板渲染的时候,这个也很容易理解,因为只有这三处依赖了其它数据的变化而变化,因此需要使用 Sub 来收集 Watcher,当数据发生变化时,会执行 Sub 里的每一个 Watcher 的 update 方法,update 里执行回调。
举例来说明:
vm.$watch('a', function (newVal, oldVal) {
// do something
})
这里 watch 了变量a,vue 里会 new 一个watcher对象 var watcher = new Watcher(vm, expOrFn, cb, options); 此时 expOrFn = 'a',cb为里面的回调函数。此时 Watcher 类里会首先执行 this.get(),即首先获取一次 data.a 的值(第19行),此时触发了 a 的 getter,上面分析到,会在 getter 里收集依赖,这里将 target 设为 this,即当前的 watcher 对象,getter 里会读取target,将该 watcher 对象推进 subs,这样就完成了依赖收集。接下来如果 a 发生变化,就会执行 setter 里的 dep.notify,会执行每一个watcher里的update,即执行了上面的cb。在模板渲染的时候也是类似,只不过cb换成了更新视图的方法,即
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
上面介绍了数据绑定的三个比较重要的点,接着就来总结一下整个流程
- 当 new Vue 对象的时候,options 里传入 data、computed、watch、template 等等
- 使用
defineReactive处理 data 里的每一项,增加 getter 和 setter,检测到变量的变化 - 处理 computed 和 watch 选项,在对应的依赖变量的 sub 里注入 watcher,这些变量发生改变的时候,执行 update 方法,触发回调函数
- 解析模板,转化为 render 函数,构造 updateComponent 方法,然后 new 一个 Watcher 对象,执行该方法时,给使用到的变量注入依赖,这样变量更新的时候就触发 updateComponent 方法,更新视图