Vue源码解析(二)-MVVM双向绑定&&Watcher介绍

前言

上一遍文章介绍了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"/>
阅读 2.2k

推荐阅读