原始碼學習VUE之Observe

寫意風流發表於2018-09-12

在文章 原始碼學習VUE之響應式原理我們大概描述了響應式的實現流程,主要寫了observe,dep和wather的簡易實現,以及推導思路。但相應程式碼邏輯並不完善,今天我們再來填之前的一些坑。

Observe

之前實現的observe函式只能處理一個物件的單個屬性,但我們更多的資料是儲存在物件中,為了抽象話,我們也封裝一個物件Observe,只要傳進一個引數,就可以把這個物件進行監聽。

對現有所有屬性進行監聽

var obj = {
    a: 1,
    b: 2
}

比如一個物件有兩個屬性 a,b。我們可以嘗試寫出下面的實現類

class Observe{
    constructor(value){
        this.value = value //要監聽的值。
        this.walk();
    }
    
    walk(){ //通過walk函式,依次處理
        const keys = Object.keys(obj);
        let self = this;
        for (let i = 0; i < keys.length; i++) {
          self.defineReactive(obj, keys[i])
        }
    }
    
    defineReactive (data, key, val) {
        var dep = new Dep();
        Object.defineProperty(obj, a, {
            enumerable: true,
            configurable: true,
            get: function(){
               if(Dep.target){
                    dep.addSub(Dep.target); // Dep.target是Watcher的例項
                }
            },
            set: function(newVal){
                if(val === newVal) return
                val = newVal;
                dep.notify();
            }
        })
    }
}

當然,為了防止重複監聽,我們可以給原object設定一個識別符號以作辨別。

class Obsever(){
    construct(){
        this.value = value //要監聽的值。
        Object.defineProperty(value, "__ob__", {
            value: this,
            enumerable: false,
            writable: true,
            configurable: true
        })
        this.walk();
    }
}

監聽陣列

雖然陣列也是一個物件,但是我們隊陣列的操作卻不會觸發set,get方法。因此必須對陣列特殊處理。
首先需要對運算元組的方法進行改寫,如push,pop,shift

//首先拿到Array的原生原型鏈
const arrayProto = Arrary.prototype;
//為了保證修改不會影響原生方法,我們建立一個新物件
const arrayMethods = Object.create(arrayProto);
//要改寫的方法
const methodsToPatch = [`push`,`pop`,`shift`,`unshift`,`splice`,`sort`,`reverse`]
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method] // 先拿到原生方法
  def(arrayMethods, method, function mutator (...args) {
  // 改寫後的方法,都是先拿到原生方法的計算結果
    const result = original.apply(this, args)
    const ob = this.__ob__
    // 拿到插入的值。
    let inserted
    switch (method) {
      case `push`:
      case `unshift`:
        inserted = args
        break
      case `splice`:
        inserted = args.slice(2)
        break
    }
    //Observe插入的值
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

其實邏輯很簡單。對於可以改變array的方法我們都改寫一下。只要呼叫了這些方法,除了返回正確的值,我們都通知觀察物件,資料改變了,觸發觀察者update操作。同時,陣列裡面可能是個物件,我們不改變陣列本身,但是改變陣列裡面的某個值,這也算是一種改變,因此,除了監聽陣列本身的改變,也要對陣列每個值進行observe。
這涉及到兩點,一是observe Array的時候,就要對每個值進行Observe。另外,插入陣列的每個值也要observe.第二點就是上面程式碼中特別關注push,unshift,splice這三個可以插值方法的原因。

class Obsever(){
    construct(){
        this.value = value //要監聽的值。
        Object.defineProperty(value, "__ob__", {
            value: this,
            enumerable: false,
            writable: true,
            configurable: true
        })
        if(Array.isArray(value)){
            this.observeArray();
        }else{
             this.walk();
        }
    }
    observeArray(items){
       for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        } 
    }
},

function observe (value) {
  let ob
  if (hasOwn(value, `__ob__`) && value.__ob__ instanceof Observer) {
  // 如果已經observe的物件就不再進行重複的observe操作
    ob = value.__ob__
  } else {
        ob = new Observer(value)
  }
  return ob
}

優化

實際開發中我們經常會遇到一個很大的資料。如渲染tables時,table的資料很可能很大(一個多多維陣列)。如果都進行observe無意會是很大的開銷。關鍵是我們只是需要拿這些資料來渲染,並不關心資料內部的變化。因此可能就存在這種需求,可以不對array或object深層遍歷observe。我們可以使用Object.freeze()將這個資料凍結起來。
因此對於凍結的資料我們就不再進行observe。上面的程式碼可以這麼優化

function observe (value) {
  let ob
  if (hasOwn(value, `__ob__`) && value.__ob__ instanceof Observer) {
  // 如果已經observe的物件就不再進行重複的observe操作
    ob = value.__ob__
  } else if(Object.isExtensible(value)){// 如果資料被凍結,或者不可擴充套件,則不進行observe操作
        ob = new Observer(value)
  }
  return ob
}

 defineReactive (data, key, val) {
        var dep = new Dep();
        var property = Object.getOwnPropertyDescriptor(obj, key)
        // 如果資料被凍結,或者不可擴充套件,則改寫set,get方法
          if (property && property.configurable === false) {
            return
          }
          //傳進來的物件可能之前已經被定義了set,get方法,因此我們不能直接拿value
        var getter = property && property.get
        var setter = property && property.set
        Object.defineProperty(obj, a, {
            enumerable: true,
            configurable: true,
            get: function(){
               var value = getter ? getter.call(obj) : val;
               if(Dep.target){
                    dep.addSub(Dep.target); // Dep.target是Watcher的例項
                }
                return value
            },
            set: function(newVal){
                if(val === newVal) return
                val = newVal;
                dep.notify();
            }
        })
    }

相關文章