Object.defineProperty
我们都知道在 Vue 的 data 中声明的数据是 响应式 的,当被声明的数据被修改之后,Vue 就可以捕捉到这个修改并进行一系列操作,捕捉数据的变化依靠的就是 Object.defineProperty 。
简单的说,Object.defineProperty 可以监听对象上属性的变动:
let person={
name:"xiaoming"
}
Object.defineProperty(person, "name",{
get(){
console.log('get value')
},
set(val){
console.log('set value')
}
})
person.name //get value
person.name = 'xiaohong' //set value
如果我们在 Object.defineProperty 的 set 方法中做操作 dom 的话,那么我们可以实现修改 name 属性的同时让页面跟着变化:
<div id="personName">xiaoming</div>
<input type="text" id="input" value="xiaoming" />
let person = {
name: "xiaoming"
};
let _name = person.name;
Object.defineProperty(person, "name", {
get() {
console.log("get value");
return _name;
},
set(val) {
console.log("set value");
_name = val;
document.getElementById("personName").innerHTML = val;
}
});
document.getElementById("input").addEventListener("input", function(e) {
person.name = e.target.value;
});
上面我们实现了一个简单的数据视图的联动,但是这里只是监听对象的一个属性,我们需要监听对象的所有属性。
observe
要完成所有属性的监听,可以创建一个 observe 函数,然后通过递归循环来完成监听:
function observe (obj){
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key)
})
}
function defineReactive (obj, key) {
let val = obj[key]
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log("get value");
return val;
},
set(newVal) {
console.log("set value");
val = newVal;
}
})
}
Dep
直接在 set 方法中写逻辑是非常不灵活的,我们需要增加一个 Dep 类,用于消息的订阅与发布。
class Dep {
constructor() {
this.subs = []
}
depend(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(fn => {
fn()
})
}
}
Dep.target = null
我们在 get 的时候,将 set 时需要做的事情,放到订阅数组中,然后在 set 时发布这个订阅。
function defineReactive (obj, key) {
let val = obj[key]
observe(val)
let dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 收集依赖,加入subs数组
if (Dep.target) {
dep.depend(Dep.target)
}
return val;
},
set(newVal) {
val = newVal;
// 触发依赖,取出subs数组并执行
dep.notify()
}
})
}
现在假设我们想在 person.name 被修改时触发一些操作,那么我们可以将这些操作写成一个函数赋值给 Dep.target,然后触发该属性的 get,就可以在 set 时触发这个方法,如下:
let person = {
name:'xiaoming',
age:'18'
}
observe(person)
Dep.target = function(){
console.log(person.name)
}
person.name
person.name = 'xiaohong'
Watcher
现在我们必须每次强行触发属性的 get 才可以将需要触发的依赖收集到 subs 数组,我们可以创建一个 Watcher 类来处理
class Watcher {
constructor(obj, key, callback) {
Dep.target = this
this.obj = obj
this.key = key
this.value = obj[key]
this.callback = callback
Dep.target = null
}
update() {
this.callback(this.obj[this.key])
}
}
// 修改 Dep 的 notify 方法
class Dep {
constructor() {
this.subs = []
}
depend(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
let person = {
name:'xiaoming',
age:'18'
}
observe(person)
new Watcher(person, 'name', function(newValue){
console.log(newValue)
// Vnode 对比逻辑
})
person.name = 'xiaohong'
Watcher 何时初始化
Watcher 函数在 Vue 中什么时候初始化呢?
在 Vue 中,template 最终会被编译成 render 函数, 在 render 函数执行时会触发 new Watcher 完成数据的 getter ,具体在 src/core/instance/lifecycle.js 中。
官网中的图也描述了这个过程:
最后
最后来总结整体监听数据与依赖收集的过程:
首先进行 observe() 监听数据,当监听结束后,此时 person 的属性已经拥有了 get 和 set 方法,每个属性的中都通过闭包引用了一个 dep 实例。
然后调用 Watcher 构造函数,通过 this.value = obj[key] 赋值。触发了属性的 get 方法。
在 get 方法中,将当前的 Watcher 实例添加到 dep 实例的 subs 中。
最后对 person.name 进行赋值,触发 person.name 的 set 方法,调用 dep 实例上的 notify 方法,触发 subs 中的 Watcher 实例的 update 方法。