1. 程式人生 > 其它 >Vue響應式系統的核心-----變化偵測(Object)

Vue響應式系統的核心-----變化偵測(Object)

什麼是變化偵測

變化偵測的作用是偵測資料的變化,當資料變化時,會通知檢視進行相應的更新

在執行時應用內部的狀態會不斷髮生變化,此時需要不停地重新渲染。這時如何確定狀態中發生了什麼變化?變化偵測主要用來解決這個問題。

Vue的變化偵測屬。當狀態發生變化時,Vue立刻知道了,在一定程度上知道哪些狀態變了。

有一個狀態繫結多個依賴,每個依賴表示一個具體的DOM節點,當狀態變化了,向這個狀態的所有依賴傳送通知,讓他們進行DOM更新操作。每個狀態繫結的依賴越多,依賴追蹤在記憶體上的開銷就會越大。

從vue.js 2.0開始引入了虛擬DOM,一個狀態所繫結的依賴不再是具體的DOM節點,而是一個元件。當狀態變化後,會通知到元件,元件內部在使用虛擬DOM進行比對。這樣可以大大降低依賴數量,從而降低依賴追蹤所消耗的記憶體。

Object的變化偵測

  • Data通過Observer轉換成了getter/setter的形式來追蹤變化

  • 當外界通過watcher讀取資料時,會觸發getter從而將watcher新增到依賴中

  • 當資料發生變化時,會觸發setter,從而向Dep中的依賴傳送通知。

  • watcher接收到通知後,會向外界傳送通知,變化通知到外界後可能會觸發檢視更新,也有可能觸發使用者的某個回撥函式等。

如何追蹤變化

在vue中如何偵測一個物件的變化?使用Object.defineProperty和ES6的Proxy

 function defineReactive(data,key,val){
        Object.defineProperty(data,key,{
            enumerable: true,
            configuration: true,
            get: function(){
                return val
            },
            set: function(newVal){
                if(val === newVal){
                    return
                }
                val = newVal
            }
        })
    }

每當從data的key中讀取資料時,get函式被觸發;每當往data的key中設定資料時,set函式被觸發。

如何收集依賴

觀察資料目的是當資料的屬性發生變化時,可以通知那些曾經使用該資料的地方。

  <template>
         <h1>{{name}}</h1>
    </template>

該模板使用了資料name,所以當它發生變化時,要向使用了它的地方傳送通知。

收集依賴:把用到資料name的地方收集起來,然後等屬性發生變化時,把之前收集好的依賴迴圈觸發一遍。

依賴收集在哪裡

在getter中收集依賴,在setter中觸發依賴

dep類專門管理依賴,使用這個類可以收集依賴、刪除依賴或向依賴傳送通知等。

    export default class Dep {
      constructor () {
        this.subs = []
      }
    
      addSub (sub) {
        this.subs.push(sub)
      }
      // 刪除一個依賴
      removeSub (sub) {
        remove(this.subs, sub)
      }
      // 新增一個依賴
      depend () {
        if (window.target) {
          this.addSub(window.target)
        }
      }
      // 通知所有依賴更新
      notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    function defineReactive (obj,key,val) {
      const dep = new Dep()  //例項化一個依賴管理器,生成一個依賴管理陣列dep
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get(){
          dep.depend()    // 在getter中收集依賴
          return val;
        },
        set(newVal){
          if(val === newVal){
              return
          }
          val = newVal;
          dep.notify()   // 在setter中通知依賴更新
        }
      })
    }

依賴是誰?

當屬性發生變化後,需要通知誰

其實在Vue中還實現了一個叫做Watcher的類,而Watcher類的例項就是我們上面所說的那個"誰"。換句話說就是:誰用到了資料,誰就是依賴,我們就為誰建立一個Watcher例項。在之後資料變化時,我們不直接去通知依賴更新,而是通知依賴對應的Watch例項,由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)
      }
    }
    
    
    /**
     * Parse simple path.
     * 把一個形如'data.a.b.c'的字串路徑所表示的值,從真實的data物件中取出來
     * 例如:
     * data = {a:{b:{c:2}}}
     * parsePath('a.b.c')(data)  // 2
     */
    const bailRE = /[^\w.$]/
    export function parsePath (path) {
      if (bailRE.test(path)) {
        return
      }
      const segments = path.split('.')
      return function (obj) {
        for (let i = 0; i < segments.length; i++) {
          if (!obj) return
          obj = obj[segments[i]]
        }
        return obj
      }
    }
    

遞迴偵測所有key

前面的程式碼只能偵測資料中的某一個屬性,我們希望把資料中的所有屬性都偵測到,所以需要封裝一個Observer類。這個類的作用將一個數據內的所有屬性都轉換成getter/setter的形式,然後再追蹤它們的變化。

 function Observer (value) {
        this.value = value;
        if (!Array.isArray(value)) {
          this.walk(value);
        }
      };
      Observer.prototype.walk = function walk (obj) {
        var keys = Object.keys(obj);
        for (var i = 0; i < keys.length; i++) {
          defineReactive$$1(obj, keys[i],obj[keys[i]]);
        }
      };

    function defineReactive (obj,key,val) {
       //新增,遞迴子屬性
        if(typeof val === 'object'){
            new Observer(val);
        }
      const dep = new Dep()  //例項化一個依賴管理器,生成一個依賴管理陣列dep
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get(){
          dep.depend()    // 在getter中收集依賴
          return val;
        },
        set(newVal){
          if(val === newVal){
              return
          }
          val = newVal;
          dep.notify()   // 在setter中通知依賴更新
        }
      })
    }

不足之處

雖然我們通過Object.defineProperty方法實現了對object資料的可觀測,但是這個方法僅僅只能觀測到object資料的取值及設定值,當我們向object資料裡新增一對新的key/value或刪除一對已有的key/value時,它是無法觀測到的,導致當我們對object資料新增或刪除值時,無法通知依賴,無法驅動檢視進行響應式更新。

當然,Vue也注意到了這一點,為了解決這一問題,Vue增加了兩個全域性API:Vue.set和Vue.delete。