簡單實現Vue的observer和watcher

非庖丁瞎解牛系列~ =。=javascript

在平常項目開發的時候,咱們將js對象傳給vue實例中的data選項,來做爲其更新視圖的基礎,事實上是vue將會遍歷它的屬性,用Object.defineProperty 設置它們的 get/set,從而讓 data 的屬性可以響應數據變化:vue

Object.defineProperty(obj, name, {
   // 獲取值的時候先置入vm的_data屬性對象中
   get() {
     // 賦值的時候顯示的特性
   },
   set() {
     // 值變化的時候能夠作點什麼
   }
 })

接下來能夠利用其實現一個最簡單的watcher.既然要綁定數據執行回調函數,data屬性和callback屬性是少不了的,咱們定義一個vm對象(vue中vm對象做爲根實例,是全局的):java

/**
 * @param {Object} _data 用於存放data值
 * @param {Object} $data data原始數據對象,當前值
 * @param {Object} callback 回調函數
 */
var vm = { _data: {}, $data: {}, callback: {} }

在設置值的時候,若是檢測到當前值與存儲在_data中的對應值發生變化,則將值更新,並執行回調函數,利用Object.definedProperty方法中的get() & set() 咱們很快就能夠實現這個功能~設計模式

vm.$watch = (obj, func) => {
    // 回調函數
    vm.callback[ obj ] = func
    // 設置data
    Object.defineProperty(vm.$data, obj, {
      // 獲取值的時候先置入vm的_data屬性對象中
      get() {
        return vm._data[ obj ]
      },
      set(val) {
        // 比較原值,不相等則賦值,執行回調
        if (val !== vm._data[ obj ]) {
          vm._data[ obj ] = val
          const cb = vm.callback[ obj ]
          cb.call(vm)
        }
      }
   })
}
vm.$watch('va', () => {console.log('已經成功被監聽啦')})
vm.$data.va = 1

雖然初步實現了這個小功能,那麼問題來了,obj對象若是隻是一個簡單的值爲值類型的變量,那以上代碼徹底能夠知足;可是若是obj是一個具備一層甚至多層樹結構對象變量,咱們就只能監聽到最外層也就是obj自己的變化,內部屬性變化沒法被監聽(沒有設置給對應屬性設置set和get),由於對象自身內部屬性層數未知,理論上能夠無限層(通常不會這麼作),因此此處仍是用遞歸解決吧~瀏覽器

我們先將Object.defineProperty函數剝離,一是解耦,二是方便咱們遞歸~模塊化

var defineReactive = (obj, key) => {
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      if (vm._data[key] === newVal) {
        return
      }
      vm._data[key] = newVal
      const cb = vm.callback[ obj ]
      cb.call(vm)
    }
  })
}

咦,說好的遞歸呢,不着急,上面只是抽離了加get和set功能的函數,
如今咱們加入遞歸~函數

var Observer = (obj) => {
  // 遍歷,讓對象中的每一個屬性能夠加上get set
  Object.keys(obj).forEach((key) =>{
    defineReactive(obj, key)
  })
}

這裏僅僅只是遍歷,要達到遞歸,則須要在defineReactive的時候再加上判斷,判斷這個屬性是否爲object類型,若是是,則執行Observer自身~咱們改寫下defineReactive函數性能

// 判斷是否爲object類型,是就繼續執行自身
var observe = (value) => {
  // 判斷是否爲object類型,是就繼續執行Observer
  if (!value || typeof value !== 'object') {
    return
  }
  return new Observer(value)
}

// 將observe方法置入defineReactive中Object.defineProperty的set中,造成遞歸
var defineReactive = (obj, key) => {
  // 判斷val是否爲對象,若是對象有不少層屬性,則這邊的代碼會不斷調用自身(由於observe又執行了Observer,而Observer執行defineReactive),一直到最後一層,從最後一層開始執行下列代碼,層層返回(能夠理解爲洋蔥模型),直到最前面一層,給全部屬性加上get/set
  var childObj = observe(vm._data[key])
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      // 若是設置的值徹底相等則什麼也不作
      if (vm._data[key] === newVal) {
         return
      }
      // 不相等則賦值
      vm._data[key] = newVal
      // 執行回調
      const cb = vm.callback[ key ]
      cb.call(vm)
      // 若是set進來的值爲複雜類型,再遞歸它,加上set/get
      childObj = observe(val)
    }
  })
}

如今咱們來整理下,把咱們剛開始實現的功能雛形進行進化學習

var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key) => {
  // 一開始的時候是不設值的,因此,要在外面作一套observe
  // 判斷val是否爲對象,若是對象有不少層屬性,則這邊的代碼會不斷調用自身(由於observe又執行了Observer,而Observer執行defineReactive),一直到最後一層,從最後一層開始執行下列代碼,層層返回(能夠理解爲洋蔥模型),直到最前面一層,給全部屬性加上get/set
  var childObj = observe(vm._data[key])
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      if (vm._data[key] === newVal) {
        return
      }
    // 若是值有變化的話,作一些操做
    vm._data[key] = newVal
    // 執行回調
    const cb = vm.callback[ key ]
    cb.call(vm)
    // 若是set進來的值爲複雜類型,再遞歸它,加上set/get
    childObj = observe(newVal)
    }
  })
}
var Observer = (obj) => {
  Object.keys(obj).forEach((key) =>{
    defineReactive(obj, key)
  })
}
var observe = (value) => {
  // 判斷是否爲object類型,是就繼續執行Observer
  if (!value || typeof value !== 'object') {
    return
  }
  Observer(value)
}
vm.$watch = (name, func) => {
  // 回調函數
  vm.callback[name] = func
  // 設置data
  defineReactive(vm.$data, name)
}
// 綁定a,a若變化則執行回調方法
var va = {a:{c: 'c'}, b:{c: 'c'}}
vm._data[va] = {a:{c: 'c'}, b:{c: 'c'}}
vm.$watch('va', () => {console.log('已經成功被監聽啦')})
vm.$data.va = 1

在谷歌瀏覽器的console中粘貼以上代碼,而後回車發現,結果不出所料,va自己被監聽了,能夠,咱們試試va的內部屬性有沒有被監聽,改下vm.$data.va = 1爲vm.$data.va.a = 1,結果發現報錯了優化

什麼鬼?

咱們又仔細檢查了代碼,WTF,原來咱們在遞歸的時候,Object.defineProperty中的回調函數cb的key參數一直在發生變化,咱們但願的是裏面的屬性變化的時候執行的是咱們事先定義好的回調函數~那麼咱們來改下方法,將一開始定義好的回調做爲參數傳進去,確保每一層遞歸set的回調都是咱們事先設置好的~

var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key, cb) => {
  // 一開始的時候是不設值的,因此,要在外面作一套observe
  var childObj = observe(vm._data[key], cb)
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      if (vm._data[key] === newVal) {
        return
      }
      // 若是值有變化的話,作一些操做
      vm._data[key] = newVal
      // 執行回調
      cb()
      // 若是set進來的值爲複雜類型,再遞歸它,加上set/get
      childObj = observe(newVal)
    }
  })
}
var Observer = (obj, cb) => {
  Object.keys(obj).forEach((key) =>{
    defineReactive(obj, key, cb)
  })
}
var observe = (value, cb) => {
  // 判斷是否爲object類型,是就繼續執行Observer
  if (!value || typeof value !== 'object') {
    return
  }
  Observer(value, cb)
}
vm.$watch = (name, func) => {
  // 回調函數
  vm.callback[name] = func
  // 設置data
  defineReactive(vm.$data, name, func)
}
// 綁定a,a若變化則執行回調方法
var va = {a:{c: 'c'}, b:{c: 'c'}}
vm._data.va = {a:{c: 'c'}, b:{c: 'c'}}
vm.$watch('va', () => {console.log('又成功被監聽啦')})
vm.$data.va.a = 1

再執行一次以上代碼,發現內部的a屬性也被監聽到了,並且屬性值變化的時候執行了咱們事先定義好的回調函數~嘻嘻嘻~

雖然實現了$watch的基本功能,可是和vue的源碼仍是有必定的距離,特別是一些扁平化和模塊化的思想須要涉及到一些設計模式,其實咱們在看源碼的時候,經常是逆着做者的思惟走的,功能從簡單到複雜每每涉及到代碼的模塊化和解耦,使得代碼很是地分散,讀起來晦澀難懂,本身動手,從小功能代碼塊實現,而後結合源碼,對比思路,慢慢豐富,也不失爲一種學習源碼的方式~

ps: 若是各位讀者看到本文的error或者由更好的優化建議,隨時聯繫~

相關文章
相關標籤/搜索