Vue 原始碼解讀(3)—— 響應式原理

李永寧發表於2022-02-23

前言

上一篇文章 Vue 原始碼解讀(2)—— Vue 初始化過程 詳細講解了 Vue 的初始化過程,明白了 new Vue(options) 都做了什麼,其中關於 資料響應式 的實現用一句話簡單的帶過,而這篇文章則會詳細講解 Vue 資料響應式的實現原理。

目標

  • 深入理解 Vue 資料響應式原理。

  • methods、computed 和 watch 有什麼區別?

原始碼解讀

經過上一篇文章的學習,相信關於 響應式原理 原始碼閱讀的入口位置大家都已經知道了,就是初始化過程中處理資料響應式這一步,即呼叫 initState 方法,在 /src/core/instance/init.js 檔案中。

initState

/src/core/instance/state.js

/**
 * 兩件事:
 *   資料響應式的入口:分別處理 props、methods、data、computed、watch
 *   優先順序:props、methods、data、computed 物件中的屬性不能出現重複,優先順序和列出順序一致
 *         其中 computed 中的 key 不能和 props、data 中的 key 重複,methods 不影響
 */
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 處理 props 物件,為 props 物件的每個屬性設定響應式,並將其代理到 vm 例項上
  if (opts.props) initProps(vm, opts.props)
  // 處理 methos 物件,校驗每個屬性的值是否為函式、和 props 屬性比對進行判重處理,最後得到 vm[key] = methods[key]
  if (opts.methods) initMethods(vm, opts.methods)
  /**
   * 做了三件事
   *   1、判重處理,data 物件上的屬性不能和 props、methods 物件上的屬性相同
   *   2、代理 data 物件上的屬性到 vm 例項
   *   3、為 data 物件的上資料設定響應式 
   */
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  /**
   * 三件事:
   *   1、為 computed[key] 建立 watcher 例項,預設是懶執行
   *   2、代理 computed[key] 到 vm 例項
   *   3、判重,computed 中的 key 不能和 data、props 中的屬性重複
   */
  if (opts.computed) initComputed(vm, opts.computed)
  /**
   * 三件事:
   *   1、處理 watch 物件
   *   2、為 每個 watch.key 建立 watcher 例項,key 和 watcher 例項可能是 一對多 的關係
   *   3、如果設定了 immediate,則立即執行 回撥函式
   */
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
    
  /**
   * 其實到這裡也能看出,computed 和 watch 在本質是沒有區別的,都是通過 watcher 去實現的響應式
   * 非要說有區別,那也只是在使用方式上的區別,簡單來說:
   *   1、watch:適用於當資料變化時執行非同步或者開銷較大的操作時使用,即需要長時間等待的操作可以放在 watch 中
   *   2、computed:其中可以使用非同步方法,但是沒有任何意義。所以 computed 更適合做一些同步計算
   */
}

initProps

src/core/instance/state.js

// 處理 props 物件,為 props 物件的每個屬性設定響應式,並將其代理到 vm 例項上
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // 快取 props 的每個 key,效能優化
  // 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)
  }
  // 遍歷 props 物件
  for (const key in propsOptions) {
    // 快取 key
    keys.push(key)
    // 獲取 props[key] 的預設值
    const value = validateProp(key, propsOptions, propsData, vm)
    // 為 props 的每個 key 是設定資料響應式
    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)) {
      // 代理 key 到 vm 物件上
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

proxy

/src/core/instance/state.js

// 設定代理,將 key 代理到 target 上
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)
}

initMethods

/src/core/instance/state.js

/**
 * 做了以下三件事,其實最關鍵的就是第三件事情
 *   1、校驗 methoss[key],必須是一個函式
 *   2、判重
 *         methods 中的 key 不能和 props 中的 key 相同
 *         methos 中的 key 與 Vue 例項上已有的方法重疊,一般是一些內建方法,比如以 $ 和 _ 開頭的方法
 *   3、將 methods[key] 放到 vm 例項上,得到 vm[key] = methods[key]
 */
function initMethods (vm: Component, methods: Object) {
  // 獲取 props 配置項
  const props = vm.$options.props
  // 遍歷 methods 物件
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

initData

src/core/instance/state.js

/**
 * 做了三件事
 *   1、判重處理,data 物件上的屬性不能和 props、methods 物件上的屬性相同
 *   2、代理 data 物件上的屬性到 vm 例項
 *   3、為 data 物件的上資料設定響應式 
 */
function initData (vm: Component) {
  // 得到 data 物件
  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
    )
  }
  /**
   * 兩件事
   *   1、判重處理,data 物件上的屬性不能和 props、methods 物件上的屬性相同
   *   2、代理 data 物件上的屬性到 vm 例項
   */
  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)
    }
  }
  // 為 data 物件上的資料設定響應式
  observe(data, true /* asRootData */)
}

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

initComputed

/src/core/instance/state.js

const computedWatcherOptions = { lazy: true }

/**
 * 三件事:
 *   1、為 computed[key] 建立 watcher 例項,預設是懶執行
 *   2、代理 computed[key] 到 vm 例項
 *   3、判重,computed 中的 key 不能和 data、props 中的屬性重複
 * @param {*} computed = {
 *   key1: function() { return xx },
 *   key2: {
 *     get: function() { return xx },
 *     set: function(val) {}
 *   }
 * }
 */
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()

  // 遍歷 computed 物件
  for (const key in computed) {
    // 獲取 key 對應的值,即 getter 函式
    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) {
      // 為 computed 屬性建立 watcher 例項
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        // 配置項,computed 預設是懶執行
        computedWatcherOptions
      )
    }

    if (!(key in vm)) {
      // 代理 computed 物件中的屬性到 vm 例項
      // 這樣就可以使用 vm.computedKey 訪問計算屬性了
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 非生產環境有一個判重處理,computed 物件中的屬性不能和 data、props 中的屬性相同
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

/**
 * 代理 computed 物件中的 key 到 target(vm)上
 */
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  // 構造屬性描述符(get、set)
  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
      )
    }
  }
  // 攔截對 target.key 的訪問和設定
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

/**
 * @returns 返回一個函式,這個函式在訪問 vm.computedProperty 時會被執行,然後返回執行結果
 */
function createComputedGetter (key) {
  // computed 屬性值會快取的原理也是在這裡結合 watcher.dirty、watcher.evalaute、watcher.update 實現的
  return function computedGetter () {
    // 得到當前 key 對應的 watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 計算 key 對應的值,通過執行 computed.key 的回撥函式來得到
      // watcher.dirty 屬性就是大家常說的 computed 計算結果會快取的原理
      // <template>
      //   <div>{{ computedProperty }}</div>
      //   <div>{{ computedProperty }}</div>
      // </template>
      // 像這種情況下,在頁面的一次渲染中,兩個 dom 中的 computedProperty 只有第一個
      // 會執行 computed.computedProperty 的回撥函式計算實際的值,
      // 即執行 watcher.evalaute,而第二個就不走計算過程了,
      // 因為上一次執行 watcher.evalute 時把 watcher.dirty 置為了 false,
      // 待頁面更新後,wathcer.update 方法會將 watcher.dirty 重新置為 true,
      // 供下次頁面更新時重新計算 computed.key 的結果
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

/**
 * 功能同 createComputedGetter 一樣
 */
function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

initWatch

/src/core/instance/state.js

/**
 * 處理 watch 物件的入口,做了兩件事:
 *   1、遍歷 watch 物件
 *   2、呼叫 createWatcher 函式
 * @param {*} watch = {
 *   'key1': function(val, oldVal) {},
 *   'key2': 'this.methodName',
 *   'key3': {
 *     handler: function(val, oldVal) {},
 *     deep: true
 *   },
 *   'key4': [
 *     'this.methodNanme',
 *     function handler1() {},
 *     {
 *       handler: function() {},
 *       immediate: true
 *     }
 *   ],
 *   'key.key5' { ... }
 * }
 */
function initWatch (vm: Component, watch: Object) {
  // 遍歷 watch 物件
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      // handler 為陣列,遍歷陣列,獲取其中的每一項,然後呼叫 createWatcher
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

/**
 * 兩件事:
 *   1、相容性處理,保證 handler 肯定是一個函式
 *   2、呼叫 $watch 
 * @returns 
 */
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 如果 handler 為物件,則獲取其中的 handler 選項的值
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 如果 hander 為字串,則說明是一個 methods 方法,獲取 vm[handler]
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

/**
 * 建立 watcher,返回 unwatch,共完成如下 5 件事:
 *   1、相容性處理,保證最後 new Watcher 時的 cb 為函式
 *   2、標示使用者 watcher
 *   3、建立 watcher 例項
 *   4、如果設定了 immediate,則立即執行一次 cb
 *   5、返回 unwatch
 * @param {*} expOrFn key
 * @param {*} cb 回撥函式
 * @param {*} options 配置項,使用者直接呼叫 this.$watch 時可能會傳遞一個 配置項
 * @returns 返回 unwatch 函式,用於取消 watch 監聽
 */
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  // 相容性處理,因為使用者呼叫 vm.$watch 時設定的 cb 可能是物件
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  // options.user 表示使用者 watcher,還有渲染 watcher,即 updateComponent 方法中例項化的 watcher
  options = options || {}
  options.user = true
  // 建立 watcher
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 如果使用者設定了 immediate 為 true,則立即執行一次回撥函式
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // 返回一個 unwatch 函式,用於解除監聽
  return function unwatchFn () {
    watcher.teardown()
  }
}

observe

/src/core/observer/index.js

/**
 * 響應式處理的真正入口
 * 為物件建立觀察者例項,如果物件已經被觀察過,則返回已有的觀察者例項,否則建立新的觀察者例項
 * @param {*} value 物件 => {}
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 非物件和 VNode 例項不做響應式處理
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // 如果 value 物件上存在 __ob__ 屬性,則表示已經做過觀察了,直接返回 __ob__ 屬性
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 建立觀察者例項
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

/src/core/observer/index.js

/**
 * 觀察者類,會被附加到每個被觀察的物件上,value.__ob__ = this
 * 而物件的各個屬性則會被轉換成 getter/setter,並收集依賴和通知更新
 */
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
    // 例項話一個 dep
    this.dep = new Dep()
    this.vmCount = 0
    // 在 value 物件上設定 __ob__ 屬性
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      /**
       * value 為陣列
       * hasProto = '__proto__' in {}
       * 用於判斷物件是否存在 __proto__ 屬性,通過 obj.__proto__ 可以訪問物件的原型鏈
       * 但由於 __proto__ 不是標準屬性,所以有些瀏覽器不支援,比如 IE6-10,Opera10.1
       * 為什麼要判斷,是因為一會兒要通過 __proto__ 運算元據的原型鏈
       * 覆蓋陣列預設的七個原型方法,以實現陣列響應式
       */
      if (hasProto) {
        // 有 __proto__
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // value 為物件,為物件的每個屬性(包括巢狀物件)設定響應式
      this.walk(value)
    }
  }

  /**
   * 遍歷物件上的每個 key,為每個 key 設定響應式
   * 僅當值為物件時才會走這裡
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * 遍歷陣列,為陣列的每一項設定觀察,處理陣列元素為物件的情況
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive

/src/core/observer/index.js

/**
 * 攔截 obj[key] 的讀取和設定操作:
 *   1、在第一次讀取時收集依賴,比如執行 render 函式生成虛擬 DOM 時會有讀取操作
 *   2、在更新時設定新值並通知依賴更新
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 例項化 dep,一個 key 一個 dep
  const dep = new Dep()

  // 獲取 obj[key] 的屬性描述符,發現它是不可配置物件的話直接 return
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // 記錄 getter 和 setter,獲取 val 值
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  // 遞迴呼叫,處理 val 即 obj[key] 的值為物件的情況,保證物件中的所有 key 都被觀察
  let childOb = !shallow && observe(val)
  // 響應式核心
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // get 攔截對 obj[key] 的讀取操作
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      /**
       * Dep.target 為 Dep 類的一個靜態屬性,值為 watcher,在例項化 Watcher 時會被設定
       * 例項化 Watcher 時會執行 new Watcher 時傳遞的回撥函式(computed 除外,因為它懶執行)
       * 而回撥函式中如果有 vm.key 的讀取行為,則會觸發這裡的 讀取 攔截,進行依賴收集
       * 回撥函式執行完以後又會將 Dep.target 設定為 null,避免這裡重複收集依賴
       */
      if (Dep.target) {
        // 依賴收集,在 dep 中新增 watcher,也在 watcher 中新增 dep
        dep.depend()
        // childOb 表示物件中巢狀物件的觀察者物件,如果存在也對其進行依賴收集
        if (childOb) {
          // 這就是 this.key.chidlKey 被更新時能觸發響應式更新的原因
          childOb.dep.depend()
          // 如果是 obj[key] 是 陣列,則觸發陣列響應式
          if (Array.isArray(value)) {
            // 為陣列項為物件的項新增依賴
            dependArray(value)
          }
        }
      }
      return value
    },
    // set 攔截對 obj[key] 的設定操作
    set: function reactiveSetter (newVal) {
      // 舊的 obj[key]
      const value = getter ? getter.call(obj) : val
      // 如果新老值一樣,則直接 return,不跟新更不觸發響應式更新過程
      /* 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()
      }
      // setter 不存在說明該屬性是一個只讀屬性,直接 return
      // #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()
    }
  })
}

dependArray

/src/core/observer/index.js

/**
 * 遍歷每個陣列元素,遞迴處理陣列項為物件的情況,為其新增依賴
 * 因為前面的遞迴階段無法為陣列中的物件元素新增依賴
 */
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

陣列響應式

src/core/observer/array.js

/**
 * 定義 arrayMethods 物件,用於增強 Array.prototype
 * 當訪問 arrayMethods 物件上的那七個方法時會被攔截,以實現陣列響應式
 */
import { def } from '../util/index'

// 備份 陣列 原型物件
const arrayProto = Array.prototype
// 通過繼承的方式建立新的 arrayMethods
export const arrayMethods = Object.create(arrayProto)

// 運算元組的七個方法,這七個方法可以改變陣列自身
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * 攔截變異方法並觸發事件
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  // 快取原生方法,比如 push
  const original = arrayProto[method]
  // def 就是 Object.defineProperty,攔截 arrayMethods.method 的訪問
  def(arrayMethods, method, function mutator (...args) {
    // 先執行原生方法,比如 push.apply(this, args)
    const result = original.apply(this, args)
    const ob = this.__ob__
    // 如果 method 是以下三個之一,說明是新插入了元素
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 對新插入的元素做響應式處理
    if (inserted) ob.observeArray(inserted)
    // 通知更新
    ob.dep.notify()
    return result
  })
})


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

protoAugment

/src/core/observer/index.js

/**
 * 設定 target.__proto__ 的原型物件為 src
 * 比如 陣列物件,arr.__proto__ = arrayMethods
 */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

copyAugment

/src/core/observer/index.js

/**
 * 在目標物件上定義指定屬性
 * 比如陣列:為陣列物件定義那七個方法
 */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

Dep

/src/core/observer/dep.js

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
 * 一個 dep 對應一個 obj.key
 * 在讀取響應式資料時,負責收集依賴,每個 dep(或者說 obj.key)依賴的 watcher 有哪些
 * 在響應式資料更新時,負責通知 dep 中那些 watcher 去執行 update 方法
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

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

  // 在 dep 中新增 watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

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

  // 像 watcher 中新增 dep
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  /**
   * 通知 dep 中的所有 watcher,執行 watcher.update() 方法
   */
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍歷 dep 中儲存的 watcher,執行 watcher.update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

/**
 * 當前正在執行的 watcher,同一時間只會有一個 watcher 在執行
 * Dep.target = 當前正在執行的 watcher
 * 通過呼叫 pushTarget 方法完成賦值,呼叫 popTarget 方法完成重置(null)
 */
Dep.target = null
const targetStack = []

// 在需要進行依賴收集的時候呼叫,設定 Dep.target = watcher
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

// 依賴收集結束呼叫,設定 Dep.target = null
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher

/src/core/observer/watcher.js

/**
 * 一個元件一個 watcher(渲染 watcher)或者一個表示式一個 watcher(使用者watcher)
 * 當資料更新時 watcher 會被觸發,訪問 this.computedProperty 時也會觸發 watcher
 */
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 = function() { return this.xx }
      // 在 this.get 中執行 this.getter 時會觸發依賴收集
      // 待後續 this.xx 更新時就會觸發響應式
      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()
  }

  /**
   * 執行 this.getter,並重新收集依賴
   * this.getter 是例項化 watcher 時傳遞的第二個引數,一個函式或者字串,比如:updateComponent 或者 parsePath 返回的讀取 this.xx 屬性值的函式
   * 為什麼要重新收集依賴?
   *   因為觸發更新說明有響應式資料被更新了,但是被更新的資料雖然已經經過 observe 觀察了,但是卻沒有進行依賴收集,
   *   所以,在更新頁面時,會重新執行一次 render 函式,執行期間會觸發讀取操作,這時候進行依賴收集
   */
  get () {
    // 開啟 Dep.target,Dep.target = this
    pushTarget(this)
    // value 為回撥函式執行的結果
    let value
    const vm = this.vm
    try {
      // 執行回撥函式,比如 updateComponent,進入 patch 階段
      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)
      }
      // 關閉 Dep.target,Dep.target = null
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   * 兩件事:
   *   1、新增 dep 給自己(watcher)
   *   2、新增自己(watcher)到 dep
   */
  addDep (dep: Dep) {
    // 判重,如果 dep 已經存在則不重複新增
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // 快取 dep.id,用於判重
      this.newDepIds.add(id)
      // 新增 dep
      this.newDeps.push(dep)
      // 避免在 dep 中重複新增 watcher,this.depIds 的設定在 cleanupDeps 方法中
      if (!this.depIds.has(id)) {
        // 新增 watcher 自己到 dep
        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
  }

  /**
   * 根據 watcher 配置項,決定接下來怎麼走,一般是 queueWatcher
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // 懶執行時走這裡,比如 computed

      // 將 dirty 置為 true,可以讓 computedGetter 執行時重新計算 computed 回撥函式的執行結果
      this.dirty = true
    } else if (this.sync) {
      // 同步執行,在使用 vm.$watch 或者 watch 選項時可以傳一個 sync 選項,
      // 當為 true 時在資料更新時該 watcher 就不走非同步更新佇列,直接執行 this.run 
      // 方法進行更新
      // 這個屬性在官方文件中沒有出現
      this.run()
    } else {
      // 更新時一般都這裡,將 watcher 放入 watcher 佇列
      queueWatcher(this)
    }
  }

  /**
   * 由 重新整理佇列函式 flushSchedulerQueue 呼叫,完成如下幾件事:
   *   1、執行例項化 watcher 傳遞的第二個引數,updateComponent 或者 獲取 this.xx 的一個函式(parsePath 返回的函式)
   *   2、更新舊值為新值
   *   3、執行例項化 watcher 時傳遞的第三個引數,比如使用者 watcher 的回撥函式
   */
  run () {
    if (this.active) {
      // 呼叫 this.get 方法
      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
      ) {
        // 更新舊值為新值
        const oldValue = this.value
        this.value = value

        if (this.user) {
          // 如果是使用者 watcher,則執行使用者傳遞的第三個引數 —— 回撥函式,引數為 val 和 oldVal
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          // 渲染 watcher,this.cb = noop,一個空函式
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * 懶執行的 watcher 會呼叫該方法
   *   比如:computed,在獲取 vm.computedProperty 的值時會呼叫該方法
   * 然後執行 this.get,即 watcher 的回撥函式,得到返回值
   * this.dirty 被置為 false,作用是頁面在本次渲染中只會一次 computed.key 的回撥函式,
   *   這也是大家常說的 computed 和 methods 區別之一是 computed 有快取的原理所在
   * 而頁面更新後會 this.dirty 會被重新置為 true,這一步是在 this.update 方法中完成的
   */
  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
    }
  }
}

總結

面試官 問:Vue 響應式原理是怎麼實現的?

  • 響應式的核心是通過 Object.defineProperty 攔截對資料的訪問和設定

  • 響應式的資料分為兩類:

    • 物件,迴圈遍歷物件的所有屬性,為每個屬性設定 getter、setter,以達到攔截訪問和設定的目的,如果屬性值依舊為物件,則遞迴為屬性值上的每個 key 設定 getter、setter

      • 訪問資料時(obj.key)進行依賴收集,在 dep 中儲存相關的 watcher

      • 設定資料時由 dep 通知相關的 watcher 去更新

    • 陣列,增強陣列的那 7 個可以更改自身的原型方法,然後攔截對這些方法的操作

      • 新增新資料時進行響應式處理,然後由 dep 通知 watcher 去更新

      • 刪除資料時,也要由 dep 通知 watcher 去更新

面試官 問:methods、computed 和 watch 有什麼區別?

<!DOCTYPE html>
<html lang="en">

<head>
  <title>methods、computed、watch 有什麼區別</title>
</head>

<body>
  <div id="app">
    <!-- methods -->
    <div>{{ returnMsg() }}</div>
    <div>{{ returnMsg() }}</div>
    <!-- computed -->
    <div>{{ getMsg }}</div>
    <div>{{ getMsg }}</div>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    new Vue({
    el: '#app',
    data: {
      msg: 'test'
    },
    mounted() {
      setTimeout(() => {
        this.msg = 'msg is changed'
      }, 1000)
    },
    methods: {
      returnMsg() {
        console.log('methods: returnMsg')
        return this.msg
      }
    },
    computed: {
      getMsg() {
        console.log('computed: getMsg')
        return this.msg + ' hello computed'
      }
    },
    watch: {
      msg: function(val, oldVal) {
        console.log('watch: msg')
        new Promise(resolve => {
          setTimeout(() => {
            this.msg = 'msg is changed by watch'
          }, 1000)
        })
      }
    }
  })
  </script>
</body>

</html>

點選檢視動圖演示,動圖地址:https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9c957654bb484ae7ba4ace1b912cff03~tplv-k3u1fbpfcp-watermark.awebp

示例其實就是答案了

  • 使用場景

    • methods 一般用於封裝一些較為複雜的處理邏輯(同步、非同步)

    • computed 一般用於封裝一些簡單的同步邏輯,將經過處理的資料返回,然後顯示在模版中,以減輕模版的重量

    • watch 一般用於當需要在資料變化時執行非同步或開銷較大的操作

  • 區別

    • methods VS computed

      通過示例會發現,如果在一次渲染中,有多個地方使用了同一個 methods 或 computed 屬性,methods 會被執行多次,而 computed 的回撥函式則只會被執行一次。

      通過閱讀原始碼我們知道,在一次渲染中,多次訪問 computedProperty,只會在第一次執行 computed 屬性的回撥函式,後續的其它訪問,則直接使用第一次的執行結果(watcher.value),而這一切的實現原理則是通過對 watcher.dirty 屬性的控制實現的。而 methods,每一次的訪問則是簡單的方法呼叫(this.xxMethods)。

    • computed VS watch

      通過閱讀原始碼我們知道,computed 和 watch 的本質是一樣的,內部都是通過 Watcher 來實現的,其實沒什麼區別,非要說區別的化就兩點:1、使用場景上的區別,2、computed 預設是懶執行的,切不可更改。

    • methods VS watch

      methods 和 watch 之間其實沒什麼可比的,完全是兩個東西,不過在使用上可以把 watch 中一些邏輯抽到 methods 中,提高程式碼的可讀性。

連結

感謝各位的:點贊收藏評論,我們下期見。


當學習成為了習慣,知識也就變成了常識。 感謝各位的 點贊收藏評論

新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn

文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。

相關文章