前端筆記 – vue2.x data初始化以及Observer

superior發表於2019-02-26

前提:已經clone下來vue原始碼

vue的data屬性是在實際使用中最常見的了,以及大家老生常談的雙向繫結。本篇文章介紹了data屬性的初始化以及雙向繫結中model繫結部分。

先概述一下整體的邏輯過程:

1、初始化vue例項

2、在例項created生命週期之前執行initState函式,這個函式對props、methods、data、computed、watch初始化

3、在data初始化中,vue會對data繫結一個觀察者Observer,每次data中的欄位更新後都會通知依賴收集器Dep觸發響應式更新


接下來說正題:data是在new Vue中初始化的,看一下具體路徑

1、new Vue初始化位置:/src/core/instance/index.js中的initMixin函式中

2、initMixin函式在/src/core/instance/state.js中呼叫了同檔案中的initState函式

3、在initState函式中首先判斷了使用者是否已經配置了data屬性,如下圖

前端筆記 – vue2.x data初始化以及Observer

4、我們直接看一下initData函式

前端筆記 – vue2.x data初始化以及Observer

在我們使用vue的時候data可以有兩種方式定義,一種是用函式返回一個物件,另一種是直接定義一個物件。

1、在initData函式中,首先判斷了data型別是否為函式,如果為函式的話則呼叫getData函式只是.call一下函式返回物件,將物件首先複製到例項的_data上。

前端筆記 – vue2.x data初始化以及Observer

2、這裡isPlainObject函式是通過呼叫data物件的toString函式判斷是否為Object,它判斷了_toString.call(obj) === `[object Object]`

前端筆記 – vue2.x data初始化以及Observer

3、keys是data中的所有欄位、props是例項中的props、methods是例項中的methodes,while開始對data中每個key進行遍歷,首先判斷methods中是否有重名的key,然後再判斷props中是否有重名的key。這裡還會通過isReserved函式判斷是用來判斷是否用到了保留關鍵字元“_”和“$”

前端筆記 – vue2.x data初始化以及Observer

4、在key字串中不含有關鍵字元,接下來會呼叫proxy函式,這個函式的工作就是設定好key的get、set屬性並配置到vue例項上。這裡我們看到proxy中vue.key的get、set方法實際獲取、改變的都是vue._data.key。

前端筆記 – vue2.x data初始化以及Observer

雙向繫結之Observer

接下來就是重頭戲了,vue雙向繫結中其中之一model繫結,接上文的原始碼最後一行回撥用observe函式開始model繫結。在開始繼續上文之前我們先看一下Observe都做了什麼

前端筆記 – vue2.x data初始化以及Observer

先看看人家官方解釋:觀察者類附加到每個觀察物件。 一旦連線,觀察者將目標物件的屬性鍵轉換為收集依賴關係和分派更新的getter / setter。說一下這個Observer類:

1、constructor中初始化了value(datakey)、dep(依賴收集器)、vmCount(訂閱此觀察者的節點)

2、接下來呼叫了def函式,為data定義__ob__屬性,可以理解為將data繫結了一個observer,__ob__指向了這個oberver

前端筆記 – vue2.x data初始化以及Observer

3、接下來會判斷value是否為陣列,我們先看一下value為物件的情況,它會呼叫this.walk函式:walk函式先獲取物件的所有key,然後遍歷key並執行defineReactive函式。


我們來看看defineReactive函式都幹嘛了(一張圖截不下就直接上原始碼吧)

/**
 * 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()
    }
  })
}
複製程式碼

面試中、各種文章中說的雙向繫結的get、set就在這個函式中實現了,讓我們來解析一下吧

1、new了一個dep依賴收集(後面文章會提起)

2、通過getOwnPropertyDescriptor獲取到data中key對應的訪問器屬性賦值給property,並判斷當前key是否在data中並且是可配置的(configurable為true才可以)

3、獲取訪問器屬性中的get、set屬性,判斷存在setter或不存在getter以及實參各戶只有兩個時將val為data中key對應的value(在walk中就是傳了兩個引數)

4、這裡我們看到了shallow欄位,這裡我理解為對val的深度淺度遍歷(如果為物件的話)繫結一個觀察者。那麼由於walk函式中並沒有傳則預設!shallow為ture。observe後面我們在討論

5、接下來就到了萬人皆知的get、set了,先說get屬性,我看到了它會先判斷是否已經設定get屬性(第3條中獲取),如果有則call getter函式,如果沒有則直接返回val值。接下來會判斷Dep的target(它的作用後面文章會搭配watcher詳細說),這裡先理解為每次get的時候都會訂閱到Dep的subs中,包括子元素、如果為陣列的話則陣列中每個子元素都會訂閱到Dep的subs中。

6、再看看set屬性,首先通過getter來獲取到value,然後判斷如果新的value與舊值===全等、新value與舊value的getter方法被重寫過(每次get的value不一致)則不做任何事情,這也就是當我們this.sthKey = oldValue重複賦值相同,不會觸發響應式的原因。

7、接下來判斷了一下是否有使用者自定義的setter,如果有則setter.call(),如果沒有的話則是常規賦值操作。

8、這裡我們看到它再次對新的value進行了一次深度遍歷為每個子元素註冊一個觀察者。

9、最後就是dep.notify()了,這個函式就是在data中的資料set之後觀察者告訴訂閱者要更新了完成model to view的響應式操作。

10、到這裡就將data中每個key的get、set屬性進行重寫結束了,雙向繫結也完成了一半。

11、我們回頭看一下如果是陣列的話,vue是如何做響應式的呢?首先判斷了hasProto這個是一個布林型它返回了__proto__是否在物件中

前端筆記 – vue2.x data初始化以及Observer

這裡augment會根據hasProto賦值上面兩個不同的函式,protoAugemnt很簡單就是將value的__proto__賦值arrayMethods(arrayMethods為Array.prototype),copyAugment則遍歷arrayKeys(arrayKeys為arrayMethods的OwnPropertyNames)每一個key,呼叫def函式,為data定義key屬性(也就是定義Array.prototype)。

接下來就是遍歷整個陣列,對每個子元素呼叫observe函式。


我們來看看observe函式的實現。

前端筆記 – vue2.x data初始化以及Observer

1、首先函式判斷了value必須為物件且不為VNode節點

2、初始化一個Observer物件,判斷value是否含有__ob__屬性且該屬性是Observer物件,如果均滿足則將初始化的Observer物件指向了value的__ob__屬性,其次判斷shouldObserve(常量預設為true)、是否為服務端渲染、value為陣列或為物件、value物件是否可擴充套件(isExtensible es6語法)、是否為vue例項,經過以上的判斷後ob又指向了新Observer物件(由value初始化)

3、判斷當前的asRootData當前的value是否為根結點的data,以及ob物件存在,則ob訂閱此觀察者的節點加一,最終返回ob物件。

以上就是筆者的理解,接下來會盡量將vue的各個模組都遍歷到,如有任何建議歡迎提出。

相關文章