Vue2源码解读之Observer篇

2,020 阅读3分钟

今天团队内部分享和讨论了 Vue 数据是如何驱动视图的,本次主要讨论 Object 的变化侦测。

  1. 监听数据变化
  2. 收集依赖
  3. 通知依赖更新

监听数据变化

需要监听哪些数据的变化?

根组件data

子组件data 和 props 也就是说只有在 data 和 props 中定义的数据,才能被监听。

什么时候监听数据的变化

创建 Vue 实例的时候,initData阶段, 调用 observe 方法进行劫持数据变化。

function initData (vm: Component) {
  let data = vm.$options.data
  // observe data
  observe(data, true /* asRootData */)
}

如何追踪数据变化

把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property

initData 阶段, Vue 会调用 observe 方法,内部会 new Observer(), 返回一个 ob对象, ob 对象是一个被 Object.defineProperty 方法劫持的对象。

Observer构造器中主要是缓存 ob 对象, 把实例缓存在 value._ob_ 中, 如果存在 value._ob_ 属性, 则不会 new Observer ,而是从缓存中获取 ob 实例, 减少重复创建。 这里 vue 用了一个 if 判断, 如果 observe 方法的参数是一个数组类型,则会遍历数组,让数组中的每一项都被 observe 方法调用,这样就可以劫数组中对象了。

observe(data, true /* asRootData */)

function observe (value, asRootData) {
  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
}
class Observer {
  constructor (value) {
    value.__ob__ = this
    if (Array.isArray(value)) {
      this.observeArray(value)
    } else {
      this.walk(value)
    }
}
// Vue 将遍历此对象所有的 property, 并绑定 defineReactive
walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
}
observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
}

使用 Object.defineProperty 把这些 property 全部转为 getter/setter, Vue 内部会通过 getter/setter 进行追踪依赖, 在 property 被修改时和被访问时通知变更。 调用 defineReactive 时, 还会递归遍历此对象下所有的 property, 绑定 defineReactive

function defineReactive (obj,property,val,customSetter,shallow) {
  observe(val)
  Object.defineProperty(obj, property, {
    get() {
      return val
    },
    set(newVal) {
      val = newVal
    }
  })
}

收集依赖

依赖是什么?

谁用到了数据谁就是依赖,数据就是上文 Object.defineProperty 响应式劫持的 data, 依赖就是使用数据的地方。比如 template 模板里面使用的 data 变量。

为什么要收集依赖

数据变化了要更新视图,提前把跟这个数据有关的依赖收集起来,等到数据改变时, 可以从刚才收集里取出依赖,方便更新。

在何时收集依赖?

Object.defineProperty 的 getter 方法中收集依赖, 这里的依赖并不直接是一个视图, 而是一个 Watch 实例, 这个 watch 实例可以去通知视图更新的。

function defineReactive (
  obj,property,val
) {
  const dep = new Dep()
  Object.defineProperty(obj, property, {
    get() {
      if (Dep.target) {
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      val = newVal
      dep.notify()
    }
  })
}

class Dep {
  static target;
  constructor () {
    this.subs = []
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

class Watcher {
    constructor (vm,expOrFn){
        this.getter = expOrFn
        this.newDeps = []
        this.get()
    }
    get() {
        Dep.target = this
        this.getter.call(vm, vm)
    }
    addDep (dep) {
       this.newDeps.push(dep)
    }
}

在 defineReactive 绑定每一个 property 之前, 会创建一个 Dep 实例, subs 里面是订阅的多个 watch, subs是一个数组, 一个 dep 可以订阅多个 watch 实例, 就是说一个数据改变了, 可能会影响多个视图更新。

在 getter 方法里, 有一个 Dep.target 参数, 这个 target 其实就是 watch 实例, 那么 target 从哪里来的呢。 在 beforeMount钩子 和 mounted 钩子初始化之间,会实例化 Watch 类, Watch 构造函数中会把 watch 实例保存在 Dep.target 上, 随后会触发所有数据的访问,也就是上面的 getter 方法,dep.depend() 会把 watch 保存起来, 这个过程就是收集依赖。

通知依赖更新

何时通知依赖