Vue2.0源码分析:响应式原理(下)

时间:2021-1-8 作者:admin

Vue2.0源码分析

如果觉得写得不错,请到GitHub我一个Star

上一篇:Vue2.0源码分析:响应式原理(上)

深入响应式原理

在介绍完propsdatawatch以及computed后,我们对响应式原理有了一定的初步认识,在这一章节中我们再次回顾响应式,来探究其实现原理。

在之前的章节中,我们以及介绍过:Vue.js通过Object.defineProperty(obj, key, descriptor)方法来定义响应式对象,我们可以在Can I Use网站上搜索到,IE8浏览器并不支持这个方法,这就是Vue.js不支持IE8及其以下版本浏览器的真正原因。

MDN网站上,我们可以发现这个方法支持很多个参数,其中descriptor支持许多个可选的属性,对于Vue.js实现响应式对象来说,最重要的是getset属性。

let val = 'msg'
const reactiveObj = {}
 Object.defineProperty(reactiveObj, msg, {
   get: function () {
     // 当访问reactiveObj.msg时被调用
     return val
   },
   set: function (newVal) {
     // 当设置reactiveObj.msg时被调用
     val = newVal
   }
 })

Vue的响应式对象中,它会在getter收集依赖、在setter派发更新,我们会在之后的章节中分别对getter收集依赖setter派发更新做单独的讲解。

在介绍完Object.defineProperty,我们来回答一个问题,什么是响应式对象?在Vue.js中对于什么是响应式对象,我们可以简单的理解成:用Object.defineProperty()方法定义时同时提供了getset选项,我们就可以将其称之为响应式对象。

Vue.js实例化时,会把propsdatacomputed等变成响应式对象,在介绍响应式对象时,我们会重点介绍propsdata的处理过程,这个过程发生在this._init()方法中的initState(vm)中。

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相关的逻辑的:

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

在之前分析initProps整体流程的过程中,我们知道initProps主要做三件事情:props校验和求值props响应式props代理。对于props代理而言它很简单,主要作用是方便我们取值。

proxy代理

proxy()方法是定义在src/core/instance/state.js文件中:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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)
}

代码分析:

  • noop:它代表空函数,空函数代表什么都不做。
  • target:它是目标代理对象,在Vue.js中就是Vue实例。
  • sourceKey:它是源属性,在props代理中传递的是_props内部私有属性。
  • key:它是要代理的属性,在props中就是我们撰写的各种props属性。
  • sharedPropertyDefinition:它就是Object.defineProperty(obj, key, descriptor)方法的descriptor参数,可以从上面代码中看到,在props代理中它提供了enumerableconfigurablegetset这几个选项。

假设我们有如下Vue实例:

export default {
  props: ['msg', 'age']
}

proxy代理后,我们就能通过this.msgthis.age代替this._props.msgthis._props.age的形式直接访问或者设置值:

// 代理前
const msg = this._props.msg
console.log(msg)
// 单项数据流,只要演示,实际不能修改props的值
this._props.msg = 'new msg

// 代理后
const msg = this.msg
console.log(msg)
// 单项数据流,只要演示,实际不能修改props的值
this.msg = 'new msg'

以上就是props的代理过程分析,对于data代理而言是相同的道理,这里就不再累述。

defineReactive

在介绍完proxy代理后,我们紧接着要分析defineReactive的实现逻辑,关于响应式的代码实现,绝大多数是在src/core/observer目录下,其中defineReactive方法是定义在其目录的index.js入口文件中

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) {
    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
    },
    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()
    }
  })
}

代码分析:

  • defineReactive实际上就是对Object.defineProperty()方法的一层包裹,主要是处理gettersetter相关的逻辑。
  • defineReactive首先通过Object.getOwnPropertyDescriptor()方法获取了当前obj.key的属性描述,如果其属性configurablefalse,则不能被定义为响应式对象,因此对于obj.key任何赋值都不会触发组件更新,例如:
export default {
  data () {
    return {
      obj: {}
    }
  },
  created () {
    const obj = {}
    Object.defineProperty(obj, 'msg', {
      configurable: false,
      value: 'msg'
    })
    this.obj = obj
    setTimeout(() => {
      // this.obj.msg不是响应式对象,修改后不会触发组件更新
      this.obj.msg = 'new msg'
    }, 3000)
  }
}

observe和Observer

我们可以在defineReactive中看到observe(val)这段代码,接下来让我们介绍observe()方法以及Observer类。observe()方法定义与defineReactive()方法定义在同一个文件中,其代码如下:

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
}

代码分析:

  • 首先对传递的value进行了类型判断,不为对象或者是VNode实例时不进行任何操作,其中VNode是一个类,它会在生成虚拟DOM的时候使用到,我们会在后面进行介绍,isObject是一个定义在src/shared/utils.js文件中的工具方法。
export function isObject (obj: mixed): boolean {
  return obj !== null && typeof obj === 'object'
}
  • 然后对value使用hasOwn判断是否有__ob__属性且__ob__Observer实例,添加这个属性是为了防止重复观察(避免重复定义响应式),既:如果已经是响应式对象了,直接返回,否则才会进行下一步操作。hasOwn是一个定义在src/shared/utils.js文件中的工具方法:
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
  return hasOwnProperty.call(obj, key)
}
  • 最后value又进行了一些条件判断,其中最重要的两个条件为Array.isArrayisPlainObject,它们分别判断value是否为数组,是否为普通对象,其它几个边界条件暂时不做介绍。其中isPlainObject是一个定义在src/shared/utils.js文件中的工具方法:
export function isPlainObject (obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

接下来,我们需要看一下Observer类的实现:

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)) {
      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: 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])
    }
  }
}

代码分析:

  • def为定义在src/core/utils/lang.js文件中的一个工具方法,def本质上也是对Object.defineProperty()方法的一层包裹封装,使用def定义__ob__的目的是让__ob__在对象属性遍历的时候不可被枚举出来。
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
  • Vue.js中对于纯对象和数组的响应式的处理方式是不同的,代码首先判断了value是否为数组。如果不是数组,则调用walk()方法。walk()方法实际上就是递归遍历对象属性,然后调用defineReactive()的过程,例如:
const nestedObj = {
  a: {
    b: {
      c: 'c'
    }
  }
}
// 递归调用
defineReactive(nestedObj)
defineReactive(a)
defineReactive(b)
defineReactive(c)

如果是数组,则调用observeArray()方法,observeArray也是一个遍历递归调用的过程,只不过这里遍历的是数组,而不是对象的属性键。然后我们还发现,在observeArray()方法调用之前,还进行了hasProto判断,然后根据判断结果进行不同的操作。其中,hasProto是定义在src/core/util/env.js文件中的一个常量,它的目的就是为了判断当前浏览器是否支持__proto__属性:

export const hasProto = '__proto__' in {}

我们都知道因为原生API某些限制因素,Vue.js对数组七种可以改变自身数组的方法提供了变异方法支持,这七种方位分别为:

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

对这七种方法的变异处理逻辑在src/core/ovserver/array.js文件中:

import { def } from '../util/index'
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
  })
})

代码分析:

  • 首先以Array.prototype原型创建一个新的变量,这个变量会在protoAugment或者copyAugment方法的时候使用到。
  • 然后遍历七种方法,使用def来重新定义一个包裹方法。也就是说:当我们调用这七种任意一种方法的时候,首先调用我们的包裹方法,在包裹方法里面再调用原生对应的数组方法,这样做的目的是让我们可以在这个包裹方法中做我们自己的事情,例如notify,这个过程可以使用以下伪代码实例描述:
// Array.prototype.push方法为例
function mutatorFunc (value) {
  const result = Array.prototype.push(value)
  // do something
  return result
}
export default {
  data () {
    return {
      arr: []
    }
  },
  created () {
    this.arr.push('123')
    // 相当于
    mutatorFunc(123)
  }
}

然后我们接下来看一下protoAugmentcopyAugment的实现,首先是最简单的protoAugment

// 定义
const arr = []
export const arrayMethods = Object.create(arrayProto)
function protoAugment (target, src: Object) {
  target.__proto__ = src
}

// 调用
protoAugment(arr, arrayMethods)

// 调用后
arr.__proto__ = {
  // 省略其它
  push: function () {},
  pop: function () {},
  shift: function () {},
  unshift: function () {},
  splice: function () {},
  sort: function () {},
  reverse: function () {}
}
arr.push()
arr.pop()
arr.shift()
arr.unshift()
arr.splice()
arr.sort()
arr.reverse()

代码分析:当浏览器支持__proto__属性的时候,直接把__proto__指向我们创建的arrayMethods变量,这个包含我们在上面定义的七种变异方法。

当浏览器不支持__proto__属性的时候,我们就调用copyAugment方法:

// 定义
const arr = []
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export const arrayMethods = Object.create(arrayProto)
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

// 调用
copyAugment(value, arrayMethods, arrayKeys)

// 调用后
arr = {
  // 省略其它
  push: function () {},
  pop: function () {},
  shift: function () {},
  unshift: function () {},
  splice: function () {},
  sort: function () {},
  reverse: function () {}
}
arr.push()
arr.pop()
arr.shift()
arr.unshift()
arr.splice()
arr.sort()
arr.reverse()

代码分析:我们可以从代码中看到,当浏览器不支持__proto__的时候,会把我们创建的arrayMethods变量上所有的key,遍历赋值到value数组上。

依赖收集

在这一节中,我们来介绍依赖收集,在介绍之前我们需要知道什么是依赖收集,以及依赖收集的目的。

问:什么是依赖收集?依赖收集的目的是什么?
答:依赖收集就是对订阅数据变化的Watcher收集的过程。其目的是当响应式数据发生变化,触发它们的setter时,能够知道应该通知哪些订阅者去做相应的逻辑处理。例如,当在template模板中使用到了某个响应式变量,在组件初次渲染的时候,对这个响应式变量而言,应该收集render watcher依赖,当其数据发生变化触发setter时,要通知render watcher进行组件的重新渲染。

在之前我们提到过,依赖收集发生在Object.defineProperty()getter中,我们回顾一下defineReactive()代码:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 省略代码
  const dep = new Dep()
  // 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)
  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
    }
  })
}

我们可以从代码中看到,当触发getter的时候,首先判断了Dep.target是否存在,如果存在则调用dep.depend()dep.depend()函数就是依赖真正收集的地方。在阅读完以上代码后,我们可能会有这样几个疑问:

  • Dep是什么?
  • Dep.target是什么?
  • dep.depend是如何进行依赖收集的?又是如何进行依赖移除的?

Dep

让我们首先来回答第一个问题,介绍一下Dep类,Dep类是定义在observer目录下dep.js文件中的一个类,observer目录结构如下:

|-- observer       
|   |-- array.js
|   |-- dep.js
|   |-- index.js
|   |-- scheduler.js
|   |-- traverse.js
|   |-- watcher.js

然后,我们来看一下Dep类的具体定义:

let uid = 0
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()
    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()
    }
  }
}

代码分析:

  • Dep类首先定义了一个静态属性target,它就是Dep.target,我们会在之后介绍它。然后又定义了两个实例属性,idDep的主键,会在实例化的时候自增,subs是一个存储各种Watcher的数组。例如render watcheruser watchercomputed watcher等。
  • addSubremoveSub对应的就是往subs数组中添加和移除各种Watcher
  • depend为依赖收集过程。
  • notify当数据发生变化触发setter的时候,有一段这样的代码:dep.notify(),它的目的就是当这个响应式数据发生变化的时候,通知subs里面的各种watcher,然后执行其update()方法。这属于派发更新的过程,我们会在之后的章节介绍。

在介绍完以上几个属性和方法后,我们就对Dep是什么以及它做哪些事情有了一个具体的认识。

Dep.target和Watcher

我们接下来回答第二个问题,Dep.target是什么?Dep.target就是各种Watcher的实例,以下面代码举例说明:

<tempalte>
  <div>{{msg}}</div>
</template>

<script>
export default {
  data () {
    return {
      msg: 'Hello, Vue.js'
    }
  }
}
</script>

当组件初次渲染的时候,会获取msg的值,然后执行pushTarget(this),其中this代表当前Watcher实例,pushTarget()函数是定义在dep.js文件中的一个方法,与之对应的还有一个叫做popTarget方法,它们的代码如下:

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中,我们传递的target参数就是Watcher实例,然后在pushTarget执行的时候,它会动态设置Dep的静态属性Dep.target的值。在分析完pushTarget函数的代码后,我们就能明白为什么说Dep.target就是各种Watcher的实例了。

然后,我们会存在一个新的问题:Watcher类是如何定义的?它其实是定义在watcher.js文件中一个类,其关键代码如下:

let uid = 0

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)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    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()
  }
  get () {
    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)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  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)
      }
    }
  }
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

从依赖收集的角度去看Watcher类的时候,我们在其构造函数中需要关注以下四个属性:

this.deps = []             // 旧dep列表
this.newDeps = []          // 新dep列表
this.depIds = new Set()    // 旧dep id集合
this.newDepIds = new Set() // 新dep id集合

我们会在之后的addDepcleanupDeps环节详细介绍以上四个属性的作用,在这一小节,我们主要关注Watcher的构造函数以及get()方法的实现。

Watcher类的构造函数中,当实例化时,depsnewDeps数组以及depIdsnewDepIds集合分别被初始化为空数组以及空集合,在构造函数的最后,判断了如果不是computed watcher(注:只有computed watcherlazy属性才为true),则会马上调用this.get()函数进行求值。

接下来,我们来分析一下this.get()方法的实现,以及pushTargetpopTarget方法配合使用的场景介绍。

get () {
  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)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

我们可以看到,get()方法的代码不是很复杂,在方法的最前面首先调用pushTarget(this),通过pushTarget()方法首先把当前Watcher实例压栈到target栈数组中,然后把Dep.target设置为当前的Watcher实例。

Dep.target = null
const targetStack = []

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

然后调用this.getter进行求值,拿以下计算属性示例来说:

export default {
  data () {
    return {
      age: 23
    }
  },
  computed: {
    newAge () {
      return this.age + 1
    }
  }
}

value = this.getter.call(vm, vm)
// 相当于
value = newAge()

对于computed watcher而言,它的getter属性就是我们撰写的计算属性方法,调用this.getter的过程,就是执行我们撰写的计算属性方法进行求值的过程。

this.get()方法的最后,调用了popTarget(),它会把当前target栈数组的最后一个移除,然后把Dep.target设置为倒数第二个。

Dep.target = null
const targetStack = []

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

在分析了pushTargetpopTarget后,我们可能会有一个疑问,就是为什么会存在这样的压栈/出栈的操作,这样做的目的是什么?

这样做的目的是因为组件是可以嵌套的,使用栈数组进行压栈/出栈的操作是为了在组件渲染的过程中,保持正确的依赖,以下面代码为例:

// child component
export default {
  name: 'ChildComponent',
  template: '<div>{{childMsg}}</div>',
  data () {
    return {
      childMsg: 'child msg'
    }
  }
}

export default {
  name: 'ParentComponent',
  template: `<div>
    {{parentMsg}}
    <child-component />
  </div>`,
  components: {
    ChildComponent
  }
  data () {
    return {
      parentMsg: 'parent msg'
    }
  }
}

我们都知道,组件渲染的时候,当父组件中有子组件时,会先渲染子组件,子组件全部渲染完毕后,父组件才算渲染完毕,因此组件渲染钩子函数的执行顺序为:

parent beforeMount()
child beforeMount()
child mounted()
parent mounted()

根据以上渲染步骤,当parent beforeMount()开始执行时,会进行parent render watcher实例化,然后调用this.get(),此时的Dep.target依赖为parent render watchertarget栈数组为:

// 演示使用,实际为Watcher实例
const targetStack = ['parent render watcher']

child beforeMount开始执行的时候,会进行child render watcher实例化,然后调用this.get(),此时的Dep.target依赖为child render watchertarget栈数组为:

// 演示使用,实际为Watcher实例
const targetStack = ['parent render watcher', 'child render watcher']

child mounted()执行时,代表子组件的this.getter()调用完毕,进而会调用popTarget()进行出栈操作,此时的栈数组和Dep.target会发生变化:

// 演示使用,实际为Watcher实例
const targetStack = ['parent render watcher']
Dep.target = 'parent render watcher'

parent mounted()执行时,代表父组件的this.getter()调用完毕,进而会调用popTarget()进行出栈操作,此时的栈数组和Dep.target会发生变化:

// 演示使用,实际为Watcher实例
const targetStack = []
Dep.target = undefined

通过以上示例分析,我们就弄明白了为什么会有依赖压栈/出栈这样的步骤以及这样做的目的了。接下来,让我们来分析依赖收集的过程中,addDepcleanupDeps的逻辑。

addDep和cleanupDeps

addDep

在之前Dep类的depend()方法中,我们介绍过其代码实现,它会调用addDep(dep)

export default Dep {
  // 省略其它代码
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
}

根据前面的分析内容,我们知道Dep.target其实就是各种Watcher实例,因此Dep.target.addDep(this)相当于:

const watcher = new Watcher()
watcher.addDep(this)

接下来,让我们来看一下Watcher类中,addDep方法的实现逻辑:

export default Watcher {
  // 精简代码
  constructor () {
    this.deps = []              // 旧dep列表
    this.newDeps = []           // 新dep列表
    this.depIds = new Set()     // 旧dep id集合
    this.newDepIds = new Set()  // 新dep id集合
  }
  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)
      }
    }
  }
}

addDep方法的逻辑不是很复杂,首先判断了当前dep是否已经在新dep id集合中,不在则更新新dep id集合以及新dep数组,随后又判断了当前dep是否在旧dep id集合中,不在则调用dep.addSub(this)方法,把当前Watcher实例添加到dep实例的subs数组中。

生硬的分析源码不是很方便我们理解addDep的代码逻辑,我们以下面代码示例说明:

<template>
  <p>位置一:{{msg}}</p>
  <p>位置二:{{msg}}</p>
</template>
<script>
export default {
  name: 'App',
  data () {
    return {
      msg: 'msg'
    }
  }
}
</script>

过程分析:

  • 当组件初次渲染的时候,会实例化render watcher,此时的Dep.targetrender watcher
const updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
  • 第一次编译读取msg响应式变量时,触发getter进行dep.depend()依赖收集,然后调用addDep()方法,因为depsnewDepsdepIdsnewDepIds初始化为空数组或者空集合,所以此时的dep被添加到newDepIdsnewDeps中并且会执行dep.addSub(this),此时可以用下面代码表示:
// 实例化Dep
const dep = {
  id: 1,
  subs: []
}

// 添加到newDepIds,newDeps
this.newDepIds.push(1)
this.newDeps.push(dep)

// 调用addSub
dep.addSub(this)
console.log(dep) // { id: 1, subs: [new Watcher()] }
  • 当第二次编译读取msg响应式变量时,触发getter进行dep.depend依赖收集,因为depdefineReactive函数中的闭包变量,因此两次触发的getter是同一个dep实例。当调用addDep判断此时的newDepIds集合中dep.id1已经存在,因此直接跳过。

你可能会发现,在分析getter中代码的时候,我们故意忽略了下面这段代码:

if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}

你可能会有这样的疑问:这点代码是干什么的?有什么作用?那么现在,我们举例说明:

<template>
  <p>{{obj.msg}}</p>
</template>
<script>
export default {
  name: 'App',
  data () {
    return {
      obj: {
        msg: 'msg'
      }
    }
  }
}
</script>

过程分析:

  • 当第一次调用defineReactive时,此时defineReactive第一个参数objkey分别为:
obj = {
  obj: {
    msg: 'msg'
  }
}

key = 'obj'

defineReactive在最开始,实例化了一个闭包dep实例,我们假设实例化后的dep如下:

const dep = new Dep()
console.log(dep) // { id: 1, subs: [] }

当代码执行到observe(val)的时候,根据之前我们分析过observe代码的逻辑,因为参数obj[key]的值是一个普通对象,因此会执行new Observer()实例化,而在Observer构造函数中,有这样一段代码:

this.dep = new Dep()

它又实例化了一个dep并且把实例化后的dep赋值给this.dep,我们假设此时实例化后的dep如下所示:

const dep = new Dep()
console.log(dep) // { id: 2, subs: [] }

因为obj = { msg: 'msg' }是一个对象,因此执行this.walk()遍历obj对象的属性,然后再次调用defineReactive又实例化了一个闭包dep实例,我们假设实例后的dep如下所示:

const dep = new Dep()
console.log(dep) // { id: 3, subs: [] }

现在,我们已经有了三个dep实例了,其中两个是defineReactive函数中的闭包实例dep,另外一个是childOb(Observer实例)的属性dep

  • 在组件开始渲染的时候,根据响应式原理加上我们在template中读取了obj.msg变量,因此会先触发obj对象的getter,此时depid=1的那个闭包变量dep。此时的Dep.targetrender watcher,然后进行dep.depend()依赖收集,当走到addDep方法的时候,因为我们关注的四个属性全部为空数组或者空集合,因此会把此时的dep添加进去,此时的dep表示如下:
const dep = {
  id: 1,
  subs: [new Watcher()]
}
  • dep.depend()依赖收集完毕后,会判断childOb,因为childObObserver的实例,因此条件判断为真,调用childOb.dep.depend()。当执行到addDep()时,此时的depid=2的那个Observer实例属性dep,不在newDepIdsdepIds中,因此会把其添加进去,此时的dep表示如下:
const dep = {
  id: 2,
  subs: [new Watcher()]
}
  • 当响应式变量objgetter触发完毕后,会触发obj.msggetter,此时的depid=3的那个闭包变量dep。此时的Dep.target依然为render watcher,然后进行dep.depend()依赖收集,这个过程与objgetter进行依赖收集的过程基本是一样的,当addDep()方法执行后,此时的dep`表示如下:
const dep = {
  id: 3,
  subs: [new Watcher()]
}

唯一的区别时,此时的childObundefined,不会调用childOb.dep.depend()进行子属性的依赖收集。

在分析完以上代码后,我们很容易回答一下问题:
问:childOb.dep.depend()是干什么的?有什么作用?
答:childOb.dep.depend()这段代码是进行子属性的依赖收集,这样做的目的是为了当对象或者对象属性任意一个发生变化时,都可以通知其依赖进行相应的处理。

<template>
  <p>{{obj.msg}}</p>
  <button @click="change">修改属性</button>
  <button @click="add">添加属性</button>
</template>
<script>
import Vue from 'vue'
export default {
  name: 'App',
  data () {
    return {
      obj: {
        msg: 'msg'
      }
    }
  },
  methods: {
    change () {
      this.obj.msg = 'new msg'
    },
    add () {
      this.$set(this.obj, 'age', 23)
    }
  },
  watch: {
    obj: {
      handler () {
        console.log(this.obj)
      },
      deep: true
    }
  }
}
</script>

拿以上例子说明:

  • 当存在childOb.dep.depend()收集子属性依赖时,我们无论是修改msg的值还是添加age新属性,都会触发user watcher,也就是打印this.obj的值。
  • 当不存在childOb.dep.depend()收集子属性依赖时,我们修改msg的值,虽然会通知render watcher进行组件重新渲染,但不会通知user watcher打印this.obj的值。

cleanupDeps

在这一小节,我们的目标是弄清楚为什么要进行依赖清除以及如何进行依赖清除。

先来看Watcher类中对于cleanupDeps的实现:

export default Watcher {
  // 精简代码
  constructor () {
    this.deps = []              // 旧dep列表
    this.newDeps = []           // 新dep列表
    this.depIds = new Set()     // 旧dep id集合
    this.newDepIds = new Set()  // 新dep id集合
  }
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
}

我们还是举例说明,假如有如下组件:

<template>
  <p v-if="count < 1">{{msg}}</p>
  <p v-else>{{age}}</p>
  <button @click="change">Add</button>
</template>
<script>
import Vue from 'vue'
export default {
  name: 'App',
  data () {
    return {
      count: 0,
      msg: 'msg',
      age: 23
    }
  },
  methods: {
    change () {
      this.count++
    }
  }
}
</script>

过程分析:

  • 当组件初次渲染完毕后,render watcher实例的newDeps数组有两个dep实例,其中一个是在count响应式变量getter被触发时收集的,另外一个是在msg响应式变量getter被触发时收集的(age因为v-if/v-else指令的原因,在组件初次渲染的时候不会触发agegetter),我们使用如下代码进行表示:
this.deps = []
this.newDeps = [
  { id: 1, subs: [new Watcher()] },
  { id: 2, subs: [new Watcher()] }
]
  • 当我们点击按钮进行this.count++的时候,会触发组件重新更新,因为count < 1条件为假,因此在组件重新渲染的过程中,也会触发age响应式变量的getter进行依赖收集。当执行完addDep后,此时newDeps发生了变化:
this.deps = [
  { id: 1, subs: [new Watcher()] },
  { id: 2, subs: [new Watcher()] }
]
this.newDeps = [
  { id: 1, subs: [new Watcher()] },
  { id: 3, subs: [new Watcher()] }
]
this.depIds = new Set([1, 2])
this.newDepIds = new Set([1, 3])

在最后一次调用this.get()的时候,会调用this.cleanupDeps()方法,在这个方法中首先遍历旧依赖列表deps,如果发现其中某个dep不在新依赖id集合newDepIds中,则调用dep.removeSub(this)移除依赖。在组件渲染的过程中,this代表render watcher,调用这个方法后当我们再修改msg变量值的时候,就不会触发组件重新渲染了。在遍历完deps数组后,会把depsnewDepsdepIdsnewDepIds的值进行交换,然后清空newDepsnewDepIds

在分析完以上示例后,我们就能明白为什么要进行依赖清除了:避免无关的依赖进行组件的重复渲染

派发更新

在介绍完依赖收集后,我们紧接着来分析一下派发更新。在这一小节,我们的目标是弄清楚派发更新主要做什么事情以及派发更新的具体过程实现。

我们先来回答第一个问题:
问:派发更新主要做什么事情?
答:派发更新就是当响应式数据发生变动的时候,通知所有订阅了这个数据变化的Watcher(既Dep依赖)执行update。对于render watcher渲染Watcher而言,update就是触发组件重新进行渲染;对于computed watcher计算属性Watcher而言,update就是对计算属性重新求值;对于user watcher用户自定义Watcher而言,update就是调用用户提供的回调函数。

场景

大多数人分析派发更新的场景,只说明了Object.defineProperty()方法中setter被触发的时候会进行派发更新,其实一共有四处派发更新的地方,其它三处分别为:

  • Vue.js中七种数组变异方法被调用时,会进行派发更新。
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

methodsToPatch.forEach(function (method) {
  def(arrayMethods, method, function mutator (...args) {
    // 精简代码
    ob.dep.notify()
    return result
  })
})
  • Vue.set或者this.$set的时候,会进行派发更新。
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 精简代码
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
  • Vue.delete或者this.$delete的时候,会进行派发更新。
export function del (target: Array<any> | Object, key: any) {
  // 精简代码
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}

其中,以上三种派发更新与Object.defineProperty()方法中的setter被触发时的派发更新有一点不一样,setter中的派发更新,它的dep是一个在defineReactive方法中定义的闭包变量,意味着其只能服务于defineReactive方法。前者的dep是从this.__ob__对象中取的,this.__ob__属性是在Observer被实例化的时候被定义的,它指向Observer实例,我们在之前已经介绍过。这种独特的处理方式,方便了我们在以上三种场景下,能方便的读取到dep依赖,进而进行依赖的派发更新。

过程

在以上代码中,我们以及了解到了dep.notify()被调用的各种时机,在这一个小节中我们需要来看一下派发更新的过程。

dep.notify()被调用时,它会执行notify()方法中的代码,我们来看一下Dep类中关于这个方法的实现:

notify () {
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

可以发现,notify主要做的就是遍历subs数组,然后调用update方法。下一步,我们来看一下Watcher类中关于update方法的代码实现:

import { queueWatcher } from './scheduler'
update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

update方法被执行的时候,首先判断了this.lazythis.sync两个属性,其中this.lazycomputed watcher计算属性的标志,因为计算属性会延后进行求值,因此这里只是把this.dirty赋值为truethis.sync不属于派发更新这一章节的重点,因此不做过多的介绍。

我们来重点分析queueWatcher,它是撰写在observer/scheduler.js文件中的一个方法:

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

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      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)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

我们可以在以上代码最顶部发现定义了几个变量,其中有几个比较重要的变量,它们的作用如下:

  • queue:各种Watcher执行队列,无论是render watcheruser watcher还是computed watcher,只要不是重复的Watcher,最终都会被推入到queue队列数组中。
  • has:用来防止重复添加Watcher的标志对象:
// 表示id为1,2的Watcher实例已经被添加到了queue
// 后续遇到同样的Watcher实例,不会重复添加到队列中
const has = {
  1: true,
  2: true
}
  • index:当前遍历的Watcher实例索引,它就是flushSchedulerQueue方法中使用for循环遍历queue队列数组的index

介绍完以上几个重要变量后,我们来分析一下queueWatcher的过程:

  • 代码首先通过获取当前Watcher的自增id,判断在标志对象has中是否已经存在,如果不存在,则对这个id进行标记,赋值为true
  • 随后判断是否为flushing状态,如果不是,则代表我们可以正常的把当前Watcher推入到queue队列数组中。
  • 接着判断了是否为waiting状态,如果不是,则代表可以执行queue队列数组,然后设置waitingtrue,最后调用nextTick(flushSchedulerQueue)nextTick方法是Vue.js自己封装的一个处理异步逻辑的工具函数,我们现在只要知道:nextTick中的函数参数,会在下一个tick执行。

接着,我们来看flushSchedulerQueue函数是如何实现的:

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    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
      }
    }
  }

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

我们粗略观看flushSchedulerQueue函数代码,可以发现它主要做几件事情:还原flushing状态排序queue队列遍历queue还原状态触发组件钩子函数。我们按照这几个步骤,分别说明:

  • 还原flushing状态:在flushSchedulerQueue首先对flushing进行了还原,这样做的目的是为了不影响在执行queue队列的时候,有Watcher推入到queue队列中。
  • 排序queue队列:使用数组的sort方法,把queue队列中的Watcher按照自增id的值从小到大进行了排序,这样做是为了保证以下三种场景:
  1. 我们都知道,组件的更新是从父组件开始,然后到子组件。在组件渲染的时候,会从父组件开始渲染,这时候会创建父组件的render watcher,假设此时的parent render watcher自增id1,接着渲染子组件,实例化子组件的render watcher,假设此时的child render watcher自增id2。进行queue.sort()排序后,id值小的排序到数组前面,这样在queue进行遍历的时候,就能保证首先处理parent render watcher,然后再处理child render watcher
  2. 因为用户自定义Watcher可以在组件渲染之前创建,因此对于用户自定义Watcher而言,需要优先于render watcher执行。
<template>
  <p>{{msg}}</p>
  <button @click="change">Add</button>
</template>
<script>
export default {
  data () {
    return {
      count: 0,
      msg: 'msg',
      age: 23
    }
  },
  created () {
    this.$watch('msg', () => {
      // 先执行回调函数,再组件渲染
      console.log(this.msg)
    })
  },
  methods: {
    change () {
      this.msg = Math.random()
    }
  }
}
</script>
  1. 如果一个子组件在父组件执行queueWatcher的过程中被销毁了,那么子组件所有的Watcher执行都应该跳过。
  • 遍历queue:在使用for循环遍历的时候,我们需要注意遍历条件,它先对queue的长度进行了求值,然后再判断循环条件,这样做是因为在遍历queue数组的过程中,queue数组中的元素有可能会发生变动。在遍历的过程中,首先会释放当前Watcherhas标志对象中的状态,然后调用watcher.run()方法。run是定义在Watcher类中的一个方法:
export default class Watcher {
  // 精简代码
  run () {
    if (this.active) {
      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)
        }
      }
    }
  }
}

run方法的代码不是很复杂,就是对不同的Watcher进行不同的处理,如果是render watcher,它在执行this.get()的过程中会执行this.getterthis.getter对应以下方法:

updateComponent = () => {
  // 组件渲染方法
  vm._update(vm._render(), hydrating)
}

如果是user watcher,其this.user值为true,会调用this.cb.call(),此时的this.cb就是用户写的user callback

export default {
  data () {
    return {
      msg: 'msg'
    }
  },
  created () {
    // user callback
    // this.cb = userCallback
    const userCallback = () => {
      console.log(this.msg)
    }
    this.$watch(this.msg, userCallback)
  }
}

如果是computed watcher,其this.user值为false,会调用this.cb.call(),此时的this.cb就是我们提供的计算属性方法:

export default {
  data () {
    return {
      msg: 'msg'
    }
  },
  computed: {
    // this.cb = newMsg () {}
    newMsg () {
      return this.msg + '!!!'
    }
  }
}
  • 还原状态:调用resetSchedulerState函数的目的是,当queue队列都执行完毕时,把所有相关状态还原为初始状态,这其中包括queuehasindex等:
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}
  • 触发组件钩子函数:调用callActivatedHookscallUpdatedHooks分别是为了触发组件activatedupdated钩子函数,其中activated是与keep-alive相关的钩子函数。

死循环

在使用Vue.js进行开发的时候,有时候我们会不小心写出死循环的代码,例如:

<template>
  <p>{{msg}}</p>
  <button @click="change">Add</button>
</template>
<script>
export default {
  data () {
    return {
      msg: 'msg'
    }
  },
  methods: {
    change () {
      this.msg = Math.random()
    }
  },
  watch: {
    msg () {
      this.msg = Math.random()
    }
  }
}
</script>

当我们点击按钮调用change方法修改this.msg的值的时候,因为我们使用watch监听了msg的值更新,所以会执行watch监听函数,但是在watch监听函数中我们又修改了this.msg的值,这样会导致一直调用我们写的监听函数,存在一个死循环。在Vue.js中,为了避免死循环导致浏览器崩溃,它做了特殊处理。

queueWatcher的时候,我们并没有分析以下else这段代码:

export const MAX_UPDATE_COUNT = 100
let circular: { [key: number]: number } = {}

if (!flushing) {
  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)
}

我们以上面的例子来分析以下这段代码:

  • 当我们点击按钮修改this.msg的值的时候,触发msgsetter,然后进行dep.notify派发更新,接着调用queueWatcher,此时msg存在两个Dep依赖,一个是render watcher,另外一个是user watcher,因此this.subs是一个长度为2Watcher数组。当初次queueWatcher的时候,flushing状态为false,因为user watcherrender watcher先创建,因此这个时候user watcher会先推入到queue队列,接着是render watcher:
// 展示使用,实际为Watcher实例
const queue = ['user watcher', 'render watcher']
  • 接着会执行watch监听函数,再次执行queueWatcher的时候,此时的flushingfalse,走else分支逻辑,while循环的作用主要是为了查找应该在queue数组什么位置插入新的watcher,例如:
const queue = [
  { id: 1, type: 'user watcher' },
  { id: 2, type: 'render watcher' },
]
// 当执行watch监听函数的时候,此时的watcher应该插入到数组第二项
const queue = [
  { id: 1, type: 'user watcher' },
  { id: 1, type: 'user watcher' },
  { id: 2, type: 'render watcher' },
]

因为我们撰写的特殊例子,queue数组会不断的推入user watcher,当queue中的数量超过限制的时候,Vue.js提前终止这种行为(某些Watcher被遍历超过100次时),Vue.js使用circular标记对象来进行计数,它标记了每一个Watcher被遍历的次数,例如:

// id为1的Watcher被遍历了101次
// id为2的Watcher被遍历了1次
const circular = {
  1: 101,
  2: 1
}

circular计数更新和终止的代码在flushSchedulerQueue函数中:

for (index = 0; index < queue.length; index++) {
  watcher = queue[index]
  if (watcher.before) {
    watcher.before()
  }
  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
    }
  }
}

因此对于以上例子,Vue.js会在控制台输出这样一个错误信息:

// You may have an infinite update loop in watcher with expression "msg"

整体流程图

在分析完以上派发更新的过程后,我们可以得到如下流程图。

nextTick实现原理

在使用Vue.js开发的时候,如果我们要根据数据状态操作正确的DOM,那么我们一定和nextTick()方法打过交道,它是Vue.js中一个比较核心的一个方法,在这一章节中我们来介绍Vue.jsnextTick是如何实现的。

异步知识

由于nextTick涉及到许多与异步相关联的知识,因此为了降低学习难度,我们先来介绍这些异步知识。

Event Loop

我们都知道JavaScript是单线程的,它是基于Event Loop事件循环来执行的,Event Loop在执行的时候遵循一定的规则:所有同步任务都在主线程中执行,形成一个执行栈,所有异步任务,都会被暂时放入一个任务队列中,当所有同步任务执行完毕时,会读取这个任务队列让其进入执行栈,开始执行。以上介绍属于一次执行机制,主线程不断重复这个过程就形成了Event Loop事件循环。

以上是对Event Loop的大体介绍,但在Event Loop执行的时候,还有一些细节需要我们去掌握。

我们在派发更新章节提到过tick,那么什么是ticktick就是主线程的一次执行过程。所有异步任务都是通过任务队列来调度的,任务队列中存放的是一个个任务(task),而这一个个task按照规范,又分为macro task宏任务和micro task微任务。macro taskmicro task在执行的时候存在一个微妙的关系:每个macro task执行结束后,会清空所有的micro task

在浏览器环境下,macro taskmicro task对应如下:

  • macro task宏任务:MessageChannelpostMessagesetImmediatesetTimeout
  • micro task微任务:Promise.thenMutationObsever

MutationObserver

MDN文档中,我们可以看到MutationObserver的详细用法,它不是很复杂,它的作用是:创建并返回一个新的 MutationObserver实例,它会在指定的DOM发生变化时被调用。

我们按照文档介绍,来撰写一个例子:

const callback = () => {
  console.log('text node data change')
}
const observer = new MutationObserver(callback)
let count = 1
const textNode = document.createTextNode(count)
observer.observe(textNode, {
  characterData: true
})

function func () {
  count++
  textNode.data = count
}
func() // text node data change

代码分析:

  • 首先定义了callback回调函数和MutationObserver的实例对象,其中构造函数传递的参数是我们的callback
  • 然后创建一个文本节点并传入文本节点的初始文本,接着调用MutationObserver实例的observe方法,传入我们创建的文本节点和一个config观察配置对象,其中characterData:true的意思是:我们要观察textNode节点的文本变动。config还有其他选项属性,你可以在MDN文档中查看到。
  • 接着,我们定义一个func函数,这个函数主要做的事情就是修改textNode文本节点中的文本内容,当文本内容变动后,callback会自动被调用,因此输出text node data change

在了解了MutationObserver的用法后,我们来看一下nextTick方法中,是如何使用MutationObserver的:

import { isIE, isNative } from './env'

// 省略代码
else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
}

我们可以看到,nextTick中首先判断了非IE浏览器并且MutationObserver可用且为原生MutationObserver时才会使用MutationObserver。对于判断非IE浏览器的情况,你可以看Vue.jsissue#6466来查看原因。

setImmediate和setTimeout

setTimeout对于大部分人来说是非常常见的一个定时器方法,因此我们不做过多的介绍。

nextTick方法实现中,它使用到了setImmediate,我们在Can I Use网站上可以发现,这个API方法只存在于高版本IE浏览器和低版本Edge浏览器中,其它浏览器不支持。

那么为什么会使用这个方法呢,这是因为我们之前提到的一个issueMutationObserverIE浏览器中并不可靠,因此在IE浏览器下降级到使用setImmediate,我们可以把setImmediate看做和setTimeout一样。

setImmediate(() => {
  console.log('setImmediate')
}, 0)
// 约等于
setTimeout(() => {
  console.log('setTimeout')
}, 0)

nextTick实现

介绍完nextTick与异步相关的知识后,我们来的分析一下nextTick方法的实现,首先要说的是:异步降级

let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
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
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

我们在前面介绍过Event Loop事件循环,由于macro taskmicro task特殊的执行机制,我们首先判断当前浏览器是否支持Promise,如果不支持,则降级到判断是否支持MutationObserver,如果还不支持,则继续降级到判断是否支持setImmediate,最后降级使用setTimeout

在介绍完异步降级之后,我们来看nextTick的实现代码:

const callbacks = []
let pending = false
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) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick真正的代码并不复杂,首先会把传入的cb收集起来,然后判断pendingfalse的时候开始执行timerFunc方法,其中timeFunc在异步降级的过程中被定义的。nextTick方法在最后还进行了判断,如果没有传入cb并且支持Promise的话,它会返回一个promise,因此我们在使用nextTick的时候可以有两种使用方式:

const callback = () => {
  console.log('nextTick callback')
}
// 方式一
this.$nextTick(callback)

// 方式二
this.$nextTick().then(() => {
  callback()
})

在最后,我们来看一个在之前没有提到的flushCallbacks方法实现:

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

flushCallbacks方法的主要作用就是:将pending状态还原为false并且遍历callbacks数组中的方法并执行。

变化侦测注意事项

虽然Object.defineProperty()方法很好用,但也会存在一些例外情况,这些例外情况的变动不能触发setter。这种情况,我们分为对象和数组两类来分析。

对象

假设我们有如下例子:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      }
    }
  },
  created () {
    // 1.新增属性b,属性b不是响应式的,不会触发obj的setter
    this.obj.b = 'b'
    // 2.delete删除已有属性,无法触发obj的setter
    delete this.obj.a
  }
}

从以上例子我们可以看到:

  • 当为一个响应式对象新增一个属性的时候,新增的属性不是响应式的,后续对于这个新增属性的任何修改,都无法触发其setter。为了解决这种问题,Vue.js提供了一个全局的Vue.set()方法和实例vm.$set()方法,它们实际上都是同一个set方法,我们会在后续的章节中介绍与响应式相关的全局API的实现。
  • 当一个响应式对象删除一个已有属性的时候,不会触发setter。为了解决这个问题,Vue.js提供了一个全局的vue.delete()方法和实例vm.$delete()方法,它们实际上都是同一个del方法,我们会在后续的章节中介绍与响应式相关的全局API的实现。

数组

假设我们有如下例子:

export default {
  data () {
    return {
      arr: [1, 2, 3]
    }
  },
  created () {
    // 1.通过索引进行修改,无法捕获到数组的变动。
    this.arr[0] = 11
    // 2.通过修改数组长度,无法捕获到数组的变动。
    this.arr.length = 0
  }
}

从以上例子我们可以看到:

  • 通过索引直接修改数组,无法捕捉到数组的变动。
  • 通过修改数组长度,无法捕获到数组的变动。

对于第一种情况,我们可以使用前面提到过的Vue.set或者vm.$set来解决,对于第二种方法,我们可以使用数组的splice()方法解决。

在最新版Vue3.0中,使用到了Proxy来代替Object.defineProperty()实现响应式,使用Proxy后以上问题全部可以解决,然而Proxy属于ES6的内容,因此对于浏览器兼容性方面有一定的要求。

变化侦测API实现

在上一节中,我们分析了变化侦测一些问题,在这一节中我们来分析一下为了解决这些问题,Vue.js是如何实现相关API的。

Vue.set实现

Vue.setvm.$set引用的是用一个set方法,其中set方法被定义在observer/index.js文件中:

export function set (target: Array<any> | Object, key: any, val: any): any {
  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)) {
    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
}

在代码分析之前,我们来回顾一下Vue.set或者vm.$set的用法:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      },
      arr: []
    }
  },
  created () {
    // 添加对象新属性
    this.$set(this.obj, 'b', 'b')
    console.log(this.obj.b) // b

    // 往数组中添加新元素
    this.$set(this.arr, 0, 'AAA')
    console.log(this.arr[0]) // AAA

    // 通过索引修改数组元素
    this.$set(this.arr, 0, 'BBB')
    console.log(this.arr[0]) // BBB
  }
}

代码分析:

  • set方法首先对传入的target参数进行了校验,其中isUndef判断是否为undefinedisPrimitive判断是否为JavaScript原始值,如果满足其中一个条件则在开发环境下提示错误信息。
export default {
  created () {
    // 提示错误
    this.$set(undefined, 'a', 'a')
    this.$set(1, 'a', 'a')
    this.$set('1', 'a', 'a')
    this.$set(true, 'a', 'a')
  }
}
  • 随后通过Array.isArray()方法判断了target是否为数组,如果是再通过isValidArrayIndex判断是否为合法的数组索引,如果都满足则会使用变异splice方法往数组中指定位置设置值。其中,还重新设置了数组的length属性,这样做是因为我们传入的索引可能比现有数组的length还要大。

  • 接着判断是否为对象,并且当前key是否已经在这个对象上,如果已经存在,则我们只需要进行重新复制即可。

  • 最后,通过defineReactive方法在响应式对象上面新增一个属性,defineReactive方法已经在之前介绍过,这里不再累述。在defineReactive执行完毕后,马上进行派发更新,通知响应式数据的依赖立即更新,可以说以下两段代码是set方法核心中的核心:

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

Vue.delete实现

解决完新增属性的问题后,我们来解决以下删除属性的情况,Vue.deletevm.$delete使用的是同一个delete方法,它被定义在observer/index.js文件中:

export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}

在代码分析之前,我们来回顾以下Vue.delete或者vm.$delete的用法:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      },
      arr: [1, 2, 3]
    }
  },
  created () {
    // 删除对象属性
    this.$delete(this.obj, 'a')
    console.log(this.obj.a) // undefined
    // 删除数组元素
    this.$delete(this.arr, 1)
    console.log(this.arr)   // [1, 3]
  }
}

代码分析:

  • 首先判断了待删除的target不能为undefined或者原始值,如果是则在开发环境下提示错误。
export default {
  created () {
    // 提示错误
    this.$delete(undefined, 'a')
    this.$delete(1, 'a')
    this.$delete('1', 'a')
    this.$delete(true, 'a')
  }
}
  • 随后通过Array.isArray()方法判断了target是否为数组,如果是再通过isValidArrayIndex判断是否为合法的数组索引,如果都满足则会使用变异splice方法删除指定位置的元素。
  • 接着判断当前要删除的属性是否在target对象中,如果不在则直接返回,什么都不做。
  • 最后,通过delete操作符删除对象上的属性,然后ob.dep.notify()进行派发更新,通知响应式对象上的依赖进行更新。

Vue.observable实现

Vue.observable是在Vue2.6+版本才会有的一个全局方法,它的作用是让一个对象变成响应式:

const obj = {
  a: 1,
  b: 2
}
const observeObj = Vue.observable(obj)
console.log(observeObj.a) // 触发getter

observeObj.b = 22 // 触发setter

这个全局方法是在initGlobalAPI的过程中被定义的,initGlobalAPI我们在之前已经介绍过,这里不在累述:

export default function initGlobalAPI (Vue) {
  // ...
  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
  // ...
}

我们可以看到observable的实现很简单,在方法内部仅仅只是调用了observe方法,然后返回这个obj。关于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
}

上一篇:Vue2.0源码分析:响应式原理(上)

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。