前言
在掘金上看染陌同學(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)行依賴收集。
有錯誤之處,望請指正。