1. 程式人生 > >Vue原始碼解讀之響應式原理

Vue原始碼解讀之響應式原理

原文地址:https://banggan.github.io/2019/01/12/Vue原始碼解讀之響應式原理/
相信用過Vue的基本上都知道Vue的響應式都是利用了Object.defineProperty。
當你把一個普通的 JavaScript 物件傳給 Vue 例項的 data 選項,Vue 將遍歷此物件所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是為什麼 Vue 不支援 IE8 以及更低版本瀏覽器。
這些 getter/setter 對使用者來說是不可見的,但是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化。
每個元件例項都有相應的 watcher 例項物件,它會在元件渲染的過程中把屬性記錄為依賴,之後當依賴項的 setter 被呼叫時,會通知 watcher 重新計算,從而致使它關聯的元件得以更新。
在這裡插入圖片描述

Object.defineProperty

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

Object.defineProperty(obj, prop, descriptor)

obj 是要在其上定義屬性的物件;prop 是要定義或修改的屬性的名稱;descriptor 是將被定義或修改的屬性描述符。
這裡我們最關心的是 get 和 set,get 是一個給屬性提供的 getter 方法,當我們訪問了該屬性的時候會觸發 getter 方法;set 是一個給屬性提供的 setter 方法,當我們對該屬性做修改的時候會觸發 setter 方法。
一旦物件擁有了 getter 和 setter,我們可以簡單地把這個物件稱為響應式物件。

observe

observe 的功能就是用來監測資料的變化

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&   //  陣列 物件  還是可擴充套件的
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)          //滿足之後呼叫Observer
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

Observer 是一個類,它的作用是給物件的屬性新增 getter 和 setter,用於依賴收集和派發更新:

/**
 * 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
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {        //遍歷物件的所有屬性  呼叫defineProperty的方法   遍歷keys
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineProperty(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 的建構函式邏輯很簡單,首先例項化 Dep 物件,接著通過執行 def 函式把自身例項新增到資料物件 value 的 ob 屬性上,def 的定義在 src/core/util/lang.js 中:

//    對defineProperty的一個封裝
    export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,     //  不可列舉的    傳入的是undefined
        writable: true,
        configurable: true
      })
    }

def 函式是一個非常簡單的Object.defineProperty 的封裝,這就是為什麼在開發中輸出 data 上物件型別的資料,會發現該物件多了一個 ob 的屬性。

回到 Observer 的建構函式,接下來會對 value 做判斷,對於陣列會呼叫 observeArray 方法,否則對純物件呼叫 walk 方法。可以看到 observeArray 是遍歷陣列再次呼叫 observe 方法,而 walk 方法是遍歷物件的 key 呼叫 defineReactive 方法

defineReactive

defineReactive 的功能就是定義一個響應式物件,給物件動態新增 getter 和 sette

/**
 * 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) {               // false就什麼都不做
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {     //  walk的時候   對key求值賦給val
    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()
    }
  })
}

defineReactive 函式最開始初始化 Dep 物件的例項,接著拿到 obj 的屬性描述符,然後對子物件遞迴呼叫 observe 方法,這樣就保證了無論 obj 的結構多複雜,它的所有子屬性也能變成響應式的物件,這樣我們訪問或修改 obj 中一個巢狀較深的屬性,也能觸發 getter 和 setter。最後利用 Object.defineProperty 去給 obj 的屬性 key 新增 getter 和 setter。

總結

響應式物件,核心就是利用 Object.defineProperty 給資料添加了 getter 和 setter,目的就是為了在我們訪問資料以及寫資料的時候能自動執行一些邏輯:getter 做的事情是依賴收集,setter 做的事情是派發更新。後面會根據依賴收集進行一個解讀。