Vue2.x雙向數據綁定原理-Object篇

前言

相信每位前端人都被問過Vue雙向數據綁定的原理是什麼吧?應該也很快能答出來是經過Object.defineProperty讓數據的每一個屬性變成getter/setter實現的,但這僅僅只回答了一半,由於ObjectArray的實現方式是不同的,這也是爲何標題是Object篇的緣由。(建議先看總結,再一步步看實現過程)javascript

基礎知識

首先了解一下下面的概念:前端

聲明式編程和命令式編程

這個概念就通俗點說了,想詳細瞭解的可自行查閱資料java

  • 命令式:命令計算機如何去作事,嚴格按照咱們的命令去實現,無論咱們想要的結果是什麼。
  • 聲明式:咱們只須要告訴計算機想要什麼,讓它本身去想辦法按它的思路去作。 這裏用一個簡單的例子去對比二者的區別,
// 給定一個數組 arr = [1, 2, 3], 想要一個新的數組每一項都加一
  const arr = [1, 2, 3];
  // 命令式 告訴瀏覽器循環數組,每個元素+1,而後push進新數組
  let newArr1 = [];
  for (let i = 0; i < arr.length; i++) {
    newArr1.push(arr[i]+1);
  }
  console.log(newArr1) // 拿到新數組

  // 聲明式 告訴瀏覽器新數組的每一項是舊數組對應的每一項加一
  let result = arr.map(item => {
    return item + 1;
  })
  console.log(result) // 新的數組
複製代碼

爲何要說這個呢?由於Vue.js是聲明式的,按API文檔的要求來寫Vue就知道要作什麼。 (回頭看好像偏題了,無論了就當鞏固知識吧😂)編程

Object.defineProperty

在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象, Vue.js是利用這個方法修改data對象的屬性。數組

let name = 'test';
  let obj = {};
  Object.defineProperty(obj, 'name', {
    configurable: true, // 可修改,可刪除
    enumerable: true, //可枚舉
    get: function() { // 讀值觸發
      console.log('讀取數據');
      return name;
    },
    set: function(newVal) { // 賦值觸發
      if(name === newVal){
        return;
      }
      console.log('從新賦值');
      name = newVal;
    }
  })
  console.log(obj.name);
  obj.name = '賦值';
  //打印出
  // 讀取數據
  // test
  // 從新賦值
  // 賦值
複製代碼

到這裏已經算是Vue的Object雙向數據綁定原理了。瀏覽器

實現完整的Object對象的雙向數據綁定,Vue作了那些操做呢?函數

數據監控:「用」和「變」

經過上面的概念介紹就知道Object.defineProperty是作數據監控的,獲取值的時候get被觸發進行相應操做,設置數據時,set被觸發這時就能知道數據是否被改變。那咱們是否是就很清楚知道能夠在數據被調用觸發get函數的時候,去收集那些地方使用了對應的數據了呢?而後在設置的時候,觸發set函數去通知get收集好的依賴進行相應的操做呢?好了,下面就針對目前這個理解,對Object.defineProperty進行封裝工具

defineReactive

function defineReactive(data, key, val) {
  //let dep = [];
  let dep = new Dep() // 修改
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      // 收集依賴
      // dep.push(window.target) // window.target後面會定義,很6的操做,期待一下
      dep.depend() // 修改
      return val
    },
    set: function(newVal){
      if(val === newVal){
        return
      }
      // 觸發依賴
      // for(let i=0; i<dep.length; i++){
      // dep[i](newVal, val);
      // }
      dep.notify() // 修改
      val = newVal
    }
  })
}
複製代碼

這裏就實現了在get的時候,收集依賴保存到dep這個數組中,當觸發set的時候,就把dep中的每一個依賴觸發。在源碼裏是把dep封裝成一個類,來管理依賴的,下面就實現一下Dep這個類吧。ui

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) // window.target是什麼?
    }
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // window.target的update方法
    }
  }
}

function remove (arr, item) {
  if(arr.length){
    const index = arr.indexOf(item)
    if(index > -1){
      return arr.splice(index, 1)
    }
  }
}
複製代碼

這樣咱們封裝的Dep類就能夠收集依賴、刪除依賴、通知依賴,那咱們就要把這個Dep類用上,對上面的defineReactive進行修改一下。Dep收集到的依賴看代碼都知道是window.target,當數據發生變化的時候,調用window.targetupdate方法進行響應更新。this

Watcher類

源碼裏有個Watcher類,它的實例就是咱們收集的window.target,下面先來看看Vue中的一個用法

vm.$watch('user.name', function(newVal, oldVal){
  console.log('個人新名叫' + newVal); // 就是update函數
})

複製代碼

當Vue實例中的data.user.name被修改時,會觸發function的執行,也就是說須要把這個函數添加到data.user.name的依賴中,怎麼收集呢?是否是調一下data.user.nameget方法就能夠了。那麼Watcher要作的就是把本身的實例添加到對應屬性的Dep中,同時也有通知去更新的能力,下面寫下Watcher

export default class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get() // 獲取初始值
  }
  get() {
    window.target = this // 把當前實例暴露給Dep,Dep就知道依賴是誰了
    let value = this.getter.call(this.vm, this.vm) // 取一下值,觸發vm實例上對應屬性的get方法收集依賴
    window.target = undefined // 用完給別人用
    return value
  }
  update() {
    const oldValue = this.value // 舊值
    this.value = this.get() // 獲取新值
    this.cb.call(this.vm, this.value, oldValue)
  }
}
複製代碼

到這裏再回顧一下上面寫好的幾個程序,你會發現之間都是很巧妙的結合了,特別是Watcher的實例,把本身給添加到Dep中了,反正我本身是以爲這操做特6。這裏也說明了Vue中de$watch是經過Watcher實現的。

固然parsePath還沒說是什麼,結合上面的例子和Watcher應該知道parsePath返回的是一個方法,而且被調用後返回一個值,也就是獲取值的功能,下面來實現一下

const bailRE = /[^\w.$]/
export function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.spilt('.')
  return function(obj){
    for(let i = 0; i < segments.length; i++){
      if(!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
複製代碼

Watcher中的this.getter.call(this.vm, this.vm)parsePath的返回的函數指向this.vm,並把this.vm當參數傳過去取值。

Observer類

Vue中data的每個屬性都會被監測到,實際上咱們使用defineReactive就能夠監測,若是一個data有不少屬性,那是否是要調用不少次呢,那麼就有了Observer這個工具類把每一個屬性變成getter/setter,來碼上

export class Observer {
  constructor(value) {
    this.value = value
    if(!Array.isArray(value)){
      this.walk(value)
    }
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}
複製代碼

那麼new Observer(obj)就能把obj下的屬性都變成getter/setter了,若是obj[key]依然是一個對象呢?是否是要繼續new Observer(obj[key])呀,那麼就是defineReactive拿到obj[key]時,須要進行判斷是否是對象,是的話就進行遞歸,那麼加上這一步操做

function defineReactive(data, key, val) {
  if(typeof val === 'object') {
    new Observer(val)
  }
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      dep.depend()
      return val
    },
    set: function(newVal){
      if(val === newVal){
        return
      }
      dep.notify()
      val = newVal
    }
  })
}
複製代碼

到這裏Vue中Object是數據響應已經完成了,可是有缺陷你們都很清楚,就是給data新增屬性或者刪除屬性時,沒法監測,上面的實現過程都是依賴現有屬性進行的,可是Vue提供$set$delete去實現這兩個功能,相信弄懂上面的代碼,這兩個的實現就不難了。

總結

對Vue中Object的數據響應,我總結的一句話就是「定義getter/setter備用,「用」:收集依賴,「變」:觸發依賴

  • 「備用」: 經過ObserverdefineReactive把屬性變成getter/setter
  • 「用」: 經過Watchergetter中把依賴收集到dep
  • 「變」: 經過setter告訴dep數據變化了,dep通知Watcher去更新;
相關文章
相關標籤/搜索