Vue 源码解析(一):依赖收集(Observer,Dep与Watcher对象)

Vue 实例新建流程

Vue源码目录结构说明

  • src
    • compiler 解析模板生成AST和render函数
    • core
      • components 目前只有keep-alive组件
      • global-api 向Vue对象注入全局方法:Vue.use(),Vue.extend()等
      • instance 向Vue实例对象注入方法:this.$emit(),this.$forceUpdate()等
      • observer 实现data与Watch对象的依赖收集与更新
      • util 工具类
      • vdom Vdom有关方法
    • entries Vue 不同类型源码入口
    • platforms
    • server
    • sfc
    • shared

这次主要我们用到的目录只有:global-api,instance,observer三个。主要介绍Vue实例化时初始数据和计算属性时的源码具体内容。

initData初始化数据

Vue.prototype._init = function (options?: Object) {
    
    
    const vm: Component = this
    // a uid
    vm._uid = uid++
    ...
    // a flag to avoid this being observed
    vm._isVue = true
    ...
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    ...
    if (vm.$options.el) {
    
    
      vm.$mount(vm.$options.el)
    }
  }
}

当我们在 new Vue() 时首先调用得就是 _init() 方法。它主要初始化生命周期,事件,渲染相关数据,调用 beforeCreate 钩子,初始化provide/injections相关数据,初始化data相关数据,调用 created 钩子。最后调用 $mount() 方法挂载到对应的DOM元素上。

今天我们主要看一下初始化数据相关的部分。也就是 _init() 方法里面的 initState() 函数。

// 初始化数据
export function initState (vm: Component) {
    
    
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // 1. 初始化参数
  if (opts.methods) initMethods(vm, opts.methods) // 2、初始化方法
  if (opts.data) {
    
     // 3、初始化数据
    initData(vm)
  } else {
    
    
    observe(vm._data = {
    
    }, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) // 4、初始化计算属性
  if (opts.watch) initWatch(vm, opts.watch) // 5、初始化监听属性
}

这个 initState() 代码很短,要做的事情也很清楚,就是初始化了 props 参数,methods 方法,data 数据,computed 计算属性,watch 监听属性。这次我们主要关注 initData() 方法和 initComputed() 方法。了解Vue依赖收集与双向绑定的完整流程。

所以,首先我们先看一下 initData() 方法。

function initData (vm: Component) {
    
    
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {
    
    }
  ...
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  let i = keys.length
  while (i--) {
    
    
    if (props && hasOwn(props, keys[i])) {
    
     // 判断props和data里的属性是否有重复
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${
      
      keys[i]}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(keys[i])) {
    
     // 判断data属性不以$或_开头
      proxy(vm, `_data`, keys[i]) // 将this.XXX代理到this._data.XXX
    }
  }
  // observe data
  observe(data, true /* asRootData */) // 
}

function getData (data: Function, vm: Component): any {
    
    
  try {
    
    
    return data.call(vm)
  } catch (e) {
    
    
    handleError(e, vm, `data()`)
    return {
    
    }
  }
}

export function proxy (target: Object, sourceKey: string, key: string) {
    
    
  sharedPropertyDefinition.get = function proxyGetter () {
    
    
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    
    
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

initData() 方法主要做了三件事:

第一件事:一般定义 data 属性都是一个函数,返回的对象才是具体的数据。所以首先判断 data 是不是函数,如果是就通过 getData() 方法运行 data 方法得到返回的数据,否则就返回 data || {}

并且将 data 函数返回的结果赋值给 vm._data 和自己。vm._data 就是保存Vue对象运行数据的属性。以后如果数据发生变化,修改得也是这里的数据。vm.$option.data 是参数里面的原始数据,不能修改。

第二件事:一般我们访问Vue对象里面的数据是使用 this.XXX 这种形式。而现在数据保存在 this._data 里面的,所以使用 proxy() 函数,将 this.XXX 代理到 this._data.XXX 上去。

不仅如此,在代理之后,还检查了 data 里面的属性是否与 props 里面的属性重名。如果重名,则认为些属性是参数,data里面的重名属性将不会被代理。

第三件事,就是运行 observer() 方法,下面会讲解这个方法具体做了什么。

export function observe (value: any, asRootData: ?boolean): Observer | void {
    
    
  if (!isObject(value)) {
    
    
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    
     // 是否已经存在__ob__观察者对象
    ob = value.__ob__
  } else if (
    observerState.shouldConvert && // ???
    !isServerRendering() && // 不是服务器渲染
    (Array.isArray(value) || isPlainObject(value)) && // data必须是数组或者简单对象
    Object.isExtensible(value) && // data对象必须是可扩展的(可以添加额外属性)
    !value._isVue // 
  ) {
    
    
    ob = new Observer(value) // 初始化data.__ob__属性
  }
  if (asRootData && ob) {
    
    
    ob.vmCount++ // 根对象
  }
  return ob
}

这个方法的关键在 ob = new Observer(value) 这一行。为数据对象添加一个__ob__属性,而这个属性就是一个Observer对象的实例。下面主要是看一下Observer类的构造函数。

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) // 等同于value.__ob__ = this
    if (Array.isArray(value)) {
    
    
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(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], obj[keys[i]])
    }
  }
}

实例化Observer对象的时候,首先初始化了一些变量value(要观察的对象),dep和vmCount。然后将自己赋值给 value.__ob__ 属性。最后,判断value的类型,如果是数组就走 observeArray() 方法,如果是其它,就走 walk() 方法。

我们先看一下其它类型,主要是对象走的 walk() 方法,很简单,就是遍历对象所有属性,然后每个属性调用 defineReactive() 方法。

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
    
    
  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 // 暂存属性之前的get和set修饰器方法
  const setter = property && property.set

  let childOb = 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
    },
    set: function reactiveSetter (newVal) {
    
    
      const value = getter ? getter.call(obj) : val
      ...
      if (setter) {
    
    
        setter.call(obj, newVal)
      } else {
    
    
        val = newVal
      }
      childOb = observe(newVal) // 触发依赖的重新收集
      dep.notify() // 触发所有依赖监听器的更新
    }
  })
}

但是,这没有Watch对象的新建流程,所以,我们现在了解的依赖收集是不完整的。接下来,我们以计算属性的Watch对象新建为例子,让Observer,Dep和Watch对象之间的关系变得清楚明白。

initComputed初始化计算属性

// 初始化数据
export function initState (vm: Component) {
    
    
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // 1. 初始化参数
  if (opts.methods) initMethods(vm, opts.methods) // 2、初始化方法
  if (opts.data) {
    
     // 3、初始化数据
    initData(vm)
  } else {
    
    
    observe(vm._data = {
    
    }, true /* asRootData */)
  }
>  if (opts.computed) initComputed(vm, opts.computed) // <-- 4、初始化计算属性
  if (opts.watch) initWatch(vm, opts.watch) // 5、初始化监听属性
}

还是再看 initState() 这个方法,我们可以看到初始化计算属性紧跟在初始化数据之后。

function initComputed (vm: Component, computed: Object) {
    
    
  const watchers = vm._computedWatchers = Object.create(null)

  for (const key in computed) {
    
    
    const userDef = computed[key]
    let getter = typeof userDef === 'function' ? userDef : userDef.get // 计算属性是函数或者拥有get/set属性的对象
    if (process.env.NODE_ENV !== 'production') {
    
    
      if (getter === undefined) {
    
    
        warn(
          `No getter function has been defined for computed property "${
      
      key}".`,
          vm
        )
        getter = noop
      }
    }
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions) // 一个计算属性对应一个Watcher对象

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
    
    
      defineComputed(vm, key, userDef)
    }
  }
}

而在 initComputed() 方法 ,首先创建一个存放所有计算属性Watch对象的 vm._computedWatchers 属性。然后在循环中将每个计算属性新建一个Watcher对象。

export default class Watcher {
    
    
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    
    
    this.vm = vm
    vm._watchers.push(this)
    ...
    // parse expression for getter
    if (typeof expOrFn === 'function') {
    
    
      this.getter = expOrFn
    } else {
    
    
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
    
    
        this.getter = function () {
    
    }
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${
      
      expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

在新建Watch对象的构造函数里面,首先把自己保存到了 vm._watchers 数组里面。这个数组保存了Vue实例所有的Watcher对象。然后将 this.getter = expOrFn 这个expOrFn函数就是 initComputed 方法里面的userDef或者userDef.get,就是用户自己定义计算属性时手写的方法,或者手写计算对象里面的get函数。最后对this.value进行赋值,这个this.value就是Watcher的返回值。也就是计算属性的返回值。

接下来我们就看看调用的this.get()方法如何得到计算属性的结果。

get () {
    
    
  pushTarget(this)
  let value
  const vm = this.vm
  if (this.user) {
    
    
    try {
    
    
      value = this.getter.call(vm, vm)
    } catch (e) {
    
    
      handleError(e, vm, `getter for watcher "${
      
      this.expression}"`)
    }
  } else {
    
    
    value = this.getter.call(vm, vm)
  }
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  if (this.deep) {
    
    
    traverse(value)
  }
  popTarget()
  this.cleanupDeps()
  return value
}
// dep.js里面导出的方法
export function pushTarget (_target: Watcher) {
    
    
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

在get方法里面首先调用了pushTarget方法,这个方法是在dep.js文件里面,主要作用就是把当前正在实例化的Watcher对象赋值到Dep.target这个全局变量里面。

之后进行了一个判断,但是无论是真还是假,都会运行语句 value = this.getter.call(vm, vm) 运行this.getter方法,就是直接运行程序员写的计算属性方法,得到方法的返回值。而在运行这个方法时就会触发data对象里面的Observer的get方法。从而触发Watcher对象的依赖收集。

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
  },
  set: function reactiveSetter (newVal) {
    
    
    ...
  }
}

在get方法里面会先判断当时Dep.target这个全局变量有没有值,很明显,在我们实例化Watcher对象的构造函数里面正好为Dep.target赋值了,所以最后运行 dep.depend() 方法。这个方法也是依赖收集的重点。

export default class Dep {
    
    
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  ...
  depend () {
    
    
    if (Dep.target) {
    
    
      Dep.target.addDep(this) // <-- 1、watcher与dep产生联系
    }
  }
  addSub (sub: Watcher) {
    
    
    this.subs.push(sub) // <-- 4、将watcher添加进subs数组
  }
}
export default class Watcher {
    
    
  ...
  addDep (dep: Dep) {
    
    
    const id = dep.id
    if (!this.newDepIds.has(id)) {
    
    
      this.newDepIds.add(id)
      this.newDeps.push(dep) // <-- 2、将dep添加进newDeps数组
      if (!this.depIds.has(id)) {
    
    
        dep.addSub(this) // <-- 3、调用dep的添加方法
      }
    }
  }
}

在dep.depend()这个方法主要就是运行一件事,就是Dep.target.addDep(this),Dep.target就是当前正在实例化的Watcher对象,所以这个语句也就是watcher.addDep(this),这个this就是计算依赖data当前属性的里面的Dep对象。Watcher对象与Dep对象终于产生了联系。

而在addDep方法里面首先将dep对象添加到了watcher对象里面的newDeps数组里面,然后调用了dep.addSub(this)方法,将watcher对象添回到dep对象里面的subs数组。

这里我们可以发现,dep对象和watcher对象是双向引用的,分别有一个数组进行保存。这样dep在属性set方法调用时,可以通知自己影响的数组里面所有的watcher更新。而watcher也知道自己依赖哪些dep对象。

而Observer对象主要是为属性代理get和set方法提供的载体。dep对象和watcher对象才是属性与视图建立的双向联系。

可能的依赖关系:

属性<–依赖–视图

属性<–依赖–计算属性<–依赖–视图

属性<–依赖–计算属性<–依赖–计算属性<–依赖–视图

视图也是由Watcher对象建立与data属性的依赖关系的。在Vue实例化后面,我们可以看到。

defineComputed 定义计算属性

计算属性也有可能被其它属性或者视图依赖,所以计算属性也应该与data里面的属性一样,也被代理才对。所以让我们继续把计算属性的逻辑看完。

// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
    
    
  defineComputed(vm, key, userDef) // <-- 计算属性后面的逻辑
}

新建Watcher对象之后,计算属性通过watcher建立与data属性的依赖关系。我们看一下之后调用 defineComputed() 方法。

const sharedPropertyDefinition = {
    
    
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function defineComputed (target: any, key: string, userDef: Object | Function) {
    
    
  if (typeof userDef === 'function') {
    
    
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {
    
    
    sharedPropertyDefinition.get = userDef.get
      ? userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

这个defineComputed方法主要功能就是在vm代理出一个计算属性让用户可以通过this.XXX访问到计算属性。其中sharedPropertyDefinition定义的计算属性的修饰器,其中定义属性可枚举,可配置,set和get。我们可以看到当计算属性为函数时get是通过createComputedGetter方法定义的,而set为空函数。不是函数时则通过userDef.get和userDef.set定义。

下面我们重点说说这个createComputedGetter方法。

function createComputedGetter (key) {
    
    
  return function computedGetter () {
    
     // 返回计算属性get代理方法
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
    
    
      if (watcher.dirty) {
    
     // ???
        watcher.evaluate()
      }
      if (Dep.target) {
    
    
        watcher.depend() // <-- 计算属性也可以触发watcher依赖收集
      }
      return watcher.value
    }
  }
}
export default class Watcher {
    
    
  depend () {
    
    
    let i = this.deps.length
    while (i--) {
    
    
      this.deps[i].depend() // <-- watcher调用自己的dep依赖数组进行其它watcher的依赖收集
    }
  }
}

可以看到createComputedGetter方法返回的是代理get方法,返回watcher.value的值。一个计算属性就有一个watcher对象。甚至可以说watcher才是计算属性的本体。因为计算,更新的逻辑都在watcher里面。计算属性本身只是watcher的壳而已。

在这个方法里面watcher对象与data里面的dep起到了相同的作用,都可以进行依赖收集。不过我们看到watcher.depend()并不直接收集,因为他也依赖其它属性,所以它直接调用自己deps数组进行收集 this.deps[i].depend(),让自己依赖的属性直接去收集。

也就是说watcher对象之间是没有直接依赖关系的,依赖被转移到了dep这一层。但是要注意,获取值的时候,计算属性还是通过自己依赖的计算获取新的值,并不能直接通过dep得到值。

dep可以通知依赖自己的watcher,watcher并不一定直接去得到dep属性的新值,也可能通过其它watcher得到新值。这样说的话,在set方法调用时,触发更新的watcher的先后顺序就成了重中之重

因为如果依赖其它计算属性的watcher先更新,那它得到的计算属性此时还没有更新,得到的value值是缓存之前的值。这样watcher实际上是没有更新的。

这个问题我们在依赖更新的时候再讨论吧。下面是简单的计算属性和data依赖图。

data属性(dep)<--依赖--计算属性(先更新)
    ^                    ^
    |                    |
   依赖<<<<<<转移<<<<<<<依赖(假)
    |                    |
计算属性(后更新) -------

Vue 数据属性依赖收集流程总结

vm._data = {
    
    
    firstName: {
    
    
        __ob__: Observer {
    
    
            dep: Dep {
    
    
                ...
                subs: Watcher [] // 依赖此项属性的所有监听器数组
            }
        },
        // 代理set方法
        set firstName: function {
    
    
            ...
            dep.notify() // 触发所有监听器的更新
        },
        // 代理get方法
        get firstName: function {
    
    
            ...
            if(Dep.target) {
    
     // 创建Watcher监听器此项有值
                dep.depend() // 进行依赖(监听器)收集
                ...
            }
            return value
        }
    }
}

数组直接赋值不能更新原因

data() {
    
    
  return {
    
    
    a: [
      {
    
    
        name: 'hello'
      }
    ]
  }
}
// 方法1 只修改数组对象里面的属性
this.a[0].name = 'world' // 修改一开始就存在的属性
this.$set(this.a[0], "age", '15') // 添加不存在的属性

// 方法2 修改数组整个对象
this.a[0] = {
    
    name: 'world'} // <-- 直接修改不生效
this.$set(this.a, 0, {
    
    name: 'world'}) // 修改数组对象
this.a.splice(0, 1, {
    
    name: 'world'}) // 另一种修改数组对象
this.a.push({
    
    name: 'world'}) // 添加数组对象
this.a.splice(0, 0, {
    
    name: 'world'}) // 另一种添加数组对象

// 方法3
this.$forceUpdate() // 直接手动刷新

猜你喜欢

转载自blog.csdn.net/cuipp0509/article/details/117252119