Vue 2.x 源码阅读记录(三):响应式系统

吐槽君 分类:javascript

写在前面

文章为阅读笔记向,需 clone 下来 Vue 源码并加以调试服用~~

Vue 的响应式:

new Vue({
  el: '#app',
  data: {
    msg: 'Hello'
  },
  methods: {
    changeMsg() {
      this.msg = 'World';
    }
  }
});
 

响应式对象

initState

在初始化_init方法中调用了initState方法,来初始化propsdata等属性,使其变成 Vue 的响应式对象。initState调用在合并配置之后。它内部集成了对propsmethodsdatacomputedwatch的初始化:

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)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
 

initProps

它做了两件事情:一是遍历传入的props,调用defineReactive将其属性变成响应式,通过定义的vm._props可以访问props中的属性;二是通过proxy代理方法将其访问路径代理到vm.props中,也就是通常访问的this.props

function initProps (vm: Component, propsOptions: Object) {
  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)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      // 将 key 转成小写
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        // 如果为保留属性则报出警告
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}
 

initData

在开头将调用data函数拿到data对象,随后它也做两件事情:一是遍历key调用proxy代理到vm.data;二是observe订阅整个data的变化:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    // data 为 function 时调用 data 返回对象
    // return data.call(vm, vm)
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        // 在 methods 中以及定义了该 data key 值
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      // props 中以及定义该 data key 值
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      // 非保留属性名,代理到 vm.data
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
 

在前面两个 init 中将propsdata都变成了响应式对象,接下来看看其中接触到的一些函数。

Proxy

在前面的例子中,是通过this.msg = 'xxx'来改变data中的数据,而this.msg实际上会被代理到this._data.msg,这就是proxy函数做的事,它使用Object.defineProperty代理了实例属性的访问路径:

const sharedPropertyDefinition = {
  enumerable: true,  // 可枚举
  configurable: true,  // 可遍历
  get: noop,
  set: noop
}

// proxy(vm, `_props`, key)
// proxy(vm, `_data`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    // 将 this.key 的访问代理到 this._xxx.key
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    // set 同理
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
 

observe

它用来监听数据(data)的变化, 给非 VNode 的对象类型数据添加一个Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer对象实例 :

// observe(vm._data = {}, true /* asRootData */)
// observe(data, true /* asRootData */)
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
}
 

Observer

它是一个类,在实例化时,它定义了一些为响应式服务的属性,实例化Dep类,调用def方法为value(这里的 value都为data) 定义一个__ob__属性,也就是打印中的可以看到的,如果value是数组则为每个子元素调用observe方法,如果是一个正常的data对象则为每个属性调用defineReactive方法:

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
    // 给 value(data) 定义一个不可枚举的 __ob__ 属性,
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // value(data) 为数组,循环调用 observe 方法
      this.observeArray(value)
    } else {
      // 为 value(data) 每个属性调用 defineReactive 方法使其变成响应式
      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])
    }
  }
}
 

dep函数使用Object.defineProperty为对象添加一个属性:

/**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
 

defineReactive

它在一开始实例化Dep拿到实例对象,再拿到obj(此处为 data 对象) 的属性描述符,然后递归为data中每个属性调用observe方法(当属性是一个对象时),这样就确保了每个属性都带有__ob__属性,都变成了响应式的, 这样我们访问或修改obj中一个嵌套较深的属性,也能触发 getter 和 setter。最后使用Object.definePropertyobjkey属性值添加gettersetter,它们做的事情就是依赖收集派发更新

// defineReactive(obj /* data */, keys[i] /* data key */)
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  // 拿到 obj 的属性描述对象
  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) {
    // 在 Observer 中 data 只传递了两个参数
    // data 中的每个属性的值
    val = obj[key]
  }
  
  // 递归地为 data 中的每个属性添加 __ob__ 对象
  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
    },
    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()
    }
  })
}
 

可以看到,初始化propsdata的过程中就是利用Object.defineProperty为数据添加gettersetter来拦截对象的读写,并递归给与__ob__对象用于追踪数据变化。


依赖收集

在给响应式对象添加完gettersetter后,对对象的读取就会执行gettergetter会进行一波依赖收集的过程,然后将访问的值返回:

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
},
 

具体流程

mountComponent执行时,会定义一个渲染Watcher,会将updateComponent函数作为Watchergetter传入:

updateComponent = () => {
    // _update() 将 vnode 生成为实际 DOM 元素
    // _render() 生成 vnode
    vm._update(vm._render(), hydrating /* false */)
}

new Watcher(vm, updateComponent, noop /* 空函数 */, {
    before () {
        if (vm._isMounted && !vm._isDestroyed) {
            // 组件已挂载并未销毁,执行 beforeUpdate 生命周期
            callHook(vm, 'beforeUpdate')
        }
    }
}, true /* isRenderWatcher */)
 

Watcher类中,会定义一些依赖相关的数组,将getter赋值为传入的updateComponent,最后会执行get方法,get方法中执行了pushTargetgetter方法:

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    //...
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        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()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    debugger
    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)
      }
      // 这里是 getter 执行完之后执行的
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
	//...
}

 

pushTarget方法会将当前渲染Watcher存储到Dep.target并添加到targetStack栈中:

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  // 当前渲染 watcher
  Dep.target = target
}
 

然后Watch会执行getter方法,也就是外部传入的updateComponent,然后结合前面可以知道,会先执行_render()走到render方法的执行,在render执行时,也就访问了data中的属性,触发了响应式对象getter开始依赖收集过程:

vnode = render.call(vm._renderProxy, vm.$createElement)
 

defineReactive中定义了一个Dep类的实例对象,首先会对这个Dep实例进行依赖收集,执行了当前Dep实例的depend方法:

// 这个 dep 是 data 中 __ob__ 中的 dep
const dep = new Dep()

if (Dep.target) {
    dep.depend()
    if (childOb) {
        // dep 在 Observer 中定义
        childOb.dep.depend()
        if (Array.isArray(value)) {
            dependArray(value)
        }
    }
 

depend方法实际执行了当前渲染WatcheraddDep方法来收集依赖:

depend () {
    if (Dep.target) {
        // Dep.target 是当前渲染 Watcherdep
        // this 是 data 中每个属性 __ob__ 中的 dep
        Dep.target.addDep(this)
    }
}
 

addDep方法通过一些判断将当前dep收集到依赖相关的数组中:

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

最后执行当前depaddSub方法将当前渲染Watcher订阅到depsubs数组中, 这个目的是为后续数据变化时候能通知到哪些subs做准备:

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

到此,已经完成了一个依赖收集的过程。此时回到Watcherget方法执行中,会接着执行finally块:

finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
        traverse(value)
    }
    // 这里是 getter 执行完之后执行的
    popTarget()
    this.cleanupDeps()
}
 

先了解一下popTarget,当前vm的依赖收集完成后,它将当前的渲染Watcher回归到上一个的状态,因为如果存在组件嵌套的关系,会递归地执行mountComponent方法:

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

暂时跳过cleanupDeps的总结

可以看到,在依赖收集的过程中当前的渲染Watcher会收集当前dep,而当前dep会收集当前渲染Watcher,这样,二者就建立了桥梁。换一种说法,Watcher作为一个观察者,它会将当前的dep收集起来,作为当前Watcher的一个订阅者,而dep则在它的subs中订阅了当前Watcher,这样,当dep对应的数据改变时,会调用depnotify通知订阅的Watcher,执行Watcher上的update方法告诉Watcher可以开始更新数据了。这是一个典型的观察者模式的实现。


派发更新

完成对数据的依赖收集之后,点击按钮进行赋值操作时会进入响应式对象的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()
    }
    // #7981: for accessor properties without setter
    if (getter && !setter) return
    if (setter) {
        setter.call(obj, newVal)
    } else {
        val = newVal
    }
    childOb = !shallow && observe(newVal)
    // notify 之前,以及将 data[val] 更新了
    dep.notify()
}
 

可以看到,setter会先获取当前需要操作的旧值,对新旧值进行对比,然后将当前值变成新值,如果新值是一个对象,则再调用observe方法将其变成响应式对象,最后调用当前depnotify方法通知Watcher开始派发更新过程。notify方法遍历了之前dep.subs订阅的Watcher,调用了它们的update方法:

notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
 

在当前流程中,update中执行了queueWatcher方法,对dep.subs下的Watcher做了一个队列操作:

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

在当前例子中只有一个渲染Watcher,所以执行queue.push分支,将当前Watcherpush 到队列中,然后调用nextTick方法执行flushSchedulerQueue, 这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue ,稍后手动添加watch的时候再来验证这一点:

const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 保证同一个 Watcher 只 push 一次
  if (has[id] == null) {
    // 存储当前 watcher.id
    has[id] = true 
    if (!flushing) {
      // 向队列中插入当前 Watcher
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      // 第二个 Watcher 进来的时候
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      // 确保下面的逻辑只执行一次
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      // 异步执行 flushSchedulerQueue
      debugger
      nextTick(flushSchedulerQueue)
    }
  }
}
 

(计算属性之后过来的补充)当在 changeMsg 中有多个赋值语句时,它们会被合并。因为它们属于同一个渲染 Watcher 。当第一个赋值语句执行时,已经进入了 has[id] == null 的逻辑,第二次赋值时这个渲染 Watcher 已经存在于 has 中了,则不会进入。又因为在这些属性的 setter 中,已经完成了新值的赋值,那么它们就只需要等待执行 nextTick(flushSchedulerQueue) 就可以了:

this.msg = 'World';
this.msg2 = 'World2';
 

先看看flushSchedulerQueue前面主要的逻辑,它会对队列中的Watcher根据自增的id进行从小到大排序,因为父组件的Watcher先于子组件的Watcher创建,手动写的watch先于渲染Watcher创建,所以要将先创建的Watcher先执行,可以考虑单向数据流的概念来理解这一点:

queue.sort((a, b) => a.id - b.id)
 

然后将队列遍历,先执行watcher.before方法,也就是在渲染Watcher执行时定义的,它内部调用了beforeUpdate生命周期函数,然后将has[id]置空,并执行watcher.run()

// queue.length 没有缓存是因为可能在 watcher.run() 的过程中插入新的 Watcher
// 参考下面的 `边界情况`
for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // mountComponent 中传入了 before
      // before 方法执行了 beforeUpdate hook
      watcher.before()
    }
    id = watcher.id
    // 清除存储的 watcher id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }
 

watcher.run方法就是重新渲染 DOM 的方法,它内部重新调用了watcher.get方法,再次执行了一遍watcher.getter,也就是渲染Watcher定义时的updateComponent方法,走一遍getter流程更新 DOM,最后拿到新旧值执行cb回调,回调对应用户手写的watch,在渲染Watcher中,回调是一个noop

run () {
    if (this.active) {
      // 这里又调用了一次 getter
      // set 中将 val 变成了新的值,这里再次调用会进入 updateComponent 的调用
      // _render() 会重新读取改变后的 val,从而渲染页面
      debugger
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
 

DOM 更新完成之后,再看看flushSchedulerQueue收尾逻辑:

// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()

resetSchedulerState()

// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)

// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
    devtools.emit('flush')
}
 

先调用resetSchedulerState重置上面的一些全局变量:

function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}
 

最后调用了两个 call hooks 函数会当前Watchervm实例执行activatedupdated生命周期:

callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
 

边界情况

如果在手动添加watch中又对监听的值进行了赋值:

watch: {
    msg() {
        this.msg = Math.random();
    }
},
 

那么它在queueWatcher函数中 push 到队列时会进入flushing = true的分支,这是因为在执行watcher.cb回调函数时,又进入的响应式对象的setter,并没有执行到watcher.run之后的resetSchedulerState重置变量的流程。此时就会向队列中当前的Watcher后一位插入一个新的Watcher,它可以看作用户手动写的Watcher的副本:

if (!flushing) {
    // 向队列中插入当前 Watcher
    queue.push(watcher)
} else {
    // if already flushing, splice the watcher based on its id
    // if already past its id, it will be run next immediately.
    let i = queue.length - 1
    while (i > index && queue[i].id > watcher.id) {
        i--
    }
    queue.splice(i + 1, 0, watcher)
}
 

由于每次run的时候又会添加一个新的Watcher,这是就会进入一个添加Watcher的死循环,最后会在watch.run()方法之后报出一个警告提示当前进入了无限更新循环:

watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
    circular[id] = (circular[id] || 0) + 1
    if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
            'You may have an infinite update loop ' + (
                watcher.user
                ? `in watcher with expression "${watcher.expression}"`
                : `in a component render function.`
            ),
            watcher.vm
        )
        break
    }
}
 

注意

在循环执行subs[i].update()方法时,会将循环的当前Watcher push 到队列中,但遇到nextTick函数时会返回循环,接着 push 下一个WatchernextTick就是派发更新操作中的关键步骤。

总结

1、更新新旧数据,调用notify开始队列操作;

2、循环调用watcher.update添加Watcher到队列;

3、添加完队列后nextTick循环队列中的Watcher执行watcher.run更新 DOM;


nextTick

nextTick是一个setter的一个核心实现,它会在 DOM 完成渲染之后执行。在 Vue 2.5 以上的版本中,在目前主流的 web 环境中nextTick函数使用Promise实现微任务(microTask)。它的整个实现都在一个next-tick.js中。

首先它定义了一些全局变量,用于控制Promise队列:

// 回调函数队列
const callbacks = []
// promise pending
let pending = false
// 微任务入口函数
let timerFunc
 

在当前环境支持Promise的情况下,使用Promise.resove()来获得一个微任务,并将timerFunc赋值为一个入口函数:

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
}
 

在微任务中,执行了fulshCallbacks函数,它将遍历执行callbacks队列:

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
 

微任务已经创建完毕了,接下来看看nextTick函数如何运用微任务去执行。它接收一个cb回调,将cb回调的调用作为一个callback 插入到callbacks回调队列中,然后开始执行timerFunc函数。最后,在没有传入cb的情况下,会返回一个Promise,也就是nextTick支持Promise调用:

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      // 相当于执行了 `this.nextTick().then(() => {})`
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
 

timerFunc会使用p.then(flushCallbacks)将所有用户传入的回调都压到一个微任务中执行,等待当前宏任务执行完毕后,会按开始读取微任务中的结果:

timerFunc = () => {
    p.then(flushCallbacks)
    // ...
}
 

按照 JS 异步的概念来说,会先执行同步的代码,比如renderpatch等过程,同步代码执行完成之后在读取异步代码执行的结果,也就是调用用户传入的回调。这也就是为什么在派发更新的过程中遇到nextTick(flushSchedulerQueue)时会回到dep.subs[i].update的循环中,flushSchedulerQueue实际是进入了异步队列,在同步代码中,组件会完成patch的过程,这就是为什么nextTick能够拿到渲染之后的 DOM,而flushSchedulerQueue又能在异步队列中按顺序执行(因为flushCallbacks循环调用了它),这也保证了父组件会先于子组件更新完毕。Vue 在nextTick中巧妙的运用了事件循环

另外,在initGlobalAPI和 Vue 初始化的的时候,向 Vue 挂载了nextTick$nextTick函数,它实际就是调用了这里的nextTick,它可以使用Vue.nextTickvm.$nextTick调用:

// initGlobalAPI
Vue.nextTick = nextTick

// renderMixin
Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
}
 

检测变化的注意事项

data中没有定义的属性是没有gettersetter的,所以访问不存在于一个对象中的值时它不会是响应式的。通常,能够使用Vue.setvm.$set来将一个属性变成响应式的,set可以给对象和数组添加响应式的属性:

var vm = new Vue({
  data:{
    a:1,
    someObj: {}
  }
})
// vm.b 与 vm.somObj.a 是非响应的
vm.b = 2
vm.someObj.a = 1

// 使其变成响应式
Vue.set(vm.someObj, 'a', 1);
 

set$set分别定义在初始化实例的时候:

// initGlobalAPI
Vue.set = set

// stateMixin
Vue.prototype.$set = set

// set
export function set (target: Array<any> | Object, key: any, val: any): any {
  debugger
  // target 不能为 undefined, null, 普通类型值
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: 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)) {
    // key 存在 target 中,直接将 val 赋值给 target
    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
}
 

对象属性

target中带有这个key的时候,说明已经在初始化响应式对象或之前的set中设置了gettersetter,则直接返回target[value]的值:

if (key in target && !(key in Object.prototype)) {
    // key 存在 target 中,直接将 val 赋值给 target
    target[key] = val
    return val
}
 

随后,它会调用defineReactiveval设置gettersetter,走一遍响应式对象的设置流程。之后,会手动调用ob.dep.notify手动派发更新。当页面没有用到这个新值的时候,dep.subs就没有订阅任何Watcher,最后直接返回val;当页面中用到了这个新值的时候,就会从notify开始派发更新,最后执行watcher.run方法来重新执行updateComponent,就会成功将新值渲染到页面上去了:

defineReactive(ob.value, key, val)
ob.dep.notify()
return val
 

数组元素

在数组的情况下,直接调用splice方法就能将val变成响应式的:

if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
}
 

其实这里的splice是 Vue 重写过的。在target被实例Observer的时候,如果遇到当前Observer的值是数组时,会进行一些处理,在大部分现代浏览器环境中,它会走到protoAugment方法,将target__proto__指向arrayMethods

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
    // 给 value(data) 定义一个不可枚举的 __ob__ 属性,
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // value(data) 为数组,循环调用 observe 方法
      this.observeArray(value)
    } else {
      // 为 value(data) 每个属性调用 defineReactive 方法使其变成响应式
      this.walk(value)
    }
  }

function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
 

arrayMethods实际就是指向了Array.prototype,然后在后面会对数组所有的原生方法进行遍历重写。这里的重写实际就是对原生方法的执行做一层拦截操作:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

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

/**
 * Intercept mutating methods and emit events
 */
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
  })
})
 

可以看到它会先执行原生的数组方法,然后将需要插入到数组中的值转为一个数组,调用observeArray循环调用observe方法使其变成响应式对象,最后调用dep.notify通知Watcher更新。

总结

set就是将响应式对象/数组中不存在的属性以对象或数组的方式最终调用defineReactive方法给它添加gettersetter。补充:响应式对象的流程:observe > Observer (Object, Array)> defineReactive,整个流程就是一个闭环。


计算属性 computed

计算属性可用于对数据的复杂计算,它会缓存计算结果:

const vm = new Vue({
  el: '#app',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
  },
  methods: 
    changeName() {
      this.firstName = 'Coven';
      // this.firstName = 'Foo';
    },
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`;
    }
  },
});
 

纠错

经过痛苦的调试与推理过程,我发现了一个之前定义响应式对象时的错误。在 defineReactivedata 中的属性设置 gettersetter 时,有一个 const dep = new Dep() 的操作,这个 dep 并不是我当时理解的是当前目标对象 __ob__ 下的 dep ,而是当前目标对象下每个属性持有的 dep。为每个属性添加一个 dep ,就能让每个属性都订阅当前的 Watcher,在当前属性更新时通知 Watcher 更新:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 这个 dep 是为 data 中每个属性创建的 dep
  const dep = new Dep()
  
  //...
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    /* 依赖收集 */
    get: function reactiveGetter () {
      if (Dep.target) {
        // 为当前属性订阅 Watcher
        dep.depend()
        // ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 当前属性已经改变,通知 Watcher 开始更新
      dep.notify()
    }
  })
}
 

初始化

首先看看初始化initComputed的过程。首先遍历用户定义的computed对象,检验定义的合法性,然后为每个计算属性方法定义一个computed watcher,存储在vm._computedWatchers中,然后判断计算属性名是否已经存在于vm下,否则为每个计算属性方法执行defineComputed

function initComputed (vm: Component, computed: Object) {
  // debugger
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    // computed 可以支持带有 set 和 get 的对象写法
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

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

    // 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)
    } else if (process.env.NODE_ENV !== 'production') {
      // key 已经在当前 vm 实例 data/props 中定义了
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
 

defineComputed的核心就是为计算属性函数添加gettersetter,开发中最常用的就是用计算属性进行复杂计算,所以这里重点看getter,它是createComputedGetter

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()  // true
  if (typeof userDef === 'function') {
    // 给当前 userDes 函数添加 getter 和 setter
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
 

createComputedGetter返回实际的getter,它的核心就是根据计算属性的key取出之前缓存的computed watcher,然后执行Watchergetter,也就是计算属性方法:

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

访问

前面知道,在_render()的过程中会访问到定义在模板中的属性,当访问到计算属性时,会触发计算属性的getter。在计算属性初始化的时候,提到过为计算属性定义了一个computed watcher

watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    // const computedWatcherOptions = { lazy: true }
    computedWatcherOptions
)
 

这里抽取出一些computed watcher定义时与渲染Watcher的不同之处,computed watcher 的关键在于 this.lazythis.dirty

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
    // options
    if (options) {
      // ...
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.dirty = this.lazy // for lazy watchers
    // ...
    // lazy 表示是一个计算属性,不同于的渲染 Watcher 的是,它不会在 new Watcher() 时进行求值
    this.value = this.lazy
      ? undefined
      : this.get()
  }
 

可以看到在最后,不会立即执行get函数求值,而是会在计算属性getter时再进行求值。回到计算属性getter,此时dirtytrue,会执行watcher.evaluate方法,evaluate 则执行了 watcher.get

if (watcher.dirty) {
    watcher.evaluate()
}
 

get 执行之前,此时的 Dep.target 是渲染 Watcherevaluate 方法执行了 Watcherget,也就是执行了计算属性函数,最终返回计算属性的值,在例子中是return ${this.firstName} ${this.lastName}。开始执行 get 时,此时的 Dep.target 是当前的 computed watcher,在执行计算属性函数时,对 this.firstNamethis.lastName 进行了访问,触发了它们的 getter,随后它们所属的 dep 对象对当前 computed watcher 进行依赖收集,订阅到 dep.subs 中:

get () {
    // debugger
    // computed watcher 接管当前流程
    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)
      }
      // 这里是 getter 执行完之后执行的
      // computed watcher 内会访问 this.xxx 属性,因此会触发 this.xxx 的 getter
      // 最终 value 拿到 computed 中 userDef 的返回值
      // 将当前 computed watcher 出栈,Dep.target 交还给上一个 Watcher
      // targetStack 是后进先出顺序
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
 

到这里,已经拿到计算属性的计算结果了,当渲染流程完成时,结果会显示到页面上。接下来,看看对计算属性依赖的值进行更改,它又是如何进行计算的:

changeName() {
    this.firstName = 'Coven';
},
 

它触发了 firstNamesetter,然后执行 dep.notify,此时的 dep.subs 中订阅了渲染 Watchercomputed watcher ,然后将它们循环执行 update。渲染 Watcher 会走正常的 queueWatcher 流程,computed watcher 则只会将 dirty 设为 true

update () {
    /* istanbul ignore else */
    if (this.lazy) {
        // 当前为 computed watcher 时,先不进入派发更新流程
        this.dirty = true
    } else if (this.sync) {
        this.run()
    } else {
        queueWatcher(this)
    }
}
 

dep.notify 执行完成后,会开始执行更新,此时又会执行到 _render(),在 _render() 的过程中,又会访问到计算属性,开始执行计算属性的 getter

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

经过 update 的流程,此时的 dirtytrue,接下来就开始走计算属性访问的流程。在 firstNamesetter 中,已经执行 val = newVal 将它的值更新了,所以这里 computed watchergetter 会拿到最新的 firstName 值,最后渲染到页面上。

结合上面的流程调试到最后,我发现,只有当计算属性中依赖(dep 中订阅了 computed watcher)的值发生变化时,并且 _render() 流程再次访问到计算属性时,才会进行重新计算。


侦听属性 Watch

在响应式系统中,用户可以手动定义一个 watch 来监听数据的变化,它是一个 user watcher,接下来看看它的实现原理:

watch: {
    msg() {
        // this.msg = Math.random();
        console.log('msg changed.');
    }
},
 

初始化

在初始化的 initState 中,如果用户定义了 watch,则会执行 initWatch 来初始化 user watcher。它会遍历 watch 属性,拿到 watch 中每一个函数/对象,对函数或对象中的 handler 执行 createWatcher 方法:

function initWatch (vm: Component, watch: Object) {
  debugger
  for (const key in watch) {
    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 对参数进行了一些规范处理,最终调用初始化 stateMixin 中定义的 $watch 方法,实际上用户定义在选项中的 watch 也是调用了 $watch API:

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
 

$watch 中实例化了一个 Watcher,注意 options.user = true,表示这是一个用户定义的 watcher,它先于渲染 Watcher 被订阅:

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
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
 

那么这时在 Watcher 被实例化的时候,expOrFn 是用户传入的 watch 函数的函数名,cbwatch 函数本身,usertrue,注意这里的 expOrFn 是一个字符串,会调用 parsePath(expOrFn) 将其转换成函数赋值给 getter,最后调用 get 方法求值:

get () {
    // debugger
    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)
        }
        // 这里是 getter 执行完之后执行的
        // computed watcher 内会访问 this.xxx 属性,因此会触发 this.xxx 的 getter
        // 最终 value 拿到 computed 中 userDef 的返回值
        // 将当前 computed watcher 出栈,Dep.target 交还给上一个 Watcher
        // targetStack 是后进先出顺序
        popTarget()
        this.cleanupDeps()
    }
    return value
}
 

实例化过程中,this.getter = parsePath(expOrFn) 是最重要的一步。parsePath 会先校验是不是一个合法的函数名,然后将函数名用 . 分割为一个字符串数组,因为用户可以采用 someObj.foo 这种方式定义 watch 的函数名,以监听对象内的某个属性,最后它返回一个函数,这个函数就是 Watchergetter

在上面 getter 调用时传入的是当前的 vm 实例,就是下面的 obj,然后循环 segments,在循环中将 obj[segments[i]] 的值赋值给 obj,适配了两种 watch 函数名的定义:一是 msg(n, o) {};二是 'someObj.msg'(n, o) {}。这是一个很巧妙的方式:

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
 

回到 get 函数,当执行 getter 的时候实际是对 data 中的数据做了访问,触发了数据的 getter,然后数据的 dep 对象就会订阅当前的 user watcher,当更新时就会通知它进行派发更新过程。

执行 watch

在看派发更新的时候,知道数据更新会触发数据的 setter,进行派发更新过程。当 watch 监听了一个属性的时候,这个属性的 dep.subs 中就会在初始化 watch 时订阅这个 user watcher,那么到这个 user watch 执行 run 的时候,它就会进入 this.user = true 的逻辑,执行 cb,也就是用户定义的 watch,返回新旧值。这一点在看派发更新时粗略地介绍过:

if (this.user) {
    try {
        this.cb.call(this.vm, value, oldValue)
    } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
    }
} else {
    this.cb.call(this.vm, value, oldValue)
}
 

deep watch

在定义一个 watch 时候,可以设置一个 deep: true 属性来监听一个对象或数组中的所有属性变化:

watch: {
    someObj: {
        deep: true,
        handler() {}
    }
}
 

user watcher 初始化调用 getter 最后的 finally 块中,会执行这一段逻辑,也就是 deep: true 的实现:

if (this.deep) {
    traverse(value)
}
 

traverse 实际调用了 _traverse,首先判断当前 user watch 监听的不是对象或数组,不是则直接返回。它遍历 value 中所有的属性,并递归地调用自身,调用时实际又触发了属性的 getter,给每个属性的 dep.subs 订阅当前的 user watcher,这样,当 value 内部的属性发生改变时也能触发 watch 的回调。 这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
 

总结 computed 和 watch

computedwatch 实际上都是 Watcher 类的实例对象。目前为止,已经接触过三个 Watcher 的不同使用方式:渲染 Watchercomputed watcheruser watcher它们的变化实质都是 _render() 方法触发数据的 getter 依赖收集,赋值操作触发数据的 setter 派发更新。 就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

回复

我来回复
  • 暂无回复内容