阅读 302

Vue数据绑定简析

作为MVVM框架的一种,Vue最为人津津乐道的当是数据与视图的绑定,将直接操作DOM节点变为修改data数据,利用Virtual DomDiff对比新旧视图,从而实现更新。不仅如此,还可以通过Vue.prototype.$watch来监听data的变化并执行回调函数,实现自定义的逻辑。虽然日常的编码运用已经驾轻就熟,但未曾去深究技术背后的实现原理。作为一个好学的程序员,知其然更要知其所以然,本文将从源码的角度来对Vue响应式数据中的观察者模式进行简析。

初始化Vue实例

在阅读源码时,因为文件繁多,引用复杂往往使我们不容易抓住重点,这里我们需要找到一个入口文件,从Vue构造函数开始,抛开其他无关因素,一步步理解响应式数据的实现原理。首先我们找到Vue构造函数:

// src/core/instance/index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
复制代码
// src/core/instance/init.js
Vue.prototype._init = function (options) {
    ...
    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    // 初始化vm实例的$options
    if (options && options._isComponent) {
        initInternalComponent(vm, options)
    } else {
        vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
        )
    }
    ...
    initLifecycle(vm) // 梳理实例的parent、root、children和refs,并初始化一些与生命周期相关的实例属性
    initEvents(vm) // 初始化实例的listeners
    initRender(vm) // 初始化插槽,绑定createElement函数的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)  // 挂载组件到节点
    }
}
复制代码

为了方便阅读,我们去除了flow类型检查和部分无关代码。可以看到,在实例化Vue组件时,会调用Vue.prototype._init,而在方法内部,数据的初始化操作主要在initState(这里的initInjectionsinitProvideinitProps类似,在理解了initState原理后自然明白),因此我们重点来关注initState

// src/core/instance/state.js
export function initState (vm) {
  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)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
复制代码

首先初始化了一个_watchers数组,用来存放watcher,之后根据实例的vm.$options,相继调用initPropsinitMethodsinitDatainitComputedinitWatch方法。

initProps

function initProps (vm, propsOptions) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    ...
    defineReactive(props, key, value)
    if (!(key in vm)) {
      proxy(vm, '_props', key)
    }
  }
  toggleObserving(true)
}
复制代码

在这里,vm.$options.propsData是通过父组件传给子组件实例的数据对象,如<my-element :item="false"></my-element>中的{item: false},然后初始化vm._propsvm.$options._propKeys分别用来保存实例的props数据和keys,因为子组件中使用的是通过proxy引用的_props里的数据,而不是父组件传递的propsData,所以这里缓存了_propKeys,用来updateChildComponent时能更新vm._props。接着根据isRoot是否是根组件来判断是否需要调用toggleObserving(false),这是一个全局的开关,来控制是否需要给对象添加__ob__属性。这个相信大家都不陌生,一般的组件的data等数据都包含这个属性,这里先不深究,等之后和defineReactive时一起讲解。因为props是通过父传给子的数据,在父元素initState时已经把__ob__添加上了,所以在不是实例化根组件时关闭了这个全局开关,待调用结束前在通过toggleObserving(true)开启。

之后是一个for循环,根据组件中定义的propsOptions对象来设置vm._props,这里的propsOptions就是我们常写的

export default {
    ...
    props: {
        item: {
            type: Object,
            default: () => ({})
        }
    }
}
复制代码

循环体内,首先

const value = validateProp(key, propsOptions, propsData, vm)
复制代码

validateProp方法主要是校验数据是否符合我们定义的type,以及在propsData里未找到key时,获取默认值并在对象上定义__ob__,最后返回相应的值,在这里不做展开。

这里我们先跳过defineReactive,看最后

if (!(key in vm)) {
  proxy(vm, '_props', key)
}
复制代码

其中proxy方法:

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

vm不存在key属性时,通过Object.defineProperty使得我们能通过vm[key]访问到vm._props[key]

defineReactive

initProps中,我们了解到其首先根据用户定义的vm.$options.props对象,通过对父组件设置的传值对象vm.$options.propsData进行数据校验,返回有效值并保存到vm._props,同时保存相应的keyvm.$options._propKeys以便进行子组件的props数据更新,最后利用getter/setter存取器属性,将vm[key]指向对vm._props[key]的操作。但其中跳过了最重要的defineReactive,现在我们将通过阅读defineReactive源码,了解响应式数据背后的实现原理。

// src/core/observer/index.js
export function defineReactive (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  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) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  ...
}
复制代码

首先const dep = new Dep()实例化了一个dep,在这里利用闭包来定义一个依赖项,用以与特定的key相对应。因为其通过Object.defineProperty重写target[key]getter/setter来实现数据的响应式,因此需要先判断对象keyconfigurable属性。接着

if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
}
复制代码

arguments.length === 2意味着调用defineReactive时未传递val值,此时valundefined,而!getter || setter判断条件则表示如果在property存在getter且不存在setter的情况下,不会获取key的数据对象,此时valundefined,之后调用observe时将不对其进行深度观察。正如之后的setter访问器中的:

if (getter && !setter) return
复制代码

此时数据将是只读状态,既然是只读状态,则不存在数据修改问题,继而无须深度观察数据以便在数据变化时调用观察者注册的方法。

Observe

defineReactive里,我们先获取了target[key]descriptor,并缓存了对应的gettersetter,之后根据判断选择是否获取target[key]对应的val,接着是

let childOb = !shallow && observe(val)
复制代码

根据shallow标志来确定是否调用observe,我们来看下observe函数:

// src/core/observer/index.js
export 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 if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
复制代码

首先判断需要观察的数据是否为对象以便通过Object.defineProperty定义__ob__属性,同时需要value不属于VNode的实例(VNode实例通过Diff补丁算法来实现实例对比并更新)。接着判断value是否已有__ob__,如果没有则进行后续判断:

  • shouldObserve:全局开关标志,通过toggleObserving来修改。
  • !isServerRendering():判断是否服务端渲染。
  • (Array.isArray(value) || isPlainObject(value)):数组和纯对象时才允许添加__ob__进行观察。
  • Object.isExtensible(value):判断value是否可扩展。
  • !value._isVue:避免Vue实例被观察。

满足以上五个条件时,才会调用ob = new Observer(value),接下来我们要看下Observer类里做了哪些工作

// src/core/observer/index.js
export class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      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) {
    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) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
复制代码

构造函数里初始化了valuedepvmCount三个属性,为this.value添加__ob__对象并指向自己,即value.__ob__.value === value,这样就可以通过value__ob__对象取到depvaluevmCount的作用主要是用来区分是否为Vue实例的根datadep的作用这里先不介绍,待与getter/setter里的dep一起解释。

接着根据value是数组还是纯对象来分别调用相应的方法,对value进行递归操作。当value为纯对象时,调用walk方法,递归调用defineReactive。当value是数组类型时,首先判断是否有__proto__,有就使用__proto__实现原型链继承,否则用Object.defineProperty实现拷贝继承。其中继承的基类arrayMethods来自src/core/observer/array.js

// src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
复制代码

这里为什么要对数组的实例方法进行重写呢?代码里的methodsToPatch这些方法并不会返回新的数组,导致无法触发setter,因而不会调用观察者的方法。所以重写了这些变异方法,使得在调用的时候,利用observeArray对新插入的数组元素添加__ob__,并能够通过ob.dep.notify手动通知对应的被观察者执行注册的方法,实现数组元素的响应式。

if (asRootData && ob) {
    ob.vmCount++
}
复制代码

最后添加这个if判断,在Vue实例的根data对象上,执行ob.vmCount++,这里主要为了后面根据ob.vmCount来区分是否为根数据,从而在其上执行Vue.setVue.delete

getter/setter

在对val进行递归操作后(假如需要的话),将obj[key]的数据对象封装成了一个被观察者,使得能够被观察者观察,并在需要的时候调用观察者的方法。这里通过Object.defineProperty重写了obj[key]的访问器属性,对getter/setter操作做了拦截处理,defineReactive剩余的代码具体如下:

...
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) {
    ...
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})
复制代码

首先在getter调用时,判断Dep.target是否存在,若存在则调用dep.depend。我们先不深究Dep.target,只当它是一个观察者,比如我们常用的某个计算属性,调用dep.depend会将dep当做计算属性的依赖项存入其依赖列表,并把这个计算属性注册到这个dep。这里为什么需要互相引用呢?这是因为一个target[key]可以充当多个观察者的依赖项,同时一个观察者可以有多个依赖项,他们之间属于多对多的关系。这样当某个依赖项改变时,我们可以根据dep里维护的观察者,调用他们的注册方法。现在我们回过头来看Dep

// src/core/observer/dep.js
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 () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    ...
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
复制代码

构造函数里,首先添加一个自增的uid用以做dep实例的唯一性标志,接着初始化一个观察者列表subs,并定义了添加观察者方法addSub和移除观察者方法removeSub。可以看到其在getter中调用的depend会将当前这个dep实例添加到观察者的依赖项,在setter里调用的notify会执行各个观察者注册的update方法,Dep.target.addDep这个方法将在之后的Watcher里进行解释。简单来说就是会在keygetter触发时进行dep依赖收集到watcher并将Dep.target添加到当前dep的观察者列表,这样在keysetter触发时,能够通过观察者列表,执行观察者的update方法。

当然,在getter中还有如下几行代码:

if (childOb) {
    childOb.dep.depend()
    if (Array.isArray(value)) {
        dependArray(value)
    }
}
复制代码

这里可能会有疑惑,既然已经调用了dep.depend,为什么还要调用childOb.dep.depend?两个dep之间又有什么关系呢?

其实这两个dep的分工是不同的。对于数据的增、删,利用childOb.dep.notify来调用观察者方法,而对于数据的修改,则使用的dep.notify,这是因为setter访问器无法监听到对象数据的添加和删除。举个例子:

const data = {
    arr: [{
        value: 1
    }],
}

data.a = 1; // 无法触发setter
data.arr[1] = {value: 2}; // 无法触发setter
data.arr.push({value: 3}); // 无法触发setter
data.arr = [{value: 4}]; // 可以触发setter
复制代码

还记得Observer构造函数里针对数组类型value的响应式转换吗?通过重写value原型链,使得对于新插入的数据:

if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
复制代码

将其转换为响应式数据,并通过ob.dep.notify来调用观察者的方法,而这里的观察者列表就是通过上述的childOb.dep.depend来收集的。同样的,为了实现对象新增数据的响应式,我们需要提供相应的hack方法,而这就是我们常用的Vue.set/Vue.delete

// src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
复制代码
  • 判断value是否为数组,如果是,直接调用已经hack过的splice即可。
  • 是否已存在key,有的话说明已经是响应式了,直接修改即可。
  • 接着判断target.__ob__是否存在,如果没有说明该对象无须深度观察,设置返回当前的值。
  • 最后,通过defineReactive来设置新增的key,并调用ob.dep.notify通知到观察者。

现在我们了解了childOb.dep.depend()是为了将当前watcher收集到childOb.dep,以便在增、删数据时能通知到watcher。而在childOb.dep.depend()之后还有:

if (Array.isArray(value)) {
    dependArray(value)
}
复制代码
/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
复制代码

在触发target[key]getter时,如果value的类型为数组,则递归将其每个元素都调用__ob__.dep.depend,这是因为无法拦截数组元素的getter,所以将当前watcher收集到数组下的所有__ob__.dep,这样当其中一个元素触发增、删操作时能通知到观察者。比如:

const data = {
    list: [[{value: 0}]],
};
data.list[0].push({value: 1});
复制代码

这样在data.list[0].__ob__.notify时,才能通知到watcher

target[key]getter主要作用:

  • Dep.target收集到闭包中dep的观察者列表,以便在target[key]setter修改数据时通知观察者
  • 根据情况对数据进行遍历添加__ob__,将Dep.target收集到childOb.dep的观察者列表,以便在增加/删除数据时能通知到观察者
  • 通过dependArray将数组型的value递归进行观察者收集,在数组元素发生增、删、改时能通知到观察者

target[key]setter主要作用是对新数据进行观察,并通过闭包保存到childOb变量供getter使用,同时调用dep.notify通知观察者,在此就不再展开。

Watcher

在前面的篇幅中,我们主要介绍了defineReactive来定义响应式数据:通过闭包保存depchildOb,在getter时来进行观察者的收集,使得在数据修改时能触发dep.notifychildOb.dep.notify来调用观察者的方法进行更新。但具体是如何进行watcher收集的却未做过多解释,现在我们将通过阅读Watcher来了解观察者背后的逻辑。

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

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    ...
  }
}
复制代码

这是Vue计算属性的初始化操作,去掉了一部分不影响的代码。首先初始化对象vm._computedWatchers用以存储所有的计算属性,isSSR用以判断是否为服务端渲染。再根据我们编写的computed键值对循环遍历,如果不是服务端渲染,则为每个计算属性实例化一个Watcher,并以键值对的形式保存到vm._computedWatchers对象,接下来我们主要看下Watcher这个类。

Watcher的构造函数

构造函数接受5个参数,其中当前Vue实例vm、求值表达式expOrFn(支持Function或者String,计算属性中一般为Function),回调函数cb这三个为必传参数。设置this.vm = vm用以后续绑定this.getter的执行环境,并将this推入vm._watchers(vm._watchers用以维护实例vm中所有的观察者),另外根据是否为渲染观察者来赋值vm._watcher = this(常用的render即为渲染观察者)。接着根据options进行一系列的初始化操作。其中有几个属性:

  • this.lazy:设置是否懒求值,这样能保证有多个被观察者发生变化时,能只调用求值一次。
  • this.dirty:配合this.lazy,用以标记当前观察者是否需要重新求值。
  • this.depsthis.newDepsthis.depIdsthis.newDepIds:用以维护被观察对象的列表。
  • this.getter:求值函数。
  • this.value:求值函数返回的值,即为计算属性中的值。

Watcher的求值

因为计算属性是惰性求值,所以我们继续看initComputed循环体:

if (!(key in vm)) {
  defineComputed(vm, key, userDef)
}
复制代码

defineComputed主要将userDef转化为getter/setter访问器,并通过Object.definePropertykey设置到vm上,使得我们能通过this[key]直接访问到计算属性。接下来我们主要看下userDef转为getter中的createComputedGetter函数:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
复制代码

利用闭包保存计算属性的key,在getter触发时,首先通过this._computedWatchers[key]获取到之前保存的watcher,如果watcher.dirtytrue时调用watcher.evaluate(执行this.get()求值操作,并将当前watcherdirty标记为false),我们主要看下get操作:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (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
}
复制代码

可以看到,求值时先执行pushTarget(this),通过查阅src/core/observer/dep.js,我们可以看到:

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
复制代码

pushTarget主要是把watcher实例进栈,并赋值给Dep.target,而popTarget则相反,把watcher实例出栈,并将栈顶赋值给Dep.targetDep.target这个我们之前在getter里见到过,其实就是当前正在求值的观察者。这里在求值前将Dep.target设置为watcher,使得在求值过程中获取数据时触发getter访问器,从而调用dep.depend,继而执行watcheraddDep操作:

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)
    }
  }
}
复制代码

先判断newDepIds是否包含dep.id,没有则说明尚未添加过这个dep,此时将depdep.id分别加到newDepIdsnewDeps。如果depIds不包含dep.id,则说明之前未添加过此dep,因为是双向添加的(将dep添加到watcher的同时也需要将watcher收集到dep),所以需要调用dep.addSub,将当前watcher添加到新的dep的观察者队列。

if (this.deep) {
  traverse(value)
}
复制代码

再接着根据this.deep来调用traversetraverse的作用主要是递归遍历触发valuegetter,调用所有元素的dep.depend()并过滤重复收集的dep。最后调用popTarget()将当前watcher移出栈,并执行cleanupDeps

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  ...
}
复制代码

遍历this.deps,如果在newDepIds中不存在dep.id,则说明新的依赖里不包含当前dep,需要到dep的观察者列表里去移除当前这个watcher,之后便是depIdsnewDepIdsdepsnewDeps的值交换,并清空newDepIdsnewDeps。到此完成了对watcher的求值操作,同时更新了新的依赖,最后返回value即可。

回到createComputedGetter接着看:

if (Dep.target) {
  watcher.depend()
}
复制代码

当执行计算属性的getter时,有可能表达式中还有别的计算属性依赖,此时我们需要执行watcher.depend将当前watcherdeps添加到Dep.target即可。最后返回求得的watcher.value即可。

总的来说我们从this[key]触发watcherget函数,将当前watcher入栈,通过求值表达式将所需要的依赖dep收集到newDepIdsnewDeps,并将watcher添加到对应dep的观察者列表,最后清除无效dep并返回求值结果,这样就完成了依赖关系的收集。

Watcher的更新

以上我们了解了watcher的依赖收集和dep的观察者收集的基本原理,接下来我们了解下dep的数据更新时如何通知watcher进行update操作。

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.notify时,我们将this.subs拷贝出来,防止在watcherget时候subs发生更新,之后调用update方法:

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
复制代码
  • 如果是lazy,则将其标记为this.dirty = true,使得在this[key]getter触发时进行watcher.evaluate调用计算。
  • 如果是sync同步操作,则执行this.run,调用this.get求值和执行回调函数cb
  • 否则执行queueWatcher,选择合适的位置,将watcher加入到队列去执行即可,因为和响应式数据无关,故不再展开。

小结

因为篇幅有限,只对数据绑定的基本原理做了基本的介绍,在这画了简单的流程图和数据流向图来帮助理解Vue的响应式数据,其中省略了一些VNode等不影响理解的逻辑及边界条件,尽可能简化地让流程更加直观:

最后,本着学习的心态,在写作的过程中也零零碎碎的查阅了很多资料,其中难免出现纰漏以及未覆盖到的知识点,如有错误,还请不吝指教。

关注下面的标签,发现更多相似文章
评论