vue 双向绑定
vue 双向绑定,问就是 Object.defineProperty 劫持数据获得状态变更,发布订阅者模式进行变更通知。好吧,现在地球人都知道这两句话了,要是别人再问你实现细节,我们应该如何回答呢。也是为了让自己更加了解,从自己大概知道和能与人清楚表达,这就是今天写文章的目的了。
阅读前,你需要具备 vue 生命周期,Object.defineProperty,以及发布订阅者模式(相关博客)的一些知识。
劫持数据
......
callHook(vm, 'beforeCreate')
initInjections(vm) //注入 Data/prop 数据
initProvide(vm) //注入完成
callHook(vm, 'created')
在进入 beforeCreate 周期后,会注入 data/prop 等数据,数据劫持就是发生在此时(initInjections 函数)。
function initInjections(){
......
Object.keys(result).forEach(key => {
defineReactive(vm, key, result[key])
})
}
export function defineReactive( obj: Object, key: string, val: any ......){
......
Object.defineProperty(obj, key, {
get:function(){
......
},
set:function(){
......
}
}
}
把对象的属性键转换为getter/setter,一旦劫持了数据,我们就可以为所欲为了。
但是这里只转换了第一层数据,我们还要嵌套遍历对象。
Observer 监听类
在转化对象属性 defineReactive 函数中,会先 new 一个监听类(Observer)。监听类通过递归数据,把 getter/setter 附加到每个被观察对象(让数据变化绑定订阅和通知这两个行为)。
function observe(value){
let ob
if (hasOwn(value, '__ob__') //通过有无__ob__属性判断是否处于监听中
ob = value.__ob__
else
ob = new Observer(value)
return ob
}
export class Observer{
......
def(value, '__ob__', this) //为属性添加属性__ob__,指向监听实例
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]]) //通过第三个参数递归,继续监听对象
}
}
单单只是获取数据状态变更不行,还得发送通知。
Dep 调度中心类
了解发布订阅者模式就该知道,订阅者(观察者)把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者(目标)发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
我们通过 getter 为每个订阅者订阅事件,setter 来发送通知。
export function defineReactive( obj, key, val ......){
......
const dep = new Dep() //每一个数据都会有一个专门的 Dep 实例。
observe(val) //递归遍历数据
Object.defineProperty(obj, key, {
get:function(){
......
dep.depend() //订阅事件
return value
},
set:function(){
......
dep.notify() //发送通知
}
}
}
在 defineReactive 的 get 里面订阅事件。然后在 set 中发送通知。
export class Dep{
......
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()
}
}
//添加依赖(订阅者)
addSub (sub: Watcher) {
this.subs.push(sub)
}
}
Dep.target 后续会讲到,指向目标 watcher 实例
Watcher 订阅者类
......
callHook(vm, 'beforeMount')
new Watcher()
......
在 beforeMount 生命周期后,会 new 一个 Watcher 对象。
export class Watcher {
......
constructor (){
this.cb = cb
this.get()
}
get(){
......
Dep.target = this; //让当前组件实例的 Dep.target 指向这个 Watcher 对象
this.getter.call(vm, vm) //执行一次 getter
Dep.target = null
return value
}
addDep(dep){ //为调度中心依赖队列添加当前 Watcher 对象
dep.addSub(this)
}
update(){
this.cb.call(this.vm, value, oldValue) //执行 setter
}
}
生成订阅者 watcher,会执行一次数据的 getter(通过监听类递归,监听并让每一个属性都有一个调度中心),然后通过订阅事件,把这个订阅者关联到所有调度中心的依赖内(通过 Dep.target 的指向互相调用)。当数据变化时,该处的调度中心就会通知相应的订阅者。
每个数据都有一个调度中心,每个组件实例都有一个订阅者
总结
实现数据的双向绑定,首先要对数据进行劫持监听,所以需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。我们通过消息调度中心Dep来专门收集这些订阅者并发布消息通知,以此在监听器Observer和订阅者Watcher之间进行统一管理。