vue的變化偵測-Object篇| 8月更文挑戰

這是我參與8月更文挑戰的第1天,活動詳情查看:8月更文挑戰vue

vue的變化偵測-Object篇

衆所周知,Vue最大的特色之一就是數據驅動視圖,那什麼是數據驅動視圖?也就是說,當數據發生變化時對應的視圖就要更新。接下來咱們就經過閱讀源碼來看一下Vue內部是怎麼對數據進行變化偵測的。react

首先咱們知道,數據驅動視圖的關鍵點在於咱們怎麼知道數據發生了變化,只要知道數據發生了變化,那麼在數據發生變化時去通知對應的視圖更新便可。要想知道數據何時被讀取了或數據何時被改寫了,其實不難,JavaScript爲咱們提供了Object.defineProperty方法,經過該方法咱們就能夠輕鬆的知道數據在何時發生變化。git

使Object數據變得可觀測

首先咱們來看一個例子:github

let car = {}
let val = 3000
Object.defineProperty(car, 'price', {
  enumerable: true,
  configurable: true,
  get(){
    console.log('price屬性被讀取了')
    return val
  },
  set(newVal){
    console.log('price屬性被修改了')
    val = newVal
  }
})
複製代碼

上面的例子中,咱們經過Object.defineProperty方法給car這個對象聲明瞭一個屬性price,並經過改寫它的get()、set()方法對這個屬性的讀寫進行了攔截,因此在咱們讀取或者修改這個price屬性的時候,就會觸發get和set方法,這樣這個price就變成了可偵測的了。vue內部也是經過攔截屬性的get和set方法來監聽數據的變化的。數組

下面咱們看一下vue的源碼是怎麼實現數據的監聽的:markdown

源碼位於 src/core/observer/index.jsasync

// 如下代碼是簡化版的,想看完整的能夠到gitHub上克隆一下vue完整的源碼 https://github.com/vuejs/vue 
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    /** * 給value新增一個 __ob__屬性,值爲該value的Observe實例 * 至關於爲value打上標記,表示它已經被轉化成響應式了,避免重複操做 */
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      //這裏是數組的變化偵測
    } else {
      this.walk(value)
    }
  }

  /** * 遍歷全部的屬性 將他們變成 可偵測的 */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
  const dep = new Dep() // 先實例化一個依賴管理器,生成一個依賴管理數組dep

  const property = Object.getOwnPropertyDescriptor(obj, key)
  /** * getOwnPropertyDescriptor() 獲取到的是 屬性描述符 對象 * { configurable: true, enumerable: true, value: 42, writable: true } */
  if (property && property.configurable === false) {
    return
  }

  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() // 在get的時候,收集依賴
        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 (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify() // 在set的時候通知依賴
    }
  })
}

複製代碼

在上面的代碼中,vue定義了一個Observe類,它用來將一個Object對象轉化爲一個可偵測的對象。而且給value增長了一個新的屬性__ob__,這個屬性就是Observe實例,這個屬性至關因而給value打上標記,表示這個值已是一個可監測的了,避免後續的重複操做。函數

而後再調用walk函數遍歷對象中的每個屬性,將每個屬性都經過Object.defineProperty方法變成get/set的形式來監聽屬性變化。post

這樣,咱們只要將一個object對象傳入到Observe中,這個對象就會變成可監測的。ui

依賴收集

第一步咱們經過Observe類將一個對象變成可監測的對象了,如今數據的讀寫變化咱們均可以監測到了,那麼咱們就應該在數據變化時去通知依賴更新了,那什麼是依賴,依賴在哪裏收集的呢?咱們怎麼去通知依賴更新的呢?

簡單來講,依賴就是誰用到了這個數據,那麼它就是那個依賴。咱們能夠爲每個數據建立一個依賴數組,誰用到了這個數據,咱們就把誰放進這個依賴數組裏,當數據變化時,咱們挨個通知這個依賴數組中的每一項去更新便可。

在哪裏收集依賴,哪裏通知依賴?

誰用到了這個數據,那麼當這個數據變化時就通知誰。所謂誰用到了這個數據,其實就是誰獲取了這個數據,而可觀測的數據被獲取時會觸發get方法,那麼咱們就能夠在get中收集這個依賴。一樣,當這個數據變化時會觸發set方法,那麼咱們就能夠在set中通知依賴更新。

依賴管理器

咱們給每一個數據都建一個依賴數組,誰依賴了這個數據咱們就把誰放入這個依賴數組中。因而vue內部建立了一個依賴管理器Dep。

源碼位置:src/core/observer/dep.js

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = [] // 初始化一個subs數組,用來存放依賴
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  /** * 刪除一個依賴 */
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  /** * 添加一個依賴 */
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  /** * 通知依賴更新 */
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
複製代碼

在上面的依賴管理器Dep類中,咱們先初始化了一個subs數組,用來存放依賴,而且定義了幾個實例方法用來對依賴進行添加,刪除,通知等操做。有了依賴管理器後,咱們就能夠在get中收集依賴,在set中通知依賴更新了。

function defineReactive (obj,key,val) {
  if (arguments.length === 2) {
    val = obj[key]
  }
  if(typeof val === 'object'){
    new Observer(val)
  }
  const dep = new Dep()  //實例化一個依賴管理器,生成一個依賴管理數組dep
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get(){
      dep.depend()    // 在get中收集依賴
      return val;
    },
    set(newVal){
      if(val === newVal){
          return
      }
      val = newVal;
      dep.notify()   // 在set中通知依賴更新
    }
  })
}
複製代碼

在上述代碼中,咱們在get中調用了dep.depend()方法收集依賴,在set中調用dep.notify()方法通知全部依賴更新。

依賴究竟是誰?

前面咱們明白了什麼是依賴?什麼時候收集依賴?以及收集的依賴存放到何處?那麼咱們收集的依賴究竟是誰?

其實在Vue中還實現了一個叫作Watcher的類,而Watcher類的實例就是咱們上面所說的那個"誰"。換句話說就是:誰用到了數據,誰就是依賴,咱們就爲誰建立一個Watcher實例。在以後數據變化時,咱們不直接去通知依賴更新,而是通知依賴對應的Watch實例,由Watcher實例去通知真正的視圖。

Watch類源碼位置:src/core/observer/watcher.js

// watcher類簡化版
export default class Watcher {
  constructor (vm,expOrFn,cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn)
    this.value = this.get()
  }
  get () {
    window.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    window.target = undefined;
    return value
  }
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}
複製代碼

誰用到了數據,誰就是依賴,咱們就爲誰建立一個Watcher實例,在建立Watcher實例的過程當中會自動的把本身添加到這個數據對應的依賴管理器中,之後這個Watcher實例就表明這個依賴,當數據變化時,咱們就通知Watcher實例,由Watcher實例再去通知真正的依賴。 那麼,在建立Watcher實例的過程當中它是如何的把本身添加到這個數據對應的依賴管理器中呢? 下面咱們分析Watcher類的代碼實現邏輯:

  1. 當實例化Watcher類時,會先執行其構造函數;
  2. 在構造函數中調用了this.get()實例方法;
  3. get()方法中,首先經過window.target = this把實例自身賦給了全局的一個惟一對象window.target上,而後經過let value = this.getter.call(vm, vm)獲取一下被依賴的數據,獲取被依賴數據的目的是觸發該數據上面的getter,上文咱們說過,在getter裏會調用dep.depend()收集依賴,而在dep.depend()中取到掛載window.target上的值並將其存入依賴數組中,在get()方法最後將window.target釋放掉。
  4. 而當數據變化時,會觸發數據的setter,在setter中調用了dep.notify()方法,在dep.notify()方法中,遍歷全部依賴(即watcher實例),執行依賴的update()方法,也就是Watcher類中的update()實例方法,在update()方法中調用數據變化的更新回調函數,從而更新視圖。

簡單總結一下就是:Watcher先把本身設置到全局惟一的指定位置(window.target),而後讀取數據。由於讀取了數據,因此會觸發這個數據的getter。接着,在getter中就會從全局惟一的那個位置讀取當前正在讀取數據的Watcher,並把這個watcher收集到Dep中去。收集好以後,當數據發生變化時,會向Dep中的每一個Watcher發送通知。

不足之處

雖然咱們經過Object.defineProperty方法實現了對object數據的可觀測,可是這個方法僅僅只能觀測到object數據的取值及設置值,當咱們向object數據裏添加一對新的key/value或刪除一對已有的key/value時,它是沒法觀測到的,致使當咱們對object數據添加或刪除值時,沒法通知依賴,沒法驅動視圖進行響應式更新。爲了解決這個問題,咱們可使用vue.$setvue.$delete

總結

  1. vue中實現了一個Observer類,這個類能夠將一個對象的全部屬性經過Object.defineProperty來攔截屬性的get和set方法對數據變化進行偵測。而後在獲取數據時,即get被觸發時收集依賴,在數據變化時,即set調用時通知依賴更新。

  2. vue實現了一個Dep類,這個類用來管理每個數據的依賴項。

  3. 當數據發生了變化時,會觸發set,從而向Dep中的依賴(即Watcher)發送通知。Watcher接收到通知後,會向外界發送通知,觸發視圖更新。

相關文章
相關標籤/搜索