前言
上一遍文章介绍了Vue模版渲染的实现(https://segmentfault.com/a/11…),这篇文章将继续介绍双向绑定的实现
demo
官网demo如下,当data。message的值变化,input的value值也会相应的变化;当用户改变input框中的内容时data.message的值也会跟着改变
<div id="app"></div>
new Vue({
el: '#app',
template:
`<div>
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
</div>`,
data(){
return {
message: 'jixiangwu',
}
}
})
ViewModel变化 -> View更新
当数据变化时,视图会直接更新,在本例中当data.message改变时,dom中绑定了data.message的视图都会更新
上一篇文章中介绍过,new Vue的过程中会将template字符串转换成render函数,render函数执行后会得到vnode对象(虚拟dom),在调用_update方法会将虚拟dom更新为真实的浏览器dom,代码如下:
updateComponent = function () {
//vm._render()生成vnode对象,vm._update()更新dom
vm._update(vm._render(), hydrating);
};
//对vue实例新建一个Watcher监听对象,每当vm.data数据有变化,Watcher监听到后负责调用updateComponent进行dom更新
vm._watcher = new Watcher(vm, updateComponent, noop);
updateComponent方法在Watcher初始化时会调用一次,后续的调用就涉及到MVVM的机制了,让我们从头开始分析
Vue初始化时会对data中的所有属性进行observe,调用defineReactive方法,将data属性转化为getter/setters存取方式。本文demo中的data={message:“jixiangwu”}相当于如下的调用:defineReactive(vm.data,’message’,vm.data[‘message’])
//vue对象的生命周期中会调用initData方法
function initData (vm) {
var data = vm.$options.data;
observe(data, true /* asRootData */);
}
function observe (value, asRootData) {
ob = new Observer(value);
}
//对data进行监听
var Observer = function Observer (value) {
if (Array.isArray(value)) {
this.observeArray(value);
} else {
this.walk(value);
}
}
//对data中的所有属性调用defineReactive,将其转化为getter/setters存取方式
//Walk through each property and convert them into getter/setters.
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]]);
}
};
function defineReactive(obj,key,val){
//利用闭包为每个属性绑定一个dep对象(可视为发布者,负责发布属性是否有变化)
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//每次new一个watcher(订阅者)对象的时候需要计算依赖的dep对象,Dep.target就是当前正在计算依赖的watcher对象
if (Dep.target) {
//调用属性的getter方法时,存在Dep.target则将当前dep和watcher绑定
dep.depend();
}
},
set: function reactiveSetter (newVal) {
//调用属性的setter方法时,dep同时发布一次属性变化的通知到所有依赖的watcher对象
dep.notify();
}
}
}
defineReactive用到了Object.defineProperty 方法,这也是vue不支持ie8的原因,这个方法的主要作用就是set和get函数,同时也可以看到vue针对data中的所有属性都会new一个dep对象,dep对象里面会存放所有依赖此属性的watcher对象,此处用到了发布/订阅模式,dep和watcher分别是发布者和订阅者,每当data中的属性变化dep对象就会通知所有依赖的watcher去更新dom,下面详细分析一下这个过程
上一篇提到,由于template中引用了{{ message }}属性,因此render函数里面会调用到vm.meessage,这时就会触发defineReactive设置的get方法,get方法里面就会进行(该属性)依赖的收集,那么get方法里的Dep.target是啥呢?
上一篇提到dom初次渲染是通过(监听整个模版的)watcher对象初始化时调用watcher.get方法实现的,watcher.get方法主要是计算getter函数的值(本例中是updateComponent,更新dom)和计算依赖(哪些属性的dep对象),Dep.target就是当前接受计算(依赖)的全局惟一的watcher对象,具体方法如下:
1、pushTarget(this),将this(当前watcher对象)赋值给Dep.target
2、调用this.getter,this.getter会访问所有依赖的属性,同时触发属性的getter方法
3、调用属性getter方法中的dep.depend(),完成dep和wathcher的绑定
4、popTarget()将Dep.target值设为targetStack栈中的上一个(没有则为空)
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// 英文注释都是源码作者的注释
Dep.target = null;
var targetStack = [];
//Evaluate the getter, and re-collect dependencies.
Watcher.prototype.get = function get () {
//将this赋值给Dep.target
pushTarget(this);
//执行wacther的更新操作,本文中是执行updateComponent方法
this.getter.call(vm);
popTarget();
}
function pushTarget (_target) {
if (Dep.target) { targetStack.push(Dep.target); }
Dep.target = _target;
}
function popTarget () {
Dep.target = targetStack.pop();
}
继续看defineReactive中dep.depend方法干了啥,其实就是dep对象上维护了一个watcher对象的队列,wathcer对象上也维护了一份dep的队列
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
//将dep对象加入到wather对象的newDeps队列中
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
// 同时将watcher对象也加入到dep对象的subs队列中
dep.addSub(this);
}
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
data值变化时会触发setter方法中的dep.notify,通知绑定在dep对象上的所有watcher对象调用update方法更新视图(watcher.update最终调用了updateComponent,用到了缓存队列,不一定立即触发)
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
总结
1、对data进行observe,针对data属性调用Object.defineProperty设置getter和setter,同时绑定一个dep对象
2、new Watcher(vm, updateComponent, noop)监听整个dom的变化
3、watcher初始化时调用updateComponent,updateComponent调用render函数更新dom(此时还会将该watcher对象赋值给全局对象Dep.target,进行依赖收集)
4、在watcher对象依赖收集期间,render函数访问data中的属性(如本例的data.message),触发data.message的getter方法,在getter方法中会将data.message绑定的dep对象和wathcer对象建立对应关系(互相加入到对方维护的队列属性上)
5、后续data属性的值变化时dep对象会通知所有依赖此data属性的watcher对象调用updateComponent方法更新视图
View变化 -> ViewModel更新
视图变化 -> 数据更新主要是通过v-model实现的,v-model本质上不过是语法糖,它负责监听用户的输入事件以更新数据,本例中
<input v-model="message">
基本等同于下面的效果
<input :value="message" @input="message = $event.target.value"/>