Vue 的双向数据绑定原理

112 阅读1分钟

Vue 的双向数据绑定即数据更新视图,视图更新数据。在了解具体过程之前,我们先来定义实现过程中需要的三个类:

  1. Observer 类:将 Data 对象所有的属性转换成 getter/setter 的形式来追踪变化,技术实现可以使用 Object.defineProperty 或者 Proxy;
  2. Dep 类:负责收集依赖,删除依赖和发送通知;
  3. Watcher 类:负责处理依赖(依赖的类型比较多),同时它也是连接视图和数据之前的桥梁。

Vue 双向数据绑定的具体实现过程:

  1. 先把 Data 通过 Observer 转换成 getter/setter 的形式来追踪变化;
  2. 在 getter 收集依赖,在 setter 中触发依赖;
  3. 当外界通过 Watcher 读取数据时,会触发 getter 从而将 Watcher 添加到依赖中;
  4. 当数据发生变化时,会触发 setter,从而向 Dep 中的依赖发送通知;
  5. Watcher 接收到通知之后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数。

所谓的依赖,就是使用数据的地方。提前收集使用数据的依赖,等到数据改变的时候,再将收集好的依赖循环触发一遍就好。

Oberver 类的简单实现

// Oberver类,负责将对象属性转换为getter和setter形式
class Oberver {
  constructor(data) {
    this.data = data

    if (!Array.isArray(data)) {
      // 跳过数组的检测
      this.walk(data)
    }
  }

  walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]; obj[keys[i]])
    }
  }
}

function defineReactive(data, key, value) {
  // 递归子属性
  if (value && typeof value === 'object') {
    new Oberver(value)
  }

  let dep = new Dep()

  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      dep.depend() // 收集依赖
      return value
    },
    set(newVal) {
      if (value === newVal) {
        return
      }
      val = newVal
      dep.notify() // 发送通知
    }
  })
}

Dep 类的简单实现

// Dep类负责新增依赖,删除依赖和发送通知
class Dep {
  constructor() {
    this.subs = []
  }

  addSub(sub) {
    this.subs.push(sub)
  }

  // 新增依赖
  depend() {
    if (window.target) {
      this.addSub(window.target)
    }
  }

  // 发送通知
  notify() {
    this.subs.forEach(function(sub) {
      sub.update();
    });
  }

  // 删除依赖
  remove(arr, item) {
    if (arr.length) {
      const index = arr.indexOf(item)
      if (index > -1) {
        return arr.splice(index, 1)
      }
    }
  }
}

Watcher 类的简单实现

class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.value = this.get() // 将自己添加到订阅器的操作
  }

  get() {
    window.target = this // 缓存自己
    var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
    window.target = null // 释放自己
    return value
  }

  update() {
    var value = this.vm.data[this.exp]
    var oldVal = this.value
    if (value !== oldVal) {
      this.value = value
      this.cb.call(this.vm, value, oldVal)
    }
  }
}