Vue 2.x 源码解读系列《响应原理剖析》

首先我们得找到响应多的起点,如果仔细查看源代码你会发现在 initData() 方法的最后面调用了 observe(),也就是这个方法对 data 对象中的所有属性进行观察(拦截)

export function initState(vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // 初始化 data
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  // 如果组件中存在 watch ,并且不是原生的 watch,那么就初始化 watch
  // 这里的 watch 为我们在组件中定义的用来监听属性变化的那个 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    // 传入实例以及组件中添加的 watch 对象
    initWatch(vm, opts.watch)
  }
}

initDate() 方法的最后一行调用了 observe() 方法

function initData(vm: Component) {
  // 缓存 data 
  let data = vm.$options.data
  // 把 data 数据挂载到 vm._data 
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
   
   ......
   省略其它代码
   ......

  // 对 data 对象中的所有属性进行观察
  observe(data, true /* asRootData */)
}

所以我们 observe() 方法开始,沿着这个顺藤摸瓜一步一步来解开 vue 响应式原理的神秘面纱。

响应式的 observe() 方法

export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

observe() 方法中调用了 new Observer() 创建一个 ob 实例。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor(value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 如果中存在 '__proto__',即环境支持 '__proto__'
      if (hasProto) {
        // 把 arrayMethods 挂载到 value.__proto__ 上
        protoAugment(value, arrayMethods)
      } else {
        // 直接把同名属性指向 arrayMethods.__proto__ ( 等同于 Array.prototype )的同名属性
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 遍历 value 数组中的每一项
      this.observeArray(value)
    } else { // 对象
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Observe 中则调用 walk() 方法遍历对象中的所有属性逐一进行 defineReactive()。至于如果属性值是一个数组导致递归这里我们可以先忽略,毕竟即使是 递归,到最后还是走 walk() 方法。

在 defineReactive() 中最重要的一步拦截属性的 set 和 get 方法。

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 每调用一次都会创建一个新的依赖实例
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    // 如果只传了两个参数,则把 obj[key] 取出赋值给 val
    val = obj[key]
  }
  // 递归观察对象属性
  let childOb = !shallow && observe(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
    },
    // 在生成 虚拟 DOM 的时候会触发属性的 set 方法
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 给新值添加观察者
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

在 Object.defineProperty() 中定义 get 方法中 先把 val 作为参数调用 observe(val) 如果值是一个对象,那递归遍历。否则继续往下执行,调用 dep.depend() 把当前的 watcher 实例添加到 dep 实例的 subs 数组中,以及把当前拦截的属性所对应 Dep 实例添加到 Watcher 的 deps 数组中,这样就完成了 Watcher 实例与 Dep 实例的相互关联。

在 set 方法中同样的先把 newVal 作为参数调用 observe(newVal) 如果值是一个对象,那么递归遍历。否则继续往下执行,通过 dep.notify() 通知 dep 关联的所有 watcher 调用各自的 update() 方法进行更新。

Dep 与 Watcher 来龙去脉大致如下:

1.前置准备 { 深度遍历 data ,为 data 数据对象中的所有属性添加 get 和 set 拦截 },拦截准备好了以后,Dep 和 Watcher 就只需要等待时机了。一旦时间成熟(页面中有使用 data 中定义的属性), Dep 和 Watcher 就开始干活了。

2.在挂载前( mounted 钩子函数触发之前),为当前组件实例创建一个 Watcher 实例,即 new Watcher()。部分代码如下:

// 挂载组件
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {

  ...... 省略代码 ......

  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }


  // 挂载前给实例创建一个 Watcher,用来处理响应式属性
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

 ...... 省略代码 ......

}

3.在 new Watcher() 时, 除了 this.lazy 为真时(比如:computed 计算属性)会调用当前 Watcher 实例中的 this.get(),此方法中会执行 this.getter.call(vm, vm) 即 vm._update() 方法,也有可能是一个对象的取值函数(组件中使用了 watcher ),还有可能是一个计算属性所对应的方法, 而在执行 vm._update() 方法时会读取 data 中的数据(属性),因此在这个过程当中会被 set 拦截。Watcher 类中的 get() 方法代代码实现如 下:

get() {
  // 把当前正在渲染的的组件的 watcher 实例缓存到 Dep.target
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    //this.getter 有可能是 vm._update() 方法,也有可能是一个对象的取值函数(组件中使用了 watcher ),还有可能是一个计算属性所对应的方法
    // 而在执行 vm._update() 方法时会读取 data 中的数据(属性),因此在这个过程当中会被 set 拦截
    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
}

如果想看 Watcher 类的完整源代码可以翻阅 [src/core/instace/obsever/watcher.js]。

4.而当 this.getter.call(vm, vm) 执行时不管是 vm._update() 方法还是获取对象属性的方法,都会触发属性的 get 方法,从而被成功拦截,实现依赖收集。

依赖的过程细节如下:

  • new Watcher() 在返回值之前会执行 Watcher 实例的 this.cleanupDeps() 方法,方法中有这么一行代码 this.deps = this.newDeps ,这行代码就是把之前缓存的所有依赖项保存到 this.deps 中,即当把 Watcher 实例创建完之后,实例上的 deps 保存了组件实例 data 里的所有属性所对应的依赖项。
  • 在属性被读取的时候, 则会触发属性的 get 方法,此时这个属性对应的依赖项 Dep 实例会调用 depend() 方法调用 Dep.target(当前正在渲染的时的 Watcher 实例)的 addDep() 方法把当前的 Watcher 实例添加到 Dep 实例的 subs 数组中,也就是说 Watcher 实例中保存了 Dep 实例,Dep 实例中保存了 Watcher 实例

依赖收集完了之后,如果属性有修改,则会通过 set 方法进行拦截,调用 dep.notify() 方法通知 dep.subs 中的所有 watcher 调用其 update() 方法进行更新。

那么现在问题来了,当同一个属性被读取了两遍,这个又是怎么处理的呢?

在 Watcher 中有这么一段代码:

 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)) {
    // 把依赖添加到 subs 数组中
    dep.addSub(this)
   }
  }
 }

在给 this.newDeps 中添加 Dep 实例时,会检测 Dep 实例是否已经添加过,如果已添加过,则不再重复添加。里层还有一个判断 !this.depIds.has(id),这又是为什么呢?首先这个判断里面是给 Dep 实例的 subs 数组中添加 Watcher 实例的,即然加了一层判断,说明要排除某些情况。而这个判断中的条件涉及到了 this.depIds 属性,而这个属性只有在 cleanupDeps() 方法才用到,所以我们重点关注这个方法是在什么时机下才会被执行的。

在触发 mounted 钩子之前创建一个 Watcher 实例( new Watcher() ) ,而在 Watcher 类里的末尾处调用了 cleanupDeps() 。 ???

一个属性对应一个 Dep 实例,即使同一属性被多次访问。

关于 Watcher 实例中的 this.deps 数组与 Dep 实例中的 this.subs 数组在什么情况下才会有多个呢?这两个数组就是把 Watcher 与 Dep 关联起来的。

对于 Watcher 实例中的 this.deps 数组,比较容易理解,即当 data 中有多个属性时, this.deps 数组中就会有多个 Dep 实例(一个属性对应一个 Dep 实例)

那么问题来,什么时候 this.subs 才会有多个 watcher 呢?

Dep 实例的 subs 数组一般情况只会有一个 Watcher 实例,但当我们在组件中使用 watch 或者 computed 来监听数据变化,然后作一些逻辑处理时,subs 数组中就会有多个 Watcher 实例。如果组件中用 watch 监听了多少个属性,或者 computed 中定义了多少个属性,那么 subs 中 Watcher 实例就会相应的增加多少个 Watcher 实例。

三类 Watcher 实例中的 this.gettter 在不同的情况下取值如下:

当在组件中使用了 watcher 来监听属性时,Watcher 类中的 this.getter 的值就变成了 parsePath(expOrFn) 返回的函数 当使用了 computed 计算属性时,其对应 Watcher 实例中的 this.getter 则是计算属性对应的方法 组件对应的 Wathcer 类中的 this.getter 则是 vm._update() 函数。

当我们修改了一个属性值导致 Vue 重新生成虚拟 Dom 时,归属于同一个 Watcher 实例管理的所有属性(Dep 实例,这里的 Dep 实例其实已经跟属性关联在一起了)都会重新被读取一遍,相当于语法树(经过处理的 template 模板)中的所有使用到的属性都会被无差别的重新获取更新一遍,即使一些没有被修改过值的属性。

dep 实例的 notify() 方法中是遍历 subs 数组逐一调用每一个 watcher 实例的 update() 方法。

Dep 实例与 Watcher 实例之间的关系其实就是一个发布订阅模式。

声明

1.原创文章,不经本站同意,不得以任何形式转载,如有不便,请多多包涵!

2.本文永久链接:http://yunkus.com/post/5e413d952034cfdb

3.如果觉得本文对你有帮助,或者解决了你的问题,不妨扫一扫右边的二维码打赏支持,你的一分一毫,可能会让世界变得更美好。

微信
扫一扫,赏我
支付宝
扫一扫,赏我