总览

Vue是一款MVVM(Model-View-ViewModel)前端框架,框架帮我们实现了数据和视图的双向绑定,我们不在需要操作DOM,只需要关心数据的变化,视图就会自动改变,我们就来探究一下Vue的数据绑定原理

先来贴一张官方的数据绑定原理的介绍图,还有一段官网的介绍

image-20210215103808465

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter

总体来说,就是将Vue中的data通过Observer进行数据观察,在读取数据值的时候添加订阅,在值进行修改时发布通知,告诉这个数值所有的订阅者更新数据

源码分析

为了方便理解,代码中有所省略

defineReactive劫持数据

众所周知,Vue2使用的是Object.defineProperty()来实现的数据劫持,defineReactive就是对Object.defineProperty()进行的一次封装

export default function defineReactive(data,key,val){
  // console.log('defineReactive',data,key)
  if(arguments.length == 2){
    val = data[key]
  }
     //子元素要进行observe,形成递归,多个函数循环调用
  let childOb = observe(val)
  Object.defineProperty(data,key,{
    //可枚举
    enumerable:true,
    //可以被配置,比如可以被delete
    configurable:true,
    //getter
    get() {
      console.log(`打开${key}属性`)
      return val
    },
    //setter
    set(newValue) {
      console.log(`改变obj的${key}属性`,newValue)
      if(val === newValue){
        return
      }
      val = newValue
      childOb = observe(newValue)
    }
  });
}

将defineReactive暴露出去,再需要进行数据劫持的时候直接就可以用这个封装好的方法

Observer类

开始我们说了,Vue中的data通过Observer进行数据观测才可以进行劫持,那么Observer的作用是什么呢?Observer主要负责将基本数据类型进行我们上面所定义的defineReactive数据劫持,然后区分引用类型,将对象的属性递归劫持,如果是数组,就要改写数组的方法来实现数据劫持,之后会详细介绍。

Observer使用时会在外面进行类型检测,如果不是引用类型,直接返回,只有引用类型才进行数据侦测,并将Observer实例返回

// observe.js
export default function(value){
  //如果value不是对象,什么都不做
  if(typeof value != 'object') return;
  //定义ob
  var ob;
  // __ob__就是对象是否真测过的标识,如果数据被Observer处理过,他的属性上就会有__ob__
  if(typeof value.__ob__ !== 'undefined'){
    ob = value.__ob__;
  }else{
    ob = new Observer(value)
  }
  return ob;
}
// Observer.js
export default class Observer {
  constructor(value) {
    //每一个Observer的实例身上,都有一个dep(下一部分会说到)
    this.dep = new Dep();
    //给实例添加了__ob__属性,值是这个new的实例
    def(value, '__ob__', this, false)
    console.log('我是Observer构造器', value)
    //Observer将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)object
    //检查它是数组还是对象
    if (Array.isArray(value)) {
      //如果是数组,就将这个数组的原型指向arrayMethods(后面会介绍)
      Object.setPrototypeOf(value, arrayMethods);
      //让这个数组变得observe
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  //遍历
  walk(value) {
    for (let k in value) {
      defineReactive(value, k)
    }
  }
  //数组的特殊遍历
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      observe(arr[i])
    }
  }
}

数组的响应式

由于通过索引来实现绑定太过浪费性能,所以,Vue在实现数组的响应式时重写了7个数组的API(为什么是7个?Array.prototype上可以改变原数组的API只有7个,其他的不会改变原数组),只要调用了这7个API就通知视图进行更新

//得到Array.prototype
const arrayPrototype = Array.prototype;

//以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype);

//要被改写的7个数组方法
const methodsNeedChange = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsNeedChange.forEach(methodName =>{
  //备份原来的方法
  const original = arrayPrototype[methodName];
  def(arrayMethods,methodName,function(){
    //恢复原来的方法
    const result = original.apply(this,arguments)
    //把类数组对象变为数组
    const args = [...arguments]
    //把这个数组身上的__ob__取出来,__ob__已经被添加了,因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。
    const ob = this.__ob__;
    // 要把插入的新项也要变为observe的
    let inserted = [];

    switch(methodName){
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        //splice新加入的参数是从第三项开始
        inserted = args.slice(2)
        break
    }
    //判断有没有要插入的新项,让新项也变成响应的
    if(inserted){
      ob.observeArray(inserted)
    }
    // 调用dep的notify方法通知更新
    ob.dep.notify()
    return result
  },false)
})

依赖收集

依赖收集的过程就是将watcher实例添加到Dep的队列中的过程,Dep实例存在 __ob__属性中,也就是上文Observer中的创建Dep对象实例的那一段代码。每一个Dep实例维护着一个watcher队列,当数据发生改变时,触发notify方法通知视图更新

var uid = 0;
export default class Dep{
  constructor(){
    this.id = uid++;
    //用数组存储自己的订阅者。subs是subscribes订阅者的意思
    this.subs = [];
  }
  //添加订阅
  addSub(sub){
    this.subs.push(sub)
  }
  //添加依赖
  depend(){
    //Dep.target我们自己指定的全局的位置,只要是全局唯一没有歧义就行
    if(Dep.target){
      this.addSub(Dep.target)
    }
  }
  //通知更新
  notify(){
    //浅克隆一份
    const subs = this.subs.slice();
    //遍历
    for(let i = 0,l = subs.length;i<l;i++){
      subs[i].update();
    }
  }
}

这里面比较难理解的是Dep.target这一个东西,这个东西其实就是一个全局标识,如果你愿意,你也可以用window.target或者其他的变量名。了解他的作用继续看下一段

var uid = 0;
export default class Watcher {
  constructor(target, expression, callback) {
    console.log('我是watcher的构造器');
    this.id = uid++;
    this.target = target;
    this.getter = parsePath(expression);
    this.callback = callback;
    this.value = this.get();
  }
  update() {
    this.run();
  }
  get() {
    //进入依赖收集阶段.让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
    Dep.target = this;
    const obj = this.target;
    var value;
    //只要能找,就一直找
    try {
      value = this.getter(obj);
    } finally {
      Dep.target = null;
    }
    return value;
  }
  run() {
    this.getAndInvoke(this.callback)
  }
  getAndInvoke(cb) {
    const value = this.get();

    if (value !== this.value || typeof value == 'object') {
      const oldValue = this.value;
      cb.call(this.target, value, oldValue)
    }
  }
}

function parsePath(str) {
  var segments = str.split('.');
  return (obj) => {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return;
      obj = obj[segments[i]]
    }
    return obj;
  }
}

在创建Watcher实例的时候,将Dep.target指向当前实例,然后触发被处理过的属性的get方法,调用dep的depend方法,将这个watcher实例添加到dep的subs队列中,这时候defineReactive方法就要发生一点改变了

export default function defineReactive(data,key,val){
  const dep = new Dep();
  if(arguments.length == 2){
    val = data[key];
  }
  //子元素要进行observe,形成递归,多个函数循环调用
  let childOb = observe(val);
  Object.defineProperty(data,key,{
    enumerable:true,
    configurable:true,

    //getter
    get() {
      console.log(`打开${key}属性`)
      //如果现在处于依赖收集阶段
      if(Dep.target){
        dep.depend();
        if(childOb){
          childOb.dep.depend()
        };
      }
      return val;
    },
    //setter
    set(newValue) {
      console.log(`改变obj的${key}属性`,newValue)
      if(val === newValue){
        return;
      }
      val = newValue;
      childOb = observe(newValue)
      dep.notify();
    }
  });
}

总结

Vue数据绑定的过程大致就是如此了,还有很多不完善的地方,比如直接通过数组索引改变数组项不会触发响应式,这里官方也是不建议使用下标直接修改,所以官方提供了$set这个API来完成操作

结束~~~


前端小白