在 Vue 中,data 对象是响应式的,即对 data 中属性的修改会触发视图更新,今天就来分析一下这是如何做到的。
首先定义一个普通的 JS 对象,例如:{ name: 'keliq' },我们希望当 name 发生变化时,能收到通知。
在 Vue 当中,有三个非常重要的概念:Observer、Dep 和 Watcher,它们的功能是:
- Observer:把普通对象变成响应式对象,实现原理就是递归地对属性进行劫持,在 get 的时候把依赖收集到 dep.subs 里面,从而在 set 的时候能够逐个通知
- Dep:用于存放 Observer 收集到的依赖
- Watcher:创建观察者,即订阅响应式通知,当目标发生变化时执行回调
Dep
Dep 用于收集依赖和通知更新。
class Dep {
constructor() {
this.subs = []
}
depend() {
if(Dep.target) this.subs.push(Dep.target) // 收集依赖
}
notify(newVal, val) {
this.subs.forEach((sub) => sub.update(newVal, val)) // 通知依赖
}
}
可以看到,依赖被赋值给了 Dep.target ,每次收集的时候只需要把 Dep.target 存入 subs 中即可。
Observer
Observer 会对 obj 中的属性进行拦截,在 get 中收集依赖,在 set 中通知更新。
class Observer {
static observe(value) {
if (typeof value !== 'object' || value == null) return
return new Observer(value)
}
constructor(value) {
this.value = value
this.walk(value)
}
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
this.intercept(obj, keys[i], obj[keys[i]]) // 劫持对象所有属性
}
}
intercept(data, key, val) {
Observer.observe(val) // 递归响应式
const dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend()
return val
},
set: function (newVal) {
if (val === newVal) return
dep.notify(newVal, val) // 通知观察者
val = newVal
},
})
}
}
walk 的目的是对 obj 的所有属性进行递归响应式处理,也就是说如果 obj 的属性还是一个对象的话,这个对象属性的变更也是能检测出来的。
Watcher
Watcher 是观察者,其中 update 方法接收属性变化时的新旧值,然后执行回调函数,get 方法用于把自己放到观察属性的依赖列表中。
class Watcher {
constructor(obj, expOrFn, cb) {
this.obj = obj
if(typeof expOrFn === 'function') this.getter = expOrFn
else this.getter = obj => expOrFn.split('.').reduce((it, k) => it && it[k], obj) // 取 obj.a.b.c.d 这种深层的属性
this.cb = cb
this.value = this.get()
}
get() {
Dep.target = this
const value = this.getter(this.obj) // 调用 getter 取属性值
Dep.target = undefined
return value
}
update(newVal, val) {
this.cb.call(this.obj, newVal, val) // 调用 cb 函数
}
}
当 new Watcher 的时候,传递三个参数:
- 观测哪个对象
- 观测对象的哪个属性(或自定义观测逻辑)
- 该属性发生变化时的回调函数
到这里代码就写完了,快来试试效果吧,大家把控制台打开,然后把上面的 Dep、Observer 和 Watcher 类复制进去,然后再运行下面代码:
const obj = { name: 'keliq', age: 12 }
Observer.observe(obj)
new Watcher(obj, 'name', (newVal, val) =>
console.log(`name发生变化:${val}->${newVal}`)
)
obj.name = 'qiao'
控制台能打印出 keliq->qiao 的同学请举爪🙋。如果 Watcher 的第二个参数是个函数,会更灵活一些:
new Watcher(
obj,
function () {
obj.name
obj.age
},
function (newVal, val) {
console.log(`发生变化:${val}->${newVal}`)
}
)
obj.name = 'qiao'
obj.age = 15
最后给出 Vue 响应式原理中「对象属性劫持」的整体流程图: