vue的響應(yīng)式系統(tǒng)和依賴收集

前言

在掘金上看染陌同學(xué)《剖析 Vue.js 內(nèi)部運行機制》的掘金小冊時,發(fā)現(xiàn)自己一個極大問題,基礎(chǔ)知識掌握的不夠牢靠,導(dǎo)致中間有時候出現(xiàn)一些錯誤,無法理解,所以在這里寫下這個筆記,加深自己的印象。

Object.defineProperty

在記錄vue的響應(yīng)式系統(tǒng)前,一定要對Object.defineProperty的用法掌握,這是實現(xiàn)vue數(shù)據(jù)雙向綁定的基礎(chǔ),但是vue的作者宣布將會在下個版本使用Proxy代替Object.defineProperty,這不重要,這里依然來說Object.defineProperty。這個對象的擴展方法是干什么的?簡單的說就是用來劫持對象屬性的,已達(dá)到對象在改變數(shù)據(jù)之前可以對對象進(jìn)行一系列操作,這就是js中數(shù)據(jù)劫持的一個基本原理。

Object.defineProperty有三個參數(shù),分別為obj, prop和descriptor

obj:要在其上定義屬性的對象。

prop:要定義或修改的屬性的名稱。

descriptor:將被定義或修改的屬性描述符。

而整個對象的操作都在descriptor里進(jìn)行,他接受一個對象參數(shù),對象參數(shù)支持六個屬性,這六個屬性在這里我們只需要使用四個,如下

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function () {},
  set: function () {}
})

enumerable: 當(dāng)且僅當(dāng)該屬性的enumerable為true時,該屬性才能夠出現(xiàn)在對象的枚舉屬性中。默認(rèn)為 false。

configurable: 當(dāng)且僅當(dāng)該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,同時該屬性也能從對應(yīng)的對象上被刪除。默認(rèn)為 false。

get: 當(dāng)讀取屬性時調(diào)用

set: 當(dāng)屬性值改變時調(diào)用

此處使用MDN的例子說明Object.defineProperty的使用

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(value) {
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

更多詳細(xì)介紹請查看MDN

vue的響應(yīng)式系統(tǒng)簡析

vue的響應(yīng)式系統(tǒng)在vue整個框架里有什么作用?或者更詳細(xì)的說,vue的數(shù)據(jù)雙向綁定是怎么實現(xiàn)的?這里就可以說Object.defineProperty是其關(guān)鍵所在,我先擼代碼,然后再詳細(xì)說

function cb(val) {
    console.log("視圖更新了?。。?)
}

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

function observer(obj) {
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

class Vue {
  constructor(options) {
    this.data = options.data
    observer(this.data)
  }
}

let o = new Vue({
  data: {
    text: "hello world!!!"
  }
})

o.data.text = "hello Tom" // 視圖更新了?。。?

從上面代碼我們可以看出,當(dāng)我們改變text的值時,就會觸發(fā)cb函數(shù),而整個過程中我們是通過Object.defineProperty的set實現(xiàn)的,當(dāng)屬性值改變時,就會觸發(fā)set,并把新值當(dāng)做參數(shù),這里就完成了簡單的響應(yīng)系統(tǒng),這里并沒有對傳入的參數(shù)做判斷,并且并不支持?jǐn)?shù)組,但是vue里是實現(xiàn)了對數(shù)組的支持的

依賴收集

為什么要依賴收集?依賴收集發(fā)揮著怎樣的作用?

在使用vue時,我們經(jīng)常會遇到data里有多個屬性,然而有的屬性并沒有在template里展示,但是我們即將修改這個屬性的值,那么就會造成視圖更新,這是沒有必要的,如下

new Vue({
  template: `
    <div>{{text1}}</div>
  `,
  data: {
    text1: '123',
    text2: '456'
  },
  mounted() {
    this.text2 = '789'
  }
})

這里我們改變了text2的值,但是我們并沒有在template里展示這個值,只是vue的內(nèi)部使用,所以并不需要通知視圖,進(jìn)行更新,所以這里我們就需要進(jìn)行依賴收集,避免不必要的視圖更新。

訂閱發(fā)布模式/觀察者模式

在說依賴收集之前,我先說在程序設(shè)計中經(jīng)常使用的兩個設(shè)計模式訂閱發(fā)布模式和觀察者模式,這是我們實現(xiàn)依賴收集的設(shè)計模式,了解到這些模式,會更容易理解如何進(jìn)行的依賴收集。

先寫個例子

class EventBus {
  constructor() {
    this._event = new Map()
  }
  addListener(type, fn) {
    const handler = this._event.get(type)
    if (!handler) {
      this._event.set(type, fn)
    }
  }
  emit(type, ...args) {
    const handler = this._event.get(type)
    if (handler && typeof handler === 'function') {
      if (args.length > 0) {
        handler.apply(this, args)
      } else {
        handler.call(this)
      }
    }
  }
}

var emitter = new EventBus()

emitter.addListener('put', function(name) {
    console.log("my name is " + name)
})

emitter.addListener('put', function(name) {
    console.log("your name is " + name)
})

emitter.emit('put', 'Lucy')    //my name is Lucy

這是一個模擬事件池的代碼,先給emitter添加一個事件,用emit觸發(fā)事件,但是在這里,我們給同一個事件綁定了多個函數(shù),當(dāng)emit時,希望可以通知綁定到這個事件的所有函數(shù),然而這里只是通知了第一個,當(dāng)我們希望這種一對多的依賴關(guān)系時,就可以用發(fā)布訂閱模式去描述??梢杂梦⑿殴娞杹硇蜗蟮恼f明這個模式,公眾號就是發(fā)布者,而用戶就是訂閱者,當(dāng)文章更新時,就會通知每一個訂閱者用戶,這樣一個發(fā)布者維護(hù)多個訂閱者,就是發(fā)布訂閱模式。那么什么是觀察者模式?其實在很多文章中,這兩個模式很難有什么區(qū)別,而在百度時,他們也是會成對出現(xiàn)的,這里我就不詳細(xì)討論他們的區(qū)別了,暫且當(dāng)做一個模式來看。

那么現(xiàn)在我來給這個EventBus進(jìn)行升級

class EventBus {
  constructor() {
    this._event = new Map()
  }
  addListener(type, fn) {
    const handler = this._event.get(type)
    if (!handler) {
      this._event.set(type, fn)
    } else if(handler && typeof handler === 'function') {
      this._event.set(type, [handler, fn])
    } else {
      this._event.set(type, handler.push(fn))
    }
  }
  emit(type, ...args) {
    const handler = this._event.get(type)
    if (handler && Array.isArray(handler)) {
      handler.forEach(fn => {
        if (args.length > 0) {
          fn.apply(this, args)
        } else {
          fn.call(this)
        }
      })
    } else {
       if (args.length > 0) {
         handler.apply(this, args)
       } else {
         handler.call(this)
       }
    }
  }
}

var emitter = new EventBus()

emitter.addListener('put', function(name) {
    console.log("my name is " + name)
})

emitter.addListener('put', function(name) {
    console.log("your name is " + name)
})

emitter.emit('put', 'Lucy')   
// my name is Lucy
// your name is Lucy

在這里實際上就是監(jiān)聽了所有綁定在listener上的函數(shù),也就是訂閱者綁定在發(fā)布者上,當(dāng)emit觸發(fā)時,就通知所有的訂閱者,發(fā)布更新了

依賴收集

現(xiàn)在我們對vue的響應(yīng)式系統(tǒng)進(jìn)行升級,先擼為敬

class Dep {
  constructor() {
    this.subs = [] 
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  // 通知所有的訂閱者sub更新
  notify(val) {
    this.subs.forEach(sub => {
      sub.update(val)
    })
  }
}
// 管理訂閱者的watcher
class Watcher {
  constructor() {
    Dep.target = this
  }
  update(val) {
    console.log("視圖更新了?。。?!")
  }
}
Dep.target = null

function defineReactive(obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function () {
        dep.addSub(Dep.target)
        return val
    },
    set: function (newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify(val)
    }
  })
}

function observer(obj) {
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

class Vue {
  constructor(options) {
    this.data = options.data
    observer(this.data)
    new Watcher()
    /**
    * 這里的console.log是模擬使用this.data的屬性
    * 以觸發(fā)defineProperty的get,這樣就會對當(dāng)屬性
    * 改變時,視圖需要更新的屬性進(jìn)行了收集,而未在
    * template里使用的進(jìn)行剔除
    */
    console.log(this.data.text)
  }
}

var vue = new Vue({
  data: {
    text: '123',
    text1: '456'
  }
})

vue.data.text = '456' // 視圖更新了?。。。?vue.data.text1 = '789'

改造好的vue響應(yīng)式系統(tǒng),基本具有了數(shù)據(jù)變化,視圖更新的流程,并能進(jìn)行依賴收集。

有錯誤之處,望請指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容