Vue2 响应式原理

2,514 阅读2分钟

Vue2 响应式主要是 ObserverDepWatcher 三部分的通信。

Observer

实例化 Observer 时,会遍历对象(即 data),在其每个属性上调用 defineReactive 函数,该函数的作用就是将这些属性转换成 gettersetter。这一步是在 beforeCreatecreated 两个钩子之间执行的。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // ...
    } else {
      this.walk(value)
    }
  }

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

接下来看看 defineReactivedefineReactive 中会实例化一个 DepDep 可以理解为一个消息中心。数据的变更最终是要通知到 Watcher 的,Dep 中收集了这些数据要通知的 Watcher。当 getter 触发时,当前的 WatcherDep.target) 会被加入 dep 中;当 setter 触发时,dep 会调用 notify 方法,通知其收集的所有 Watcher

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  // ...

  Object.defineProperty(obj, key, {
    // ...
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        // ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // ...
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // ...
      dep.notify()
    }
  })
}

Dep

Dep 就是一个消息中心,代码也很简单,目的就是收集 Watcher,当数据变更时,调用所有 Watcherupdate 方法。

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

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

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  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()
    }
  }
}

Watcher

Watcher 是一个观察者,数据变化时会调用其 update 方法,更新对应视图。在组件 mount 的时候,会实例化一个 Watcher,每个组件对应一个 Watcher。因此 Vue 每次更新都是以组件为单位,diff 算法也是以组件为单位进行对比,这也是组件必须只有一个根元素的原因。

Watcher 实例化之后,会修改 Dep.target,将其指向自己。接着组件挂载,模板渲染,data 中的值被读取,触发其 getterDep 收集当前的 Watcher,形成闭环。

下面代码中,get 方法中的 pushTarget 修改了 Dep.targetvalue = this.getter.call(vm, vm) 这行代码中,getter 方法其实进行了视图的更新。

export default class Watcher {
  // ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  addDep (dep: 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)
      }
    }
  }

  // ...
  
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
   
  // ...
}

实例分析

下图是在控制台打印的 data 中的某个属性。其中的 __ob__ 属性,就是实例化的 Observer__ob__ 中有一个 dep 属性,就是实例化的 Depdep 中的 subs 就是收集的 Watcher

ab 修改时,会调用其 setter;然后 dep 会通知 subs 中的 Watcher,即调用 Watcherupdate 方法。然后 Watcher 更新对应的视图(也可能是 computedWatch)。

image.png

总结

整个响应式的过程总结下来就是:

在 data 初始化时,将其属性转换成 gettersetter,同时实例化一个 Dep。视图渲染时, getter 触发,Dep 会收集 Watcher;在 setter 触发时,Dep 会通知其收集的 Watcher 更新视图。Wathcer 在组件挂载时实例化,Watcher 与组件一一对应。

再看 Vue 官网这张图就会容易理解了。

image.png