Vue源码解析(二)-MVVM双向绑定&&Watcher介绍
前言
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"/>