Vue(2.6.11)源碼閱讀——響應(yīng)式系統(tǒng)

Vue的響應(yīng)式系統(tǒng)將按照如下幾個部分進(jìn)行講解:
1. 響應(yīng)式對象
2. 依賴收集
3. 派發(fā)更新
4. nextTick
5. 檢測變化的注意事項
6. 計算屬性VS偵聽屬性
7. 組件更新
8. Props
9. 原理圖

[響應(yīng)式對象]

對于響應(yīng)式對象,拿props的初始作為一個例子,其他屬性都可以觸類旁通。
vue的初始化有這么一條路:

initMixin => instate => initProps

來看initProps:

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

通過defineReactive(props, key, value)將props將props變成一個響應(yīng)式對象,通過proxy(vm, _props, key)使得下面這條訪問鏈路成立:

vm._props.xxx => vm.xxx =>$options.prop

來看下proxy的實現(xiàn)

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

就是通過sharedPropertyDefinition橋接了下,我們平常在組件使用的data也一樣。這也就我們在data中對應(yīng)的數(shù)據(jù)可以通過this.xxxx訪問的原因。

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? 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)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, 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)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

來看下這個observe(src\core\observer\index.js)

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

這段邏輯就是說如果之前添加過observe,那么就用之前的,否則就new一個Observer。ps:大家在看響應(yīng)式代碼之前建議先去了解下屬性描述符的一系列概念。
來看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])
    }
  }
}

其中dep有關(guān)依賴收集,我們等會再講,def(value, 'ob', this),就是將這個Observer添加到被觀察對象的"ob"上,作用我們之后再說。

如果被觀察的對象是數(shù)組的話,調(diào)用protoAugment 或copyAugment ,這兩個函數(shù)主要是重寫了數(shù)組的一些方法,最后調(diào)用observeArray。如果不是數(shù)組的話則調(diào)用walk方法。
來看walk方法:

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

其實就是調(diào)用了defineReactive方法(敲黑板了?。。。。?/p>


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

這玩意賊有用,vue-router的響應(yīng)式也是靠他實現(xiàn)的。
首先它先獲取了目標(biāo)對象屬性的屬性描述符。如果不可配置,直接return。
然后他通過Object.defineProperty,定義了該屬性的get和set,get我們后面會講,是用來做依賴收集的,set是用來做派發(fā)更新的。
先不管具體的依賴收集和派發(fā)更新。
整個創(chuàng)建一個響應(yīng)式對象,是這么一個流程

observe(xxx) => xxx是數(shù)組 ?遞歸observe :walk(調(diào)用defineReactive), observe整個動作是vue核心庫需要的,最后變成響應(yīng)式對象是靠defineReactive實現(xiàn)的。

[依賴收集]

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

這段代碼中其實真正重要的就是兩部分:

const dep = new Dep()
childOb.dep.depend()

Dep 是整個 getter 依賴收集的核心,它的定義在 src/core/observer/dep.js 中:

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

  constructor () {
    this.id = uid++
    this.subs = []
  }

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

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    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()
    }
  }
}

可以看到這個類實現(xiàn)了1個構(gòu)造器和4個方法:

  1. constructor
  2. addSub
  3. removeSub
  4. depend
  5. notify

Dep 實際上就是對 Watcher 的一種管理,Dep 脫離 Watcher 單獨存在是沒有意義的,為了完整地講清楚依賴收集過程,我們有必要看一下 Watcher 的一些相關(guān)實現(xiàn),它的定義在 src/core/observer/watcher.js 中:

/* @flow */

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError,
  noop
} from '../util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '../util/index'

let uid = 0

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
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()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  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
  }

  /**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  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
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  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)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

可以看到這個類實現(xiàn)了1個構(gòu)造器和8個方法:

  1. constructor
  2. get
  3. addDep
  4. cleanupDeps
  5. update
  6. run
  7. evaluate
  8. depend
  9. teardown

了解這些之后我們看下這個依賴收集的過程。
之前我們講過,在調(diào)用$mount方法的時候其實是調(diào)用了mountComponent方法(src\platforms\web\runtime\index.js)
而在mountComponent這個方法的定義的時候有這么一段邏輯:

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

我們來看下new watch干了啥:

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

首先實例中的_watchers收集了這個新的watch

 vm._watchers.push(this)

拋去一些我們暫時不關(guān)心的邏輯,看下最后訪問了get方法

    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
  }

第一步就是干了這么一件事:

  pushTarget(this)

看下pushTarget的定義(src\core\observer\dep.js):

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

也就是Dep.target會變成這個新的watch,同時把這個watch壓入targetStack中(其實后面會講到是為了恢復(fù)用)。
回到之前,我們講的defineReactive:

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      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()
    }
  })

當(dāng)我們開始訪問一個屬性的時候,那么依賴收集的工作就開始了:

首先調(diào)用

dep.depend()

進(jìn)而調(diào)用了addDep

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

然后調(diào)用addSub

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

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

上面的流程也就是下面這樣是等價的:

dep.subs.push(Dep.target)

而這個Dep.target在new Watcher的時候已經(jīng)被這個new Watcher賦值了。
也就是說我這個watcher就是持有這個dep的數(shù)據(jù)的訂閱者了,這個目的是為后續(xù)數(shù)據(jù)變化時候能通知到哪些訂閱者做準(zhǔn)備。

讓我們再關(guān)注一些細(xì)節(jié),我們先回到Wathcer的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
  }

首先是做了

 traverse(value)
``
這個等會會細(xì)講。
然后是
```js
popTarget()

看其定義:

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

實際上就是把 Dep.target 恢復(fù)成上一個狀態(tài),因為當(dāng)前 vm 的數(shù)據(jù)依賴收集已經(jīng)完成,那么對應(yīng)的渲染Dep.target 也需要改變。最后執(zhí)行:

this.cleanupDeps()
  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
  }

可以看到這個函數(shù)就是將新的依賴ID組和新的依賴組賦值到實際用的依賴ID組合依賴組里,然后清空了新的依賴ID組合依賴組。
考慮到一種場景,我們的模板會根據(jù) v-if 去渲染不同子模板 a 和 b,當(dāng)我們滿足某種條件的時候渲染 a 的時候,會訪問到 a 中的數(shù)據(jù),這時候我們對 a 使用的數(shù)據(jù)添加了 getter,做了依賴收集,那么當(dāng)我們?nèi)バ薷?a 的數(shù)據(jù)的時候,理應(yīng)通知到這些訂閱者。那么如果我們一旦改變了條件渲染了 b 模板,又會對 b 使用的數(shù)據(jù)添加了 getter,如果我們沒有依賴移除的過程,那么這時候我去修改 a 模板的數(shù)據(jù),會通知 a 數(shù)據(jù)的訂閱的回調(diào),這顯然是有浪費(fèi)的。

因此 Vue 設(shè)計了在每次添加完新的訂閱,會移除掉舊的訂閱,這樣就保證了在我們剛才的場景中,如果渲染 b 模板的時候去修改 a 模板的數(shù)據(jù),a 數(shù)據(jù)訂閱回調(diào)已經(jīng)被移除了,所以不會有任何浪費(fèi)。

[派發(fā)更新]

之前我們說派發(fā)個更新是在set里面進(jìn)行的:

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

可以看到有這么幾步:

  1. 判斷新值和舊值如果相等或者同為NaN,則直接返回。
  2. 如果有g(shù)etter沒有setter則直接返回
  3. 有setter調(diào)用setter,否則直接賦值。
  4. 將新值變?yōu)轫憫?yīng)式對象。
  5. 派發(fā)更新。
    這里需要注意的是,這里對新值和舊值是否相等是通過“===”判斷的,因此如果值是引用類型是判斷不出來的,所以通過set方法最后還是會調(diào)用defineReactive的,所以只適用值是普通值的情況。
    重點來看下用于派發(fā)更新的方法:
dep.notify()
  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()
    }
  }

可以看到這里是遍歷所有訂閱的watcher,然后執(zhí)行watcher的update方法:

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
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)
    }
  }
}

這里的sync等會會講,先看queueWatcher,這里has[id]保證每個watcher的回調(diào)只觸發(fā)一次,else等會再講,看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')
  }
}

隊列排序
queue.sort((a, b) => a.id - b.id) 對隊列做了從小到大的排序,這么做主要有以下要確保以下幾點:

1.組件的更新由父到子;因為父組件的創(chuàng)建過程是先于子的,所以 watcher 的創(chuàng)建也是先父后子,執(zhí)行順序也應(yīng)該保持先父后子。

2.用戶的自定義 watcher 要優(yōu)先于渲染 watcher 執(zhí)行;因為用戶自定義 watcher 是在渲染 watcher 之前創(chuàng)建的。

3.如果一個組件在父組件的 watcher 執(zhí)行期間被銷毀,那么它對應(yīng)的 watcher 執(zhí)行都可以被跳過,所以父組件的 watcher 應(yīng)該先執(zhí)行。

隊列遍歷
在對 queue 排序后,接著就是要對它做遍歷,拿到對應(yīng)的 watcher,執(zhí)行 watcher.run()。這里需要注意一個細(xì)節(jié),在遍歷的時候每次都會對 queue.length 求值,因為在 watcher.run() 的時候,很可能用戶會再次添加新的 watcher,這樣會再次執(zhí)行到 queueWatcher,如下:

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

這個時候flushing為true走到else分支里,這里其實就是將id按照從小到大插入queue中。run完,接著進(jìn)行了resetSchedulerState操作:

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

其實就是清空了 has,queue,然后將waiting,flushing重置為fasle.我們再來看看run:

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

先通過 this.get() 得到它當(dāng)前的值,然后做判斷,如果滿足新舊值不等、新值是對象類型、deep 模式任何一個條件,則執(zhí)行 watcher 的回調(diào),注意回調(diào)函數(shù)執(zhí)行的時候會把第一個和第二個參數(shù)傳入新值 value 和舊值 oldValue,這就是當(dāng)我們添加自定義 watcher 的時候能在回調(diào)函數(shù)的參數(shù)中拿到新舊值的原因。
梳理下流程:

值發(fā)生改變 => dep.notify()=> 遍歷觸發(fā)訂閱的wathcer的update=> 根據(jù)是否同步等條件最后觸發(fā)watcher.run方法(渲染函數(shù)觸發(fā)get執(zhí)行更新DOM)=> 觸發(fā)watcher的回調(diào)(用戶可以拿到新值和舊值)
那么對于渲染 watcher 而言(new的時候第5參數(shù)傳不傳true),它在執(zhí)行 this.get() 方法求值的時候,會執(zhí)行 getter 方法:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

接著講nextTick:

[nextTick]

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

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

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
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)
  }
}

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

可以看到這里用了依次降級(Promise.then => MutationObserver => setImmediate => setTimeout)的方式創(chuàng)建了異步函數(shù)timerFunc ,
然后異步函數(shù)里循環(huán)執(zhí)行之前添加過的函數(shù)。如果不傳回調(diào)函數(shù),那返回的就是一個promise,可以直接用then方法。這里callback用數(shù)組的原因是在一個循環(huán)中可以執(zhí)行多個傳入的函數(shù),不會開啟多個異步任務(wù),而把這些異步任務(wù)都壓成一個同步任務(wù),在下一個 tick 執(zhí)行完畢。
舉個例子:

nextTick(f1)
nextTick(f1)
nextTick(f1)

這個時候callbacks中已經(jīng)有f1,f2,f3了...然后一次性依次執(zhí)行。

[檢測變化的注意事項]

平常我們有三種情況不能被vue的響應(yīng)式系統(tǒng)直接監(jiān)聽到:

  1. 通過a.b = 1給對象新添加屬性。
  2. 通過arr[0] = 1直接給原數(shù)組的元素賦值。
  3. 通過vm.items.length = newLength修改數(shù)組長度。

1的原因是因為data在初始化的時候已經(jīng)完成了數(shù)據(jù)的響應(yīng)式化,所以新添加的屬性不是響應(yīng)式的。
2和3的原因是因為vue的響應(yīng)式只對數(shù)組本身做了響應(yīng)式監(jiān)聽,而其元素并沒有,length其實也算其一個屬性。

對于1和2,vue提供了set方法:

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
}
  • 對于target是一個數(shù)組,并且傳入的索引是有效的,那么直接通過splice返回(這里的splice被重寫了,等會講)
  • 對于本來就在對象中的屬性,就不需要處理了,因為已經(jīng)是響應(yīng)式對象了
  • 對于 Vue的實例和根data直接return
  • 如果target不存在ob,說明這個對象不是一個響應(yīng)式對象,就不添加了了。
  • 否則通過defineReactive轉(zhuǎn)為響應(yīng)式對象并通過ob.dep.notify()派發(fā)更新。

再來看看被重寫的數(shù)組方法:

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

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

可以看到對于push,unshift,splice3種方法,最后都通過了observeArray將傳進(jìn)去的所有參數(shù)對象變成了響應(yīng)式的并派發(fā)了更新。PS:Vue.delete用于數(shù)組也是基于splice的,而對象無需操作,最后也是通過ob.dep.notify()派發(fā)更新。

計算屬性VS偵聽屬性

計算屬性
先看他初始化的地方(src\core\instance\state.js):

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // $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]
    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') {
      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)
      }
    }
  }
}

先創(chuàng)建了一個空對象watchers,然后把我們傳進(jìn)去的computed的key作為它的key,賦值一個new Watcher,這就是計算watcher,注意我們傳了一個computedWatcherOptions進(jìn)去,就是一個{lazy: true}.再回頭看我們的watcher類的構(gòu)造器:

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

可以看到最后如果我們的lazy是true就是一個計算watcher的話,那么是先不調(diào)用watcher的。
接著上面的initComputed看,接下來是執(zhí)行了defineComputed(vm, key, userDef):


export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    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)
}

可以看到如果需要緩存,那么返回一個函數(shù),否則直接調(diào)用我們傳進(jìn)去的函數(shù)(就是computed的那些函數(shù)),最后通過Object.defineProperty將我們computed的同名key添加到是vue的實例上,所以我們可以通過this.xxxx直接訪問??聪耤reateComputedGetter:

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

可以看到由于首次傳的是lazy為true,這個dirty就是lazy,所以會先計算下watcher的值。然后開始收集依賴。接著調(diào)用了depend:

  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

即Dep.target將計算屬性的依賴變成了自己的依賴。
這里有個疑問,為什么每次獲取計算屬性的值時都要進(jìn)行依賴收集呢,而不是僅進(jìn)行一次性的依賴收集?原因是,計算屬性的依賴項可能會改變,這次有x個依賴項,下次可能有y個依賴項。比如三元表達(dá)式

那么每當(dāng)依賴發(fā)生變化就會產(chǎn)生這么一條路徑:
**dep.notify() => watcher.update(這個時候只是將lazy置位true) **
然后再次訪問計算屬性的值是會觸發(fā)computedGetter去重新計算wathcer的值。
這個時候如果有渲染watcher訂閱了這個計算watcher,那么會對比前后值是否一樣,不一樣才去重新渲染。
再來看偵聽屬性watch:

function initWatch (vm: Component, watch: Object) {
  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)
    }
  }
}
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)
}
  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爾。

整理下所有的watcher;

  1. deep watcher(遞歸添加watcher)
  2. user watcher(用戶的watcher)
  3. computed watcher
  4. sync watcher(可以在user watcher里配置)
  5. renderwatcher(渲染 watcher,一般來說處的位置靠上)

組件更新

之前我們降到如果依賴發(fā)生變化,會觸發(fā)組件更新,那么組件最后是怎么更新的,我們這邊來了解下:

依賴發(fā)生變化 => watcher.getter => vm._update => vm.patch

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    //   console.log(123)
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)//找了oldELm的父node(實際上的DOM元素)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

由于這次存在oldVnode,走else分支:

先通過sameVnode判斷是不是同一個vnode:

function sameVnode (a, b) {//判定是否是同一個vnode
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

就是按照這么一段邏輯判斷:
key => tag => isComment => data => sameInputType
異步組件則通過asyncFactory(常用的就是() => import(xxxx))判斷.
如果新舊節(jié)點不同,那么主要份3步進(jìn)行:

  1. 創(chuàng)建新節(jié)點
  2. 更新父的占位節(jié)點
  3. 刪除舊節(jié)點

創(chuàng)建新節(jié)點

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)//找了oldELm的父node(實際上的DOM元素)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

更新父的占位節(jié)點

        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

刪除舊節(jié)點

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }

刪除節(jié)點邏輯很簡單,就是遍歷待刪除的 vnodes 做刪除,其中 removeAndInvokeRemoveHook 的作用是從 DOM 中移除節(jié)點并執(zhí)行 module 的 remove 鉤子函數(shù),并對它的子節(jié)點遞歸調(diào)用 removeAndInvokeRemoveHook 函數(shù);invokeDestroyHook 是執(zhí)行 module 的 destory 鉤子函數(shù)以及 vnode 的 destory 鉤子函數(shù),并對它的子 vnode 遞歸調(diào)用 invokeDestroyHook 函數(shù);removeNode 就是調(diào)用平臺的 DOM API 去把真正的 DOM 節(jié)點移除。
在之前介紹組件生命周期的時候提到 beforeDestroy & destroyed 這兩個生命周期鉤子函數(shù),它們就是在執(zhí)行 invokeDestroyHook 過程中,執(zhí)行了 vnode 的 destory 鉤子函數(shù),它的定義在 src/core/vdom/create-component.js 中:

const componentVNodeHooks = {
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

當(dāng)組件并不是 keepAlive 的時候,會執(zhí)行 componentInstance.$destroy() 方法,然后就會執(zhí)行 beforeDestroy & destroyed 兩個鉤子函數(shù)。

當(dāng)兩個vnode相同的時候開始執(zhí)行patchVnode:

  function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

這里也大致分為4個步驟:

1. 執(zhí)行prepatch鉤子函數(shù)

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

看下prepatch的定義:

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

prepatch 方法就是拿到新的 vnode 的組件配置以及組件實例,去執(zhí)行 updateChildComponent 方法,它的定義在 src/core/instance/lifecycle.js 中:

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = true
  }

  // determine whether component has slot children
  // we need to do this before overwriting $options._renderChildren.

  // check if there are dynamic scopedSlots (hand-written or compiled but with
  // dynamic slot names). Static scoped slots compiled from template has the
  // "$stable" marker.
  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  const hasDynamicScopedSlot = !!(
    (newScopedSlots && !newScopedSlots.$stable) ||
    (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
    (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
  )

  // Any static slot children from the parent may have changed during parent's
  // update. Dynamic scoped slots may also have changed. In such cases, a forced
  // update is necessary to ensure correctness.
  const needsForceUpdate = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )

  vm.$options._parentVnode = parentVnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render

  if (vm._vnode) { // update child tree's parent
    vm._vnode.parent = parentVnode
  }
  vm.$options._renderChildren = renderChildren

  // update $attrs and $listeners hash
  // these are also reactive so they may trigger child update if the child
  // used them during render
  vm.$attrs = parentVnode.data.attrs || emptyObject
  vm.$listeners = listeners || emptyObject

  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }

  // update listeners
  listeners = listeners || emptyObject
  const oldListeners = vm.$options._parentListeners
  vm.$options._parentListeners = listeners
  updateComponentListeners(vm, listeners, oldListeners)

  // resolve slots + force update if has children
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }

  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = false
  }
}

就是更新了vnode.

2. 執(zhí)行 update 鉤子函數(shù)

    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }

3. 完成 patch 過程

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }

大致邏輯是這樣的:

  1. 新節(jié)點存在文本節(jié)點的情況下,當(dāng)舊節(jié)點的子節(jié)點和新節(jié)點的子節(jié)點都存在的情況下,開始diff算法,等會講。
  2. 當(dāng)舊節(jié)點沒有子節(jié)點的情況下,檢查新節(jié)點子節(jié)點的key的重復(fù)性.如果舊節(jié)點存在文本節(jié)點,則清空文本節(jié)點。然后批量將新節(jié)點的子節(jié)點添加到elem后面
  3. 如果舊節(jié)點存在子節(jié)點,則移除所以的子節(jié)點。
  4. 如果新節(jié)點沒有子節(jié)點但是舊節(jié)點有子節(jié)點,則清空舊節(jié)點的文本內(nèi)容
  5. 若新舊節(jié)點文本內(nèi)容不同則替換。
    然來開看第一步的diff算法:
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        debugger
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

diff算法其實遵循幾個原則:

  1. 能移動就移動
  2. 先比頭和尾
  3. 頭尾比不了,就找中間key或索引一樣的比,然后添加到舊頭
  4. 新vnode較長則在舊vnode上添加node,較短則刪除。

props

在Vue的初始化的時候有這么一個流程:

規(guī)范化:
initGlobalAPI(Vue) => initMixin(Vue) => mergeOptions => normalizeProps
當(dāng) props 是一個數(shù)組,每一個數(shù)組元素 prop 只能是一個 string,表示 prop 的 key,轉(zhuǎn)成駝峰格式,prop 的類型為空。

當(dāng) props 是一個對象,對于 props 中每個 prop 的 key,我們會轉(zhuǎn)駝峰格式,而它的 value,如果不是一個對象,我們就把它規(guī)范成一個對象。
如果 props 既不是數(shù)組也不是對象,就拋出一個警告。
初始化:


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 主要做 3 件事情:校驗、響應(yīng)式和代理。
校驗


export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // boolean casting
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    // skip validation for weex recycle-list child component props
    !(__WEEX__ && isObject(value) && ('@binding' in value))
  ) {
    assertProp(prop, key, value, vm, absent)
  }
  return value
}

validateProp 主要就做 3 件事情:處理 Boolean 類型的數(shù)據(jù),處理默認(rèn)數(shù)據(jù),prop 斷言,并最終返回 prop 的值。
Boolean 類型特殊處理

若是定義proptype時,Boolean為其中之一,則可能有如下情況,則重新設(shè)置該prop的值。

  1. 無值且無默認(rèn)值的情況:調(diào)用組件時未傳入prop的值 && prop定義時未設(shè)置默認(rèn)值,則將prop的值置為false

  2. 針對布爾特性的情況:調(diào)用組件時傳入的prop的值為空字符串 || prop的值為key的連字符形式,則可能出現(xiàn)如下情況:

    • prop指定的類型里沒有String,則將prop的值置為true
    • prop指定的類型里有String,但是Boolean類型在String之前,則將prop的值置為false

經(jīng)過以上Boolean類型的處理之后,若是prop的值仍為undefined,則將獲取prop的默認(rèn)值。

非Boolean類型的默認(rèn)值

  1. default是函數(shù) && prop配置的type里沒有Function,則返回該函數(shù)調(diào)用后的返回值作為默認(rèn)值
  2. default是函數(shù) && prop配置的type里有Function,則返回該函數(shù)作為默認(rèn)值
  3. default為非函數(shù)類型,則返回該default值作為默認(rèn)值

驗證

  • 需要做以下三個驗證
  • case 1: 驗證 required 屬性
  • case 2: prop 定義時是 required,但是調(diào)用組件時沒有傳遞該值(警告)
  • case 3: prop 定義時是非 required 的,且 value === null || value === undefined(符合要求,返回)
  • case 4: 驗證 type 屬性-- value 的類型必須是 type 數(shù)組里的其中之一
  • case 5: 驗證自定義驗證函數(shù)

響應(yīng)式

defineReactive(props, key, value)

代理

defineReactive(props, key, value)
proxy(vm, `_props`, key)

總結(jié):

1. 創(chuàng)建響應(yīng)式對象:

observe(xxx) => xxx是數(shù)組 ?遞歸observe :walk(調(diào)用defineReactive), observe整個動作是vue核心庫需要的,最后變成響應(yīng)式對象是靠defineReactive實現(xiàn)的。

依賴收集和派發(fā)更新有2個概念:

  1. dep ,這是一個包含 依賴id, 訂閱者(watcher), 依賴數(shù)組的類,這類主要是用來管理watcher的。
  2. watcher,這個類的作用主要是數(shù)據(jù)發(fā)生變化的時候,調(diào)用各個方法產(chǎn)生計算和更改視圖的,包含了各種屬性。

2. 依賴收集:

依賴收集發(fā)生在數(shù)據(jù)屬性的get階段。干了這么一件事:
將當(dāng)前wather變成持有這個dep的訂閱者。

依賴收集要清空原來依賴,使用的新的依賴。防止重復(fù)訂閱的浪費(fèi)。

3. 派發(fā)更新:

派發(fā)更新主要發(fā)生在數(shù)據(jù)屬性的set階段, 做了這么幾個步驟:

  1. 判斷新值和舊值如果相等或者同為NaN,則直接返回。
  2. 通過observe將新值變?yōu)轫憫?yīng)式對象
  3. 通過dep.notify()派發(fā)更新。

派發(fā)更新的流程:
值發(fā)生改變 => dep.notify()=> 遍歷觸發(fā)訂閱的wathcer的update=> 根據(jù)是否同步等條件最后觸發(fā)watcher.run方法(渲染函數(shù)觸發(fā)get執(zhí)行更新DOM)=> 觸發(fā)watcher的回調(diào)(用戶可以拿到新值和舊值)

4. [檢測變化的注意事項]

通過a.b = 1給對象新添加屬性。
通過arr[0] = 1直接給原數(shù)組的元素賦值。
通過vm.items.length = newLength修改數(shù)組長度。
上述3中可以通過set方法變成響應(yīng)式。核心還是通過defineReactive和dep.notify()

5. 計算屬性VS偵聽屬性

  1. 計算屬性在創(chuàng)建wathcer的時候會置上一個標(biāo)志位lazy,做了2層優(yōu)化;

    1. 在初始化的時候不會去計算
    2. 在更新時候比較前后值是否一樣否則不會渲染(這是所有相應(yīng)是數(shù)據(jù)都一樣)。
  2. watch
    就是通過traverse做了遞歸響應(yīng)式。
    watcher的種類:
    deep watcher(遞歸添加watcher)
    user watcher(用戶的watcher)
    computed watcher
    sync watcher(可以在user watcher里配置)
    renderwatcher(渲染 watcher,一般來說處的位置靠上)

6. 組件更新:

說下數(shù)據(jù)變化到更新整個流程:

依賴發(fā)生變化 => watcher.getter => vm._update => vm.patch

判斷是否已是同一個節(jié)點的邏輯:
key => tag => isComment => data => sameInputType

如果新舊節(jié)點不同,那么主要分3步進(jìn)行:

  1. 創(chuàng)建新節(jié)點
  2. 更新父的占位節(jié)點
  3. 刪除舊節(jié)點

如果新舊節(jié)點相同:

  1. 執(zhí)行prepatch鉤子函數(shù)(去拿新的 vnode 的組件配置以及組件實例,去執(zhí)行 updateChildComponent 方法)
  2. 執(zhí)行 update 鉤子函數(shù)
  3. 完成 patch 過程

patch大致邏輯是這樣的:

  1. 新節(jié)點存在文本節(jié)點的情況下,當(dāng)舊節(jié)點的子節(jié)點和新節(jié)點的子節(jié)點都存在的情況下,開始diff算法。
  2. 當(dāng)舊節(jié)點沒有子節(jié)點的情況下,檢查新節(jié)點子節(jié)點的key的重復(fù)性.如果舊節(jié)點存在文本節(jié)點,則清空文本節(jié)點。然后批量將新節(jié)點的子節(jié)點添加到elem后面
  3. 如果舊節(jié)點存在子節(jié)點,則移除所以的子節(jié)點。
  4. 如果新節(jié)點沒有子節(jié)點但是舊節(jié)點有子節(jié)點,則清空舊節(jié)點的文本內(nèi)容
  5. 若新舊節(jié)點文本內(nèi)容不同則替換。

diff算法其實遵循幾個原則:

能移動就移動
先比頭和尾
頭尾比不了,就找中間key或索引一樣的比,然后添加到舊頭
新vnode較長則在舊vnode上添加node,較短則刪除。

7. Props;

  1. 規(guī)范化
    當(dāng) props 是一個數(shù)組,每一個數(shù)組元素 prop 只能是一個 string,表示 prop 的 key,轉(zhuǎn)成駝峰格式,prop 的類型為空。
    當(dāng) props 是一個對象,對于 props 中每個 prop 的 key,我們會轉(zhuǎn)駝峰格式,而它的 value,如果不是一個對象,我們就把它規(guī)范成一個對象。
    如果 props 既不是數(shù)組也不是對象,就拋出一個警告。
  2. 初始化(校驗、響應(yīng)式和代理)

釋疑:
1. 為什么說Vue是異步更新,因為dep.notify()調(diào)用了watcher的udpate方法,這個方法調(diào)用了queueWatcher方方法,最終調(diào)用了nextTick這個異步方法,所以是異步更新的。
2. 因為在依賴收集階段如果碰到對象里面的屬性是數(shù)組的,我們對該屬性是沒有做依賴收集的,我們對對象屬性的值判斷是通過===來看他是否發(fā)生變化的,因此數(shù)組元素的變化是無法觸發(fā)更新的
我們能在watch拿到新值和舊值的原因是因為在wather執(zhí)行run的時候,會將新舊值傳到回調(diào)里
3. push, unshift, splice是響應(yīng)式操作的原因是Vue重寫了這3個方法。
4. 為什么每次獲取計算屬性的值時都要進(jìn)行依賴收集呢,而不是僅進(jìn)行一次性的依賴收集?原因是,計算屬性的依賴項可能會改變,這次有x個依賴項,下次可能有y個依賴項。比如三元表達(dá)式

返回目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容