你不知道的Vue响应式原理

8,404 阅读9分钟

文章首发于github Blog

本文根据Vue源码v2.x进行分析。这里只梳理最源码中最主要的部分,略过非核心的一些部分。响应式更新主要涉及到WatcherDepObserver这几个主要类。

watcher-dep-observer

本文主要弄清楚以下几个容易搞混的问题:

  • WatcherDepObserver这几个类之间的关系?
  • Dep中的 subs 存储的是什么?
  • Watcher中的 deps 存储的是什么?
  • Dep.target 是什么,该值是何处赋值的?

本文直接从新建Vue实例入手,一步一步揭开Vue的响应式原理,假设有以下简单的Vue代码:

var vue = new Vue({
    el: "#app",
    data: {
        counter: 1
    },
    watch: {
        counter: function(val, oldVal) {
            console.log('counter changed...')
        }
    }
})

1. Vue实例初始化

从Vue的生命周期可知,首先进行init初始化操作,这部分代码在instance/init.js中。

src/core/instance/init.js

initLifecycle(vm) // vm生命周期相关变量初始化操作
initEvents(vm) // vm事件相关初始化
initRender(vm) // 模板解析相关初始化
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子函数
initInjections(vm) // resolve injections before data/props 
initState(vm) // vm状态初始化(重点在这里)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 调用created钩子函数

上述源码中的initState(vm)是要研究的重点,里面实现了propsmethodsdatacomputedwatch的初始化操作。这里根据上述例子,重点看datawatch,源码位置在instance/state.js

src/core/instance/state.js

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) {
    initData(vm) // 对vm的data进行初始化,主要是通过Observer设置对应getter/setter方法
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  // 对添加的watch进行初始化
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

2. initData

Vue实例为它的每一个data都实现了getter/setter方法,这是实现响应式的基础。关于getter/setter可查看MDN web docs。 简单来说,就是在取值this.counter的时候,可以自定义一些操作,再返回counter的值;在修改值this.counter = 10的时候,也可以在设置值的时候自定义一些操作。initData(vm)的实现在源码中的instance/state.js

src/core/instance/state.js

while (i--) {
	...
    // 这里将data,props,methods上的数据全部代理到vue实例上
	// 使得vm.counter可以直接访问
}
// 这里略过上面的代码,直接看最核心的observe方法
// observe data
observe(data, true /* asRootData */)

这里observe()方法将data变成可观察的,为什么说是可观察的?主要是实现了getter/setter方法,让Watcher可以观察到该数据的变化。下面看看observe的实现。

src/core/observer/index.js

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 (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value) // 重点在这里,响应式的核心所在
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

这里只关注new Observer(value),这是该方法的核心所在,通过Observer类将vue的data变成响应式。 根据我们的例子,此时入参value的值是{ counter: 1 }。 下面就具体看看Observer类。

3. Observer

首先看看该类的构造方法,new Observer(value)首先执行的是该构造方法。作者的注释说了,Observer Class将每个目标对象的键值(即data中的数据)转换成getter/setter形式,用于进行依赖收集和通过依赖通知更新。

src/core/observer/index.js

/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object's property keys into getter/setters that
 * collect dependencies and dispatches updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has 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)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value) // 遍历data对象中{counter : 1, ..} 中的每个键值(如counter),设置其setter/getter方法。
    }
  }

  ...
}

这里最核心的就是this.walk(value)方法,this.observeArray(value)是对数组数据的处理,实现对应的变异方法,这里先不考虑。

继续看walk()方法,注释中已说明walk()做的是遍历data对象中的每一设置的数据,将其转为setter/getter

  /**
   * Walk through each property 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], obj[keys[i]])
    }
  }

那么最终将对应数据转为getter/setter的方法就是defineReactive()方法。从方法命名上也容易知道该方法是定义为可响应的,结合最开始的例子,这里调用就是defineReactive(...)如图所示:

defineReactive

源码如下:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // dep 为当前数据的依赖实例
  // dep 维护着一个subs列表,保存依赖与当前数据(此时是当前数据是counter)的观察者(或者叫订阅者)。观察者即是Watcher实例。
  const dep = new Dep() ---------------(1)

  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

  let childOb = !shallow && observe(val)
  
  // 定义getter与setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      
      // 这里在获取值之前先进行依赖收集,如果Dep.target有值的话。
      if (Dep.target) {    -----------------(2)
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      
      // 依赖收集完后返回值
      return value
    },
    
    ...
}

先看getter方法,该方法最重要的有两处。

  1. 为每个data声明一个dep实例对象,随后dep就被对应的data给闭包引用了。举例来说就是每次对counter取值或修改时,它的dep实例都可以访问到,不会消失。
  2. 根据Dep.target来判断是否收集依赖,还是普通取值。这里Dep.target的赋值后面再将,这里先知道有这么一回事。

然后再看下setter方法,源码如下:

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()
  }
  // 这里对数据的值进行修改
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal)
  
  // 最重要的是这一步,即通过dep实例通知观察者我的数据更新了
  dep.notify()
}

到这里基本上Vue实例data的初始化就基本结束,通过下图回顾下initData的过程:

initData flow

随后要进行的是watch的初始化:

src/core/instance/state.js

export function initState (vm: Component) {
  ...
  
  if (opts.data) {
    initData(vm) // 对vm的data进行初始化,主要是通过Observer设置对应getter/setter方法
  } 
  
  // initData(vm) 完成后进行 initWatch(..)
  ...
  
  // 对添加的watch进行初始化
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

4. initWatch

这里initWatch(vm, opts.watch)对应到我们的例子中如下所示:

initWatch

initWatch源码如下:

src/core/instance/state.js

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    // handler 是观察对象的回调函数
    // 如例子中counter的回调函数
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

createWatcher(vm, key, handler)是根据入参构建Watcher实例信息,源码如下:

function createWatcher (
  vm: Component,
  keyOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 判断是否是对象,是的话提取对象里面的handler方法
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 判断handler是否是字符串,是的话说明是vm实例上的一个方法
  // 通过vm[handler]获取该方法
  // 如 handler='sayHello', 那么handler = vm.sayHello
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  
  // 最后调用vm原型链上的$watch(...)方法创建Watcher实例
  return vm.$watch(keyOrFn, handler, options)
}

$watch是定义在Vue原型链上的方法,源码如下:

core/instance/state.js

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // 创建Watcher实例对象
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    
    // 该方法返回一个函数的引用,直接调用该函数就会调用watcher对象的teardown()方法,从它注册的列表中(subs)删除自己。
    return function unwatchFn () {
      watcher.teardown()
    }
  }

经过一系列的封装,这里终于看到了创建Watcher实例对象了。下面将详细讲解Watcher类。

5. Watcher

根据我们的例子,new Watcher(...)如下图所示:

newWatcher

首先执行Watcher类的构造方法,源码如下所示,省略了部分代码:

core/observer/watcher.js

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
  	 ...
    this.cb = cb // 保存传入的回调函数
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = [] // 保存观察数据当前的dep实例对象
    this.newDeps = []  // 保存观察数据最新的dep实例对象
    this.depIds = new Set()
    this.newDepIds = new Set()

    // parse expression for getter
    // 获取观察对象的get方法
    // 对于计算属性, expOrFn为函数
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
    // 通过parsePath方法获取观察对象expOrFn的get方法
      this.getter = parsePath(expOrFn)
      ...
    }
    
    // 最后通过调用watcher实例的get()方法,
    // 该方法是watcher实例关联观察对象的关键之处
    this.value = this.lazy
      ? undefined
      : this.get()
  }

parsePath(expOrFn)的具体实现方法如下:

core/util/lang.js

/**
 * Parse simple path.
 */
const bailRE = /[^\w.$]/ // 匹配不符合包含下划线的任意单词数字组合的字符串
export function parsePath (path: string): any {
  // 非法字符串直接返回
  if (bailRE.test(path)) {
    return
  }
  // 举例子如 'counter'.split('.') --> ['counter']
  const segments = path.split('.')
  // 这里返回一个函数给this.getter
  // 那么this.getter.call(vm, vm),这里vm就是返回函数的入参obj
  // 实际上就是调用vm实例的数据,如 vm.counter,这样就触发了counter的getter方法。
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

这里很巧妙的返回了一个方法给this.getter, 即:

this.getter = function(obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
}

this.getter将在this.get()方法内调用,用来获取观察对象的值,并触发它的依赖收集,这里即是获取counter的值。

Watcher构造方法的最后一步,调用了this.get()方法,该方法源码如下:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    // 该方法实际上是设置Dep.target = this
    // 把Dep.target设置为该Watcher实例
    // Dep.target是个全局变量,一旦设置了在观察数据中的getter方法就可使用了
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用观察数据的getter方法
      // 进行依赖收集和取得观察数据的值
      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)
      }
      // 此时观察数据的依赖已经收集完
      // 重置Dep.target=null
      popTarget()
      // 清除旧的deps
      this.cleanupDeps()
    }
    return value
  }

关键步骤已经在上面代码中注释了,下面给出一个Observer,Watcher类之间的关联关系,图中还是以我们的例子进行描述:

Observer-Watcher-rel

  • 红色箭头:Watcher类实例化,调用watcher实例的get()方法,并设置Dep.target为当前watcher实例,触发观察对象的getter方法。
  • 蓝色箭头:counter对象的getter方法被触发,调用dep.depend()进行依赖收集并返回counter的值。依赖收集的结果:1.counter闭包的dep实例的subs添加观察它的watcher实例w12. w1的deps中添加观察对象counter的闭包dep
  • 橙色箭头:当counter的值变化后,触发subs中观察它的w1执行update()方法,最后实际上是调用w1的回调函数cb。

Watcher类中的其他相关方法都比较直观这里就直接略过了,详细请看Watcher类的源码。

6. Dep

上图中关联Observer和Watcher类的是Dep,那么Dep是什么呢?

Dep可以比喻为出版社,Watcher好比读者,Observer好比东野圭吾相关书籍。比如读者w1对东野圭吾的白夜行(我们例子中的counter)感兴趣,读者w1一旦买了东野圭吾的书,那么就会自动在这本书的出版社(Dep实例)里面注册填w1信息,一旦该出版社有了东野圭吾这本书最新消息(比如优惠折扣)就会通知w1。

现在看下Dep的源码:

core/observer/dep.js

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

  constructor () {
    this.id = uid++
    // 保存观察者watcher实例的数组
    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 () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep类比较简单,对应方法也非常直观,这里最主要的就是维护了保存有观察者实例watcher的一个数组subs

7. 总结

到这里,主要的三个类都研究完了,现在基本可以回答文章开头的几个问题了。

Q1:WatcherDepObserver这几个类之间的关系?

A1:Watcher是观察者观察经过Observer封装过的数据,DepWatcher和观察数据间的纽带,主要起到依赖收集和通知更新的作用。

Q2:Dep中的subs存储的是什么?

A2: subs存储的是观察者Watcher实例。

Q3:Watcher中的deps存储的是什么?

A3:deps存储的是观察数据闭包中的dep实例。

Q4:Dep.target是什么, 该值是何处赋值的?

A4:Dep.target是全局变量,保存当前的watcher实例,在new Watcher()的时候进行赋值,赋值为当前Watcher实例。

8. 扩展

这里看一个计算属性的例子:

var vue = new Vue({
    el: "#app",
    data: {
        counter: 1
    },
    computed: {
        result: function() {
            return 'The result is :' + this.counter + 1;
        }
    }
})

这里的result的值是依赖与counter的值,通过result更能体现出Vue的响应式计算。计算属性是通过initComputed(vm, opts.computed)初始化的,跟随源码追踪下去会发现,这里也有Watcher实例的创建:

core/instance/state.js

  watchers[key] = new Watcher(
    vm,  // 当前vue实例
    getter || noop,  // result对应的方法 function(){ return 'The result is :' + this.counter + 1;}
    noop, // noop是定义的一个空方法,这里没有回调函数用noop代替
    computedWatcherOptions // { lazy: true }
  )

示意图如下所示:

computed-watcher

这里计算属性result因为依赖于this.counter,因此设置一个watcher用来观察result的值。随后通过definedComputed(vm, key, userDef)来定义计算属性。在计算获取result的时候,又会触发this.countergetter方法,这样使得result的值依赖于this.counter的值。

definedComputed

最后会为计算属性result定义它的setter/getter属性:Object.defineProperty(target, key, sharedPropertyDefinition)。更详细信息请查看源码。

9. 参考

  1. vue 官方文档
  2. vue 源码
  3. vue 源码解析