Vue 源碼深入解析之 深入響應式原理、響應式對象、依賴收集 和 派發更新

一、深入響應式原理

  1. 之前都是 Vue 怎麼實現數據渲染和組件化的,主要是初始化的過程,把原始的數據最終映射到 DOM 中,但並沒有涉及到數據變化到 DOM 變化的部分。而 Vue 的數據驅動除了數據渲染 DOM 之外,還有一個很重要的體現就是數據的變更會觸發 DOM 的變化。

  2. 其實前端開發最重要的兩個工作,一個是把數據渲染到頁面,另一個是處理用戶交互。Vue 把數據渲染到頁面的能力我們已經通過源碼分析出其中的原理了,但是由於一些用戶交互或者是其它方面導致數據發生變化重新對頁面渲染的原理我們還未分析,考慮如下示例:

<div id="app" @click="changeMsg">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  methods: {
    changeMsg() {
      this.message = 'Hello World!'
    }
  }
})

當我們去修改 this.message 的時候,模板對應的插值也會渲染成新的數據,那麼這一切是怎麼做到的呢?

  1. 在分析前,我們先直觀的想一下,如果不用 Vue 的話,我們會通過最簡單的方法實現這個需求:監聽點擊事件,修改數據,手動操作 DOM 重新渲染。這個過程和使用 Vue 的最大區別就是多了一步“手動操作 DOM 重新渲染”。這一步看上去並不多,但它背後又潛在的幾個要處理的問題:
  • 我需要修改哪塊的 DOM
  • 我的修改效率和性能是不是最優的?
  • 我需要對數據每一次的修改都去操作 DOM 嗎?
  • 我需要 case by case 去寫修改 DOM 的邏輯嗎?
  1. 如果我們使用了 Vue,那麼上面幾個問題 Vue 內部就幫你做了,那麼 Vue 是如何在我們對數據修改後自動做這些事情呢,接下來就是 Vue 響應式系統。

二、響應式對象

  1. Vue.js 實現響應式的核心是利用了 ES5Object.defineProperty,這也是爲什麼 Vue.js 不能兼容 IE8 及以下瀏覽器的原因,我們先來對它有個直觀的認識。

  2. Object.definePropertyObject.defineProperty 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回這個對象,先來看一下它的語法:

Object.defineProperty(obj, prop, descriptor)

obj 是要在其上定義屬性的對象;prop 是要定義或修改的屬性的名稱;descriptor 是將被定義或修改的屬性描述符。

  1. 這裏比較核心的是 descriptor,它有很多可選鍵值,具體的可以去參閱它的文檔。這裏我們最關心的是 getsetget 是一個給屬性提供的 getter 方法,當我們訪問了該屬性的時候會觸發 getter 方法;set 是一個給屬性提供的 setter 方法,當我們對該屬性做修改的時候會觸發 setter 方法。一旦對象擁有了 gettersetter,我們可以簡單地把這個對象稱爲響應式對象。那麼 Vue.js 把哪些對象變成了響應式對象了呢,接下來我們從源碼層面分析。

  2. initState,在 Vue 的初始化階段,_init 方法執行的時候,會執行 initState(vm) 方法,它的定義在 src/core/instance/state.js 中,如下所示:

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

initState 方法主要是對 propsmethodsdatacomputedwathcer 等屬性做了初始化操作。這裏我們重點分析 propsdata,對於其它屬性的初始化我們之後再詳細分析。

  1. 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 (vm.$parent && !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)
}

props 的初始化主要過程,就是遍歷定義的 props 配置。遍歷的過程主要做兩件事情:一個是調用 defineReactive 方法把每個 prop 對應的值變成響應式,可以通過 vm._props.xxx 訪問到定義 props 中對應的屬性。對於 defineReactive 方法,我們稍後會介紹;另一個是通過 proxyvm._props.xxx 的訪問代理到 vm.xxx 上,我們稍後也會介紹。

  1. initData,如下所示:
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 */)
}

data 的初始化主要過程也是做兩件事,一個是對定義 data 函數返回對象的遍歷,通過 proxy 把每一個值 vm._data.xxx 都代理到 vm.xxx 上;另一個是調用 observe 方法觀測整個 data 的變化,把 data 也變成響應式,可以通過 vm._data.xxx 訪問到定義 data 返回函數中對應的屬性,observe 後面會介紹。可以看到,無論是 props 或是 data 的初始化都是把它們變成響應式對象,這個過程我們接觸到幾個函數,接下來我們來詳細分析它們。

  1. proxy,首先介紹一下代理,代理的作用是把 propsdata 上的屬性代理到 vm 實例上,這也就是爲什麼比如我們定義瞭如下 props,卻可以通過 vm 實例訪問到它,如下所示:
let comP = {
  props: {
    msg: 'hello'
  },
  methods: {
    say() {
      console.log(this.msg)
    }
  }
}

我們可以在 say 函數中通過 this.msg 訪問到我們定義在 props 中的 msg,這個過程發生在 proxy 階段:

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)
}
  1. proxy 方法的實現很簡單,通過 Object.definePropertytarget[sourceKey][key] 的讀寫變成了對 target[key] 的讀寫。所以對於 props 而言,對 vm._props.xxx 的讀寫變成了 vm.xxx 的讀寫,而對於 vm._props.xxx 我們可以訪問到定義在 props 中的屬性,所以我們就可以通過 vm.xxx 訪問到定義在 props 中的 xxx 屬性了。同理,對於 data 而言,對 vm._data.xxxx 的讀寫變成了對 vm.xxxx 的讀寫,而對於 vm._data.xxxx 我們可以訪問到定義在 data 函數返回對象中的屬性,所以我們就可以通過 vm.xxxx 訪問到定義在 data 函數返回對象中的 xxxx 屬性了。

  2. observeobserve 的功能就是用來監測數據的變化,它的定義在 src/core/observer/index.js 中:

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
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 方法的作用就是給非 VNode 的對象類型數據添加一個 Observer,如果已經添加過則直接返回,否則在滿足一定條件下去實例化一個 Observer 對象實例。接下來我們來看一下 Observer 的作用。

  1. ObserverObserver 是一個類,它的作用是給對象的屬性添加 gettersetter,用於依賴收集和派發更新:
/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through each property 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])
    }
  }
}
  1. Observer 的構造函數邏輯很簡單,首先實例化 Dep 對象,這塊稍後會介紹,接着通過執行 def 函數把自身實例添加到數據對象 value__ob__ 屬性上,def 的定義在 src/core/util/lang.js 中:
/**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

def 函數是一個非常簡單的Object.defineProperty 的封裝,這就是爲什麼我在開發中輸出 data 上對象類型的數據,會發現該對象多了一個 __ob__ 的屬性。

  1. 回到 Observer 的構造函數,接下來會對 value 做判斷,對於數組會調用 observeArray 方法,否則對純對象調用 walk 方法。可以看到 observeArray 是遍歷數組再次調用 observe 方法,而 walk 方法是遍歷對象的 key 調用 defineReactive 方法,那麼我們來看一下這個方法是做什麼的。

  2. defineReactivedefineReactive 的功能就是定義一個響應式對象,給對象動態添加 gettersetter,它的定義在 src/core/observer/index.js 中:

/**
 * Define a reactive property on an Object.
 */
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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
  1. defineReactive 函數最開始初始化 Dep 對象的實例,接着拿到 obj 的屬性描述符,然後對子對象遞歸調用 observe 方法,這樣就保證了無論 obj 的結構多複雜,它的所有子屬性也能變成響應式的對象,這樣我們訪問或修改 obj 中一個嵌套較深的屬性,也能觸發 gettersetter。最後利用 Object.defineProperty 去給 obj 的屬性 key 添加 gettersetter

  2. 總結:這裏介紹了響應式對象,核心就是利用 Object.defineProperty 給數據添加了 gettersetter,目的就是爲了在我們訪問數據以及寫數據的時候能自動執行一些邏輯:getter 做的事情是依賴收集,setter 做的事情是派發更新。

三、依賴收集

  1. 瞭解 Vue 會把普通對象變成響應式對象,響應式對象 getter 相關的邏輯就是做依賴收集,詳細分析這個過程,我們先來回顧一下 getter 部分的邏輯:
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
    },
    // ...
  })
}

這段代碼我們只需要關注 2 個地方,一個是 const dep = new Dep() 實例化一個 Dep 的實例,另一個是在 get 函數中通過 dep.depend 做依賴收集,這裏還有個對 childOb 判斷的邏輯,我們之後會介紹它的作用。

  1. DepDep 是整個 getter 依賴收集的核心,它的定義在 src/core/observer/dep.js 中:
import type Watcher from './watcher'
import { remove } from '../util/index'

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

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

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

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

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

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

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []

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

export function popTarget () {
  Dep.target = targetStack.pop()
}
  1. Dep 是一個 Class,它定義了一些屬性和方法,這裏需要特別注意的是它有一個靜態屬性 target,這是一個全局唯一 Watcher,這是一個非常巧妙的設計,因爲在同一時間只能有一個全局的 Watcher 被計算,另外它的自身屬性 subs 也是 Watcher 的數組。Dep 實際上就是對 Watcher 的一種管理,Dep 脫離 Watcher 單獨存在是沒有意義的,依賴收集過程,我們有必要看一下 Watcher 的一些相關實現,它的定義在 src/core/observer/watcher.js 中。

  2. Watcher,如下所示:

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;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  dep: Dep;
  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.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed 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 = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = 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
  }
  // ...
}
  1. Watcher 是一個 Class,在它的構造函數中,定義了一些和 Dep 相關的屬性:
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
  1. 其中,this.depsthis.newDeps 表示 Watcher 實例持有的 Dep 實例的數組;而 this.depIdsthis.newDepIds 分別代表 this.depsthis.newDepsid Set(這個 Set 是 ES6 的數據結構,它的實現在 src/core/util/env.js 中)。Watcher 還定義了一些原型的方法,和依賴收集相關的有 getaddDepcleanupDeps 方法。

  2. 過程分析,當對數據對象的訪問會觸發他們的 getter 方法,那麼這些對象什麼時候被訪問呢?在 Vuemount 過程是通過 mountComponent 函數,其中有一段比較重要的邏輯,大致如下:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
  1. 當我們去實例化一個渲染 watcher 的時候,首先進入 watcher 的構造函數邏輯,然後會執行它的 this.get() 方法,進入 get 函數,首先會執行:
pushTarget(this)

pushTarget 的定義在 src/core/observer/dep.js 中:

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

實際上就是把 Dep.target 賦值爲當前的渲染 watcher 並壓棧(爲了恢復用)。接着又執行了:

value = this.getter.call(vm, vm)

this.getter 對應就是 updateComponent 函數,這實際上就是在執行:

vm._update(vm._render(), hydrating)
  1. 它會先執行 vm._render() 方法,因爲之前分析過這個方法會生成 渲染 VNode,並且在這個過程中會對 vm 上的數據訪問,這個時候就觸發了數據對象的 getter。那麼每個對象值的 getter 都持有一個 dep,在觸發 getter 的時候會調用 dep.depend() 方法,也就會執行 Dep.target.addDep(this)。剛纔我們提到這個時候 Dep.target 已經被賦值爲渲染 watcher,那麼就執行到 addDep 方法:
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)
    }
  }
}
  1. 這時候會做一些邏輯判斷(保證同一數據不會被添加多次)後執行 dep.addSub(this),那麼就會執行 this.subs.push(sub),也就是說把當前的 watcher 訂閱到這個數據持有的 depsubs 中,這個目的是爲後續數據變化時候能通知到哪些 subs 做準備。所以在 vm._render() 過程中,會觸發所有數據的 getter,這樣實際上已經完成了一個依賴收集的過程。那麼到這裏就結束了麼,其實並沒有,在完成依賴收集後,還有幾個邏輯要執行,首先是:
if (this.deep) {
  traverse(value)
}

這個是要遞歸去訪問 value,觸發它所有子項的 getter,這個之後會詳細講。接下來執行:

popTarget()

popTarget 的定義在 src/core/observer/dep.js 中:

Dep.target = targetStack.pop()

實際上就是把 Dep.target 恢復成上一個狀態,因爲當前 vm 的數據依賴收集已經完成,那麼對應的渲染Dep.target 也需要改變。最後執行:

this.cleanupDeps()
  1. 瞭解到 Vue 有依賴收集的過程,但分析依賴清空的過程,其實這是會忽視的一點,也是 Vue 考慮特別細的一點,如下所示:
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
}
  1. 考慮到 Vue 是數據驅動的,所以每次數據變化都會重新 render,那麼 vm._render() 方法又會再次執行,並再次觸發數據的 getters,所以 Wathcer 在構造函數中會初始化兩個 Dep 實例數組,newDeps 表示新添加的 Dep 實例數組,而 deps 表示上一次添加的 Dep 實例數組。在執行 cleanupDeps 函數的時候,會首先遍歷 deps,移除對 dep.subs 數組中 Wathcer 的訂閱,然後把 newDepIdsdepIds 交換,newDepsdeps 交換,並把 newDepIdsnewDeps 清空。

  2. 那麼爲什麼需要做 deps 訂閱的移除呢,在添加 deps 的訂閱過程,已經能通過 id 去重避免重複訂閱了。考慮到一種場景,我們的模板會根據 v-if 去渲染不同子模板 ab,當我們滿足某種條件的時候渲染 a 的時候,會訪問到 a 中的數據,這時候我們對 a 使用的數據添加了 getter,做了依賴收集,那麼當我們去修改 a 的數據的時候,理應通知到這些訂閱者。那麼如果我們一旦改變了條件渲染了 b 模板,又會對 b 使用的數據添加了 getter,如果我們沒有依賴移除的過程,那麼這時候我去修改 a 模板的數據,會通知 a 數據的訂閱的回調,這顯然是有浪費的。因此 Vue 設計了在每次添加完新的訂閱,會移除掉舊的訂閱,這樣就保證了在我們剛纔的場景中,如果渲染 b 模板的時候去修改 a 模板的數據,a 數據訂閱回調已經被移除了,所以不會有任何浪費。

  3. 總結:我們對 Vue 數據的依賴收集過程已經有了認識,並且對這其中的一些細節做了分析。收集依賴的目的是爲了當這些響應式數據發生變化,觸發它們的 setter 的時候,能知道應該通知哪些訂閱者去做相應的邏輯處理,我們把這個過程叫派發更新,其實 WatcherDep 就是一個非常經典的觀察者設計模式的實現。

四、派發更新

  1. 響應式數據依賴收集過程,收集的目的就是爲了當我們修改數據的時候,可以對相關的依賴派發更新,那麼詳細分析這個過程,回顧一下 setter 部分的邏輯:
/**
 * Define a reactive property on an Object.
 */
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,
    // ...
    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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

setter 的邏輯有 2 個關鍵的點,一個是 childOb = !shallow && observe(newVal),如果 shallow 爲 false 的情況,會對新設置的值變成一個響應式對象;另一個是 dep.notify(),通知所有的訂閱者,接下來會完整的分析整個派發更新的過程。

  1. 過程分析,當我們在組件中對響應的數據做了修改,就會觸發 setter 的邏輯,最後調用 dep.notify() 方法,
    它是 Dep 的一個實例方法,定義在 src/core/observer/dep.js 中:
class Dep {
  // ...
  notify () {
  // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

這裏的邏輯非常簡單,遍歷所有的 subs,也就是 Watcher 的實例數組,然後調用每一個 watcherupdate 方法,它的定義在 src/core/observer/watcher.js 中:

class Watcher {
  // ...
  update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}  

這裏對於 Watcher 的不同狀態,會執行不同的邏輯,computedsync 等狀態的分析,在一般組件數據更新的場景,會走到最後一個 queueWatcher(this) 的邏輯,queueWatcher 的定義在 src/core/observer/scheduler.js 中:

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
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
      nextTick(flushSchedulerQueue)
    }
  }
}
  1. 這裏引入了一個隊列的概念,這也是 Vue 在做派發更新的時候的一個優化的點,它並不會每次數據改變都觸發 watcher 的回調,而是把這些 watcher 先添加到一個隊列裏,然後在 nextTick 後執行 flushSchedulerQueue。這裏有幾個細節要注意一下,首先用 has 對象保證同一個 Watcher 只添加一次;接着對 flushing 的判斷;最後通過 waiting 保證對 nextTick(flushSchedulerQueue) 的調用邏輯只有一次,另外 nextTick 的實現後面會分析,目前就可以理解它是在下一個 tick,也就是異步的去執行 flushSchedulerQueue

  2. 接下來我們來看 flushSchedulerQueue 的實現,它的定義在 src/core/observer/scheduler.js 中,如下所示:

let flushing = false
let index = 0
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  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')
  }
}

這裏有幾個重要的邏輯要梳理一下,對於一些分支邏輯如 keep-alive 組件相關和之前提到過的 updated 鉤子函數的執行會略過。

  1. 隊列排序,queue.sort((a, b) => a.id - b.id) 對隊列做了從小到大的排序,這麼做主要有以下要確保以下幾點:
  • 組件的更新由父到子;因爲父組件的創建過程是先於子的,所以 watcher 的創建也是先父後子,執行順序也應該保持先父後子。
  • 用戶的自定義 watcher 要優先於渲染 watcher 執行;因爲用戶自定義 watcher 是在渲染 watcher 之前創建的。
  • 如果一個組件在父組件的 watcher 執行期間被銷燬,那麼它對應的 watcher 執行都可以被跳過,所以父組件的 watcher 應該先執行。
  1. 隊列遍歷,在對 queue 排序後,接着就是要對它做遍歷,拿到對應的 watcher,執行 watcher.run()。這裏需要注意一個細節,在遍歷的時候每次都會對 queue.length 求值,因爲在 watcher.run() 的時候,很可能用戶會再次添加新的 watcher,這樣會再次執行到 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)
    }
    // ...
  }
}

可以看到,這時候 flushing 爲 true,就會執行到 else 的邏輯,然後就會從後往前找,找到第一個待插入 watcher 的 id 比當前隊列中 watcher 的 id 大的位置。把 watcher 按照 id的插入到隊列中,因此 queue 的長度發生了變化。

  1. 狀態恢復,這個過程就是執行 resetSchedulerState 函數,它的定義在 src/core/observer/scheduler.js 中,如下所示:
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
/**
 * Reset the scheduler's state.
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

邏輯非常簡單,就是把這些控制流程狀態的一些變量恢復到初始值,把 watcher 隊列清空。

  1. 接下來我們繼續分析 watcher.run() 的邏輯,它的定義在 src/core/observer/watcher.js 中,如下所示:
class Watcher {
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

  getAndInvoke (cb: Function) {
    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
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }
}
  1. run 函數實際上就是執行 this.getAndInvoke 方法,並傳入 watcher 的回調函數。getAndInvoke 函數邏輯也很簡單,先通過 this.get() 得到它當前的值,然後做判斷,如果滿足新舊值不等、新值是對象類型、deep 模式任何一個條件,則執行 watcher 的回調,注意回調函數執行的時候會把第一個和第二個參數傳入新值 value 和舊值 oldValue,這就是當我們添加自定義 watcher 的時候能在回調函數的參數中拿到新舊值的原因。

  2. 那麼對於渲染 watcher 而言,它在執行 this.get() 方法求值的時候,會執行 getter 方法:

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

所以這就是當我們去修改組件相關的響應式數據的時候,會觸發組件重新渲染的原因,接着就會重新執行 patch 的過程,但它和首次渲染有所不同。

  1. 總結:對 Vue 數據修改派發更新的過程也有了認識,實際上就是當數據發生變化的時候,觸發 setter 邏輯,把在依賴過程中訂閱的的所有觀察者,也就是 watcher,都觸發它們的 update 過程,這個過程又利用了隊列做了進一步優化,在 nextTick 後執行所有 watcherrun,最後執行它們的回調函數。nextTickVue 一個比較核心的實現了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章