Vue源码解读-双向绑定原理

644 阅读2分钟

Vue2采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。 下面直接上代码讲述吧!

1、实现一个Observer函数,对需要observe的数据对象进行递归遍历(区分数组或者对象),包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,读取某个对象的值就会触发getter,监听到数据变化

 function observe (value) {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    let ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else {
      // ....
      ob = new Observer(value);
    }
    return ob
}

// 假设我们传的value={str: 'Hello', obj: { a: 'A' }}
let Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep(); // 为value创建dep实例
  def(value, '__ob__', this); // 实现了响应式就会有一个__ob__属性
  if (Array.isArray(value)) {
    // ···重写数组方法
    this.observeArray(value); // 遍历数组每一项,实现响应式
  } else { // 是对象调用walk
    this.walk(value); // 为对象每个键对应的值进行响应式处理
  }
};

Observer.prototype.walk = function walk (obj) {
  const keys = Object.keys(obj); 
  for (let i = 0; i < keys.length; i++) { // 实现深层次响应式的关键
    defineReactive$$1(obj, keys[i]); // 为对象每个键对应的值进行响应式处理
  }
};

Observer.prototype.observeArray = function observeArray (items) { // 对数组每个成员进行响应式处理
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

2、defineReactive$$1函数是实现数据劫持的地方,getter里面添加订阅,setter里面通知更新

function defineReactive$$1 (obj, key, val) {
  // 为每个key创建一个Dep实例
  const dep = new Dep();
  
  const getter = Object.getOwnPropertyDescriptor(obj, key).get;
  const setter = Object.getOwnPropertyDescriptor(obj, key).set;
  
  let childOb = observe(val); // 对val响应式处理
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend(); // 进行依赖收集
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      let value = getter ? getter.call(obj) : val;
      //...
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = observe(newVal); // 对newVal也进行响应式处理
      dep.notify(); // 通知更新
    }
  });
}

3、以上代码实现已经可以监听每个数据的变化了,监听到变化之后就要通知订阅者,vue使用发布订阅模式,维护一个数组收集订阅者,数据变化通知更新

let Dep = function Dep () {
  this.subs = []; // 里面保存订阅者,其实就是watcher实例
};

Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

Dep.prototype.notify = function notify () { // 通知更新
  let subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

image.png 4、代码实现的巧妙之处:watcher把自己设置到全局的一个指定位置,然后读取数据,读取数据时会触发这个数据的getter,在getter就能得到当前正在读取数据的watcher,并把watcher收集到Dep中。组件内的响应式数据变更了,就会触发setter, setter里面调用了dep.nofity(), dep.subs循环拿到每一个watcher, 执行watcher.update()

// Watcher主要是保存更新函数,值变化的时候调用更新函数
let Watcher = function Watcher (vm,expOrFn,cb,options) {
  this.vm = vm;
  vm._watchers.push(this);
  this.getter = expOrFn; // expOrFn就是updateComponent函数
  this.value = this.get();
}

Watcher.prototype.get = function() {
    pushTarget(this)
    const vm = this.vm
    const value = this.getter.call(vm, vm)
    popTarget()
    return value
}
Watcher.prototype.addDep = function(dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
}
Watcher.prototype.update = function () {
    // ...
    this.run()
}
Watcher.prototype.run = function () { // watcher真正更新的函数
    const value = this.get() // 调用watcher的get方法
    if (value !== this.value) {
       const oldValue = this.value
       this.value = value
       if (!this.user) {
         this.cb.call(this.vm, value, oldValue)
       }
    }
}

5、Vue 会在初始化实例时进行双向数据绑定,使用Object.defineProperty()对属性遍历添加 getter/setter 方法,所以属性必须在 data 对象上存在时才能让它是响应的。如果要给对象添加新的属性,此时新属性没有进行过响应式处理,会出现数据变化,页面不变的情况。此时需要用到set或者$set。

function set (target, key, val) {
  // ....
  // 若key本来就是对象中的一个属性,并且key不是Object原型上的属性。说明这个key在对象上已经定义过,直接修改值就可以,可自动触发响应。
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  //如果有这个 __ob__ 属性,那就说明这个对象是响应式的,我们修改对象已有属性的时候就会触发页面渲染。
  const ob = target.__ob__
  if (!ob) { // 当前的target对象不是响应式对象,那么直接赋值返回即可。
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val) 
  ob.dep.notify()
  return val
}

6、Vue 不能检测利用索引直接设置数组中的某一项,比如:this.arr[0] = '修改值'。此时数据更新但是页面没更新。你需要通过this.set(this.arr, 0, '修改值')的方法修改,或者使用Vue重写后的7个方法中的splice触发数组的响应式。

image.png

数组的响应式原理

  let arrayProto = Array.prototype
  let arrayMethods = Object.create(arrayProto)
  let methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];

  methodsToPatch.forEach(function (method) {
    let original = arrayProto[method]
    def(arrayMethods, method, function mutator () {
      let args = [], len = arguments.length
      while (len--) args[len] = arguments[len]

      let result = original.apply(this, args)
      let ob = this.__ob__
      let inserted  // 收集参数
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          inserted = args.slice(2)
          break
      }
      if (inserted) { ob.observeArray(inserted) }
      ob.dep.notify()
      return result
    })
  })