Vue 2.x原始碼學習:資料響應式改造

beckyyyy發表於2022-11-23

眾所周知,Vue是以資料驅動檢視展示的,即Vue會監聽資料的變化,從而自動重新渲染頁面的結構。

Vue主要透過三步走來實現這個功能:

第一步是對資料進行響應式改造,即對資料的讀寫操作進行劫持;

第二步是對模板依賴的資料進行收集;

第三步是在資料發生變化時,觸發元件更新。

資料響應式改造

0. defineReactive

對資料進行響應式改造的核心程式碼

// core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // 當前`Dep.target`不為空時,通常指向一個`watcher`例項
        dep.depend() // 屬性被收集到當前`watcher`例項的依賴陣列中
        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()
    }
  })
}

透過Object.defineProperty修改物件屬性的屬性描述符descriptor,來實現劫持物件屬性的讀寫操作。

前置知識,物件的屬性分為data型和accessor型。

data型的屬性描述符包含value和writable;accessor型的屬性描述符包含getter和setter函式(兩者至少存在一個)。

由上述程式碼可以看出,所有屬性被處理成了accessor型屬性,即透過getter和setter來完成讀寫,比如當我們讀取person物件上的屬性name,實際得到的是name的屬性訪問符中的getter函式執行後的返回值。

上述的響應式改造中,每個屬性會對應一個dep例項:const dep = new Dep(),假如屬性值val是物件或陣列,會被列入觀察物件,他的屬性會被遞迴進行響應式改造let childOb = !shallow && observe(val)

get函式被用於收集依賴

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

該函式在物件屬性被訪問時會執行,如果Dep.target不為空,即當下有一個監聽器watcher在收集依賴,就進行依賴的收集,dep例項會被收集到該watcher的依賴陣列newDeps中,同時dep也會將此watcher記錄到自己的subs訂閱陣列中,記錄有誰訂閱了自己的變更。

如果childOb不為空(即屬性值val為陣列或物件,且可擴充套件),就對val的__ob__屬性也進行收集操作。

如果value是陣列,對陣列中的物件元素也進行依賴收集。

就是一層層的遞迴收集。

set函式被用於通知變更:

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

如果屬性的新值是屬性或物件,就更新childOb

完成屬性賦值操作後,呼叫dep.notify(),通知所有訂閱了自己的watcher例項執行update操作,即下面程式碼中的for迴圈操作。

// core/observer/dep.js
export default class Dep {
  // ...

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

如果是同步this.sync的watcher會立即被執行,否則會插入到watcher佇列queueWatcher(this)排隊等待執行:

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

1. initInjections

// core/instance/inject.js
export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

透過執行resolveInject解析inject中的資料,解析結果賦值給result。result包含inject中所有的key,如果上級元件中沒有對應inject資料的provide,就賦預設值,簡單來說大致就是result[key] = inject[key] || default

再呼叫defineReactive(vm, key, result[key])將這些key加到vm例項上,即inject中的資料也會進行響應式處理。

假設存在一個inject:["person"],如果person的值是個物件,它是一個被觀察物件,當前子元件的watcher會對該物件的__ob__屬性依賴收集,在上級元件中更改了原始person的某個屬性,就會觸發子元件的更新。

2. initProps

// core/instance/state.js
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)
}

propsOptions,接收的是vm.$options.props,是宣告接收的props的配置,;vm.$options.propsData是實際接收到的props資料。

呼叫defineReactive(props, key, result[key])將propsOptions上的key加到props物件上,即vm._props上,進行響應式處理,如果是在vm上不存在的key,透過proxy(vm, '_props', key)操作,使得可以透過vm直接訪問到_props的屬性,而不需要透過_props物件來訪問。

3. initData

// core/instance/state.js
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選項會被掛在vm._data上,從上述程式碼中可以看出,data必須是一個物件,或者返回值為物件的函式。

透過proxy(vm, '_data', key)操作,vm可以直接訪問到_data的屬性,而不需要透過_data物件來訪問。

最後透過observe(data, true /* asRootData */)來對資料做響應式改造,可以看到這個observe方法多傳了一個引數值為true,標記當前處理的資料是$options.data物件。

observe方法實際是建立一個新的ob例項,資料的__ob__屬性相信在控制檯列印過vue中資料的同學都不陌生,都是指向ob例項。

// core/observe/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
}
// core/observe/index.js
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])
    }
  }
}

從上面Observer的建構函式中可以看出,建立ob例項後,這個例項就掛載資料的__ob__屬性上了,因為在iniDats時傳遞給建構函式的引數是個物件,所以會呼叫walk方法,繼續看walk方法的定義,可以看出,是把這個物件的屬性逐個取出,呼叫defineReactive(obj, keys[i])進行響應式改造。

4. initComputed

// core/instance/state.js
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)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}

獲得每個計算屬性對應watcher的初始值

從上述程式碼中可以看出,會遍歷computed所有的屬性,每個屬性對應配置一個watcher例項,watcher例項在建立時,會呼叫每個computed對應的getter獲取一遍初始值,放在watcher例項的value屬性上

// core/observer/watcher.js
export default class Watcher {
  // ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // ...
    this.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
  }

  // ...
}
// core/observer/dep.js
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

可以看到,執行watcher例項的get()方法時,會進行一個pushTarget(this)的操作,此操作修改了Dep.target,使它指向了當前的watcher例項,如果某個computed屬性依賴了data中的某個屬性,需要讀取data中的某個屬性值,就會觸發該data屬性的getter函式,使得該data屬性被收集到當前watcher例項的依賴陣列中。

完成computed屬性的取值後,執行popTarget(),即下面的程式碼:

// core/observer/dep.js
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

會使Dep.target指回上一個watcher例項。

最後清理依賴this.cleanupDeps(),將不再關聯的依賴dep其訂閱陣列中對應的watcher移除,將newDeps賦值給deps並清空newDeps,代表該watcher例項一次依賴收集完畢。

計算屬性被讀取時,其對應watcher依賴的資料會被當前watcher收集為自身的依賴

如果computed某個屬性的識別符號不在vm例項上,就繼續執行defineComputed(vm, key, userDef),會將給vm例項新增一個名為key的屬性,該屬性的getter函式由下述程式碼定義:

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

即在這個計算屬性被讀取時,會拿到它所對應的watcher例項,如果當前Dep.target不為null時,watcher會執行例項方法depend()

export default class Watcher {
  // ...

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

  // ...
}

可以看到此watcher例項的依賴deps會被一一取出,執行dep例項的depend方法:

// core/observerdep.js
depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

即此watcher例項的依賴都會被收集到當前Dep.target指向的watcher例項的依賴陣列中。

5. initWatch

// core/instance/state.js
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) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

initWatch的內容比較簡單,就是透過呼叫createWatcher(vm, key, handler),一一對應生成watcher例項,並且給watcher例項標記options.user = true,代表這個watcher是使用者配置的。

每個watch通常是有一個對應的表示式(通常是vm的data資料)和一個對應的回撥函式,使用場景通常是當vm中的某些資料發生改變時,使用者需要做一些自定義的操作來做處理。

與computed中生成對應watcher例項類似,watcher例項在建立時,每個watch對應的表示式就會被求值一遍,即vm例項上的某些資料屬性被讀取,這些屬性對應的dep會被收集到該watcher例項的依賴陣列中,求得的值會放在watcher例項的value屬性上,如果某個watch配置了immediate,就立即執行一遍watch對應的回撥函式,入參為watchervalue屬性值。

可以看到在執行回撥函式前,執行了一個pushTarget(),此時Dep.target會指向空,所以在回撥函式執行過程中,如果vm的某些資料屬性被訪問,這些屬性不會被收集依賴,因為屬性的getter函式中在屬性被收集依賴前有個對Dep.target的判空檢查。

6. 一些說明

__ob__是給物件加的屬性,指向observer例項,ob和物件是一對一,代表這個物件被觀察,該物件的屬性的讀寫操作會被做響應式處理,即被劫持。

dep是給屬性配置的用於依賴收集的,通常物件的某個屬性與dep是一對一,可以被多個watcher收集,即多個watcher例項在監聽這個屬性的變化;__ob__是物件的特殊屬性,它也有自己的dep,可以被watcher收集。

一個watcher例項只會關聯一個vm,一個vm例項可以關聯多個watcher,watcher例項會放在vm._watchers陣列中;渲染watcher還會放在vm._watcher上,渲染watcher從字面上理解就是與元件渲染有關的watcher

相關文章