
數(shù)據(jù)劫持Observer
????????所謂數(shù)據(jù)劫持就是給對象的每一個屬性增加get,set方法
1.觀察對象,給對象增加Object.defineProperty
2.vue特點(diǎn)是不能新增不存在的屬性,不存在的屬性沒有g(shù)et和set
3.深度響應(yīng),因?yàn)槊看钨x予一個新對象時會給這個新對象增加defineProperty
// 創(chuàng)建一個Observer構(gòu)造函數(shù)
function Observe(data) {
let dep = new Dep()
// 既然要給對象的每一個屬性增加get、set,那就先遍歷一遍對象
for(let key in data) {
let val = data[key]
// 遞歸繼續(xù)向下找,實(shí)現(xiàn)深度的數(shù)據(jù)劫持
observe(val)
Object.defineProperty(data, key, {
configurable: true,
get() {
// 當(dāng)獲取值的時候就會自動調(diào)用get方法,于是在數(shù)據(jù)劫持observe修改一下get方法,將watcher添加到訂閱事件中
// Dep.target && dep.addSub(Dep.target)
if (Dep.target) {
dep.depend() // 和上面一行代碼的意思是一樣的
}
return val
},
set(newVal) {
// 如果設(shè)置的新值和以前的值一樣,就不處理
if (val === newVal) {
return
}
val = newVal
// 當(dāng)設(shè)置完新值后,也需要把新值再去數(shù)據(jù)劫持(不然新值的屬性沒有g(shù)et和set方法)
observe(newVal)
// 讓所有watcher的update方法執(zhí)行
dep.notify()
}
})
}
}
數(shù)據(jù)代理
????????數(shù)據(jù)代理就是讓我們每次取data里面的數(shù)據(jù)時,不用每次都寫一長串,比如mvvm._data.album.name這種,我們可以直接寫成mvvm.album.name這種顯而易見的方式。
for(let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key]
},
set(newVal) {
this._data[key] = newVal
}
})
}
數(shù)據(jù)編譯Compile
????????options中的el參數(shù),為我們指定了需要編譯哪些內(nèi)容,而我們需要做的僅僅是解析出通過v-model、v-text、{{}}等等標(biāo)識和指令,然后獲取綁定數(shù)據(jù)的值,替換掉標(biāo)識的內(nèi)容,并進(jìn)行數(shù)據(jù)的變化監(jiān)聽watcher,當(dāng)再有值發(fā)生變化時,可以及時通知其修改對應(yīng)dom元素。
function Compile(el, vm) {
// 講el掛載到實(shí)例上方便調(diào)用
vm.$el = document.querySelector(el)
// 創(chuàng)建一個新的空白的文檔片段,在el范圍里將內(nèi)容都拿到,當(dāng)然不能一個一個的拿,可以選擇移到內(nèi)存中去,然后放入文檔碎片中,節(jié)省開銷
// DocumentFragment是DOM節(jié)點(diǎn),它不是DOM樹的一部分,通常的用例是創(chuàng)建文檔片段,講元素附加到文檔片段,然后將文檔片段附加到DOM樹,在DOM樹中,文檔片段將其所有的子元素所代替。因?yàn)槲臋n片段存在于內(nèi)存中,并不在DOM樹中,所以將子元素插入到文檔片段時不會引起頁面回流(對元素位置和幾何上的計算)。因此使用文檔片段通常會帶來更多好的性能。
let fragment = document.createDocumentFragment()
while (child = vm.$el.firstChild) {
// 將el中的內(nèi)容放入到內(nèi)存中
fragment.appendChild(child)
}
// 對el里面的內(nèi)容進(jìn)行替換
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent
// 正則匹配{{}}
let reg = /\{\{(.*?)\}\}/g
// 如果既是文本節(jié)點(diǎn)又有大括號
if (node.nodeType === 3 && reg.test(txt)) {
function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placeholder) => {
// 我們需要訂閱一個事件,當(dāng)數(shù)據(jù)改變的時候需要重新刷新視圖,這就需要在replace替換的邏輯來進(jìn)行處理
// 通過new Watcher 把數(shù)據(jù)訂閱一下,數(shù)據(jù)一變就執(zhí)行改變內(nèi)容的操作
// 監(jiān)聽變化,給watcher再添加兩個參數(shù),用來取新的值給回調(diào)函數(shù)
new Watcher(vm, placeholder, replaceTxt)
return placeholder.split('.').reduce((val, key) => {
return val[key]
}, vm)
// 舉個例子解釋一下上面的代碼
// 'album.name'.split('.') => ['album','name'] => ['album','name'].reduce((val,key) => val[key])
// 這里vm還是作為初始值傳給val,進(jìn)行第一次調(diào)用,返回的是vm['album'],然后將返回的vm['album']這個對象傳給下一次調(diào)用的val
// 最后變成了vm['album']['name'] => '知足'
})
}
replaceTxt()
}
// 如果還有子節(jié)點(diǎn),繼續(xù)遞歸replace
if (node.childNodes && node.childNodes.length) {
replace(node)
}
})
}
replace(fragment)
vm.$el.appendChild(fragment)
}
發(fā)布訂閱Dep、Watcher
????????就像買房的中介一樣,用戶(watcher)去買房,不可能天天去房地產(chǎn)開發(fā)商那邊去問有沒有房源,更多的是找一個中介(dep),然后把我們的需求和聯(lián)系方式告訴中介(dep.depend()),中介一旦有滿足需求的房源,便會打電話來通知我們dep.notify()。
????????我們需要一個訂閱器Dep,它需要有收集需求和聯(lián)系方式的功能,也需要有打電話通知的功能。
function Dep() {
// 定義一個數(shù)組,用來存放函數(shù)的事件池
this.subs = []
}
Dep.prototype = {
// 收集需求和聯(lián)系方式的功能
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
},
addSub(sub) {
this.subs.push(sub)
},
// 發(fā)通知的功能
notify() {
// 綁定的方法,都有一個update方法
this.subs.forEach(sub => sub.update())
}
}
????????我們需要一個訂閱者watcher,它包含接受通知的功能,以及建立與Dep關(guān)聯(lián)的功能。
function Watcher(vm, exp, fn) {
// 將fn放到實(shí)例上
this.fn = fn
this.vm = vm
this.exp = exp
// 建立關(guān)聯(lián)
Dep.target = this
let arr = exp.split('.')
// 這里取值,會觸發(fā)value的get方法,所以需要在get方法里將聯(lián)系人的方式給中介,代碼47行g(shù)et方法
let val = vm
// 取值,獲取到this.album.name,默認(rèn)就會調(diào)用get方法
arr.forEach(key => {
val = val[key]
})
// 釋放關(guān)聯(lián)
Dep.target = null
}
Watcher.prototype = {
// 接受通知的功能,收到消息后,進(jìn)行更新數(shù)據(jù)的操作
update() {
// notify的時候值已經(jīng)更改了,再通過vm,exp來獲取新的值
let arr = this.exp.split('.')
let val = this.vm
arr.forEach(key => {
// 通過get獲取到新的值
val = val[key]
})
// 將每次拿到的新值去替換{{}}的內(nèi)容
this.fn(val)
},
addDep(dep) {
dep.addSub(this)
}
}
雙向數(shù)據(jù)綁定
數(shù)據(jù)--------------->Dom
1.通過compile解析指令和數(shù)據(jù),為其添加watcher
2.watcher觸發(fā)對應(yīng)的get方法,使其進(jìn)行依賴收集,把對應(yīng)的watcher進(jìn)行收集
3.當(dāng)數(shù)據(jù)發(fā)送變化的時候,觸發(fā)set方法,使其通知watcher進(jìn)行視圖更新
Dom--------------->數(shù)據(jù)
1.通過compile解析指令和數(shù)據(jù)
2.監(jiān)聽Dom input等更新動作,當(dāng)觸發(fā)dom更新時,在對應(yīng)回調(diào)函數(shù)中更新實(shí)例vm中的數(shù)據(jù)值
// 如果是元素節(jié)點(diǎn)
if (node.nodeType === 1) {
// 獲取dom上的所有屬性,是個類數(shù)組
let nodeAttr = node.attributes
Array.from(nodeAttr).forEach(attr => {
let name = attr.name // v-model
let exp = attr.value // who
if (name.includes('v-')) {
node.value = vm[exp] // 獲取this.who的值
}
// 監(jiān)聽變化
new Watcher(vm, exp, function (newVal) {
node.value = newVal
})
node.addEventListener('input', e => {
let newVal = e.target.value
// 相當(dāng)于給this.who 賦了一個新值,而值的改變會調(diào)用set,set中又會調(diào)用notify,notify中調(diào)用watcher的update方法實(shí)現(xiàn)了更新
vm[exp] = newVal
})
})
}
以上就實(shí)現(xiàn)了一個MVVM模型
完整代碼
Index.html
<head>
<meta charset="utf-8">
</head>
<body>
<div id="app">
<h1>{{song}}</h1>
<p>《{{album.name}}》是{{singer}}2005年發(fā)行的專輯</p>
<p>主打歌為{{album.theme}}</p>
<input v-model="who" type="text">
</div>
<script src="mvvm.js"></script>
<script>
let mvvm = new Mvvm({
el: '#app',
data: {
song: '閑魚',
album: {
name: '知足專輯',
theme: '知足主打歌'
},
singer: '五月天',
who: '五月天還是周杰倫'
}
})
</script>
</body>
mvvm.js
// 創(chuàng)建一個Mvvm構(gòu)造函數(shù),講options賦一個初始值,防止沒傳,等同于options || {}
function Mvvm(options = {}) {
// 在vue上將所有的屬性都掛載到了vm.$options 上,所以我們也同樣實(shí)現(xiàn),將所有屬性掛載到了$options
this.$options = options;
// this._data這里也和vue一樣
let data = this._data = this.$options.data;
// 一、數(shù)據(jù)劫持
observe(data)
// 二、數(shù)據(jù)代理
// 數(shù)據(jù)代理就是讓我們每次取data里面的數(shù)據(jù)時,不用每次都寫一長串,比如mvvm._data.album.name這種,我們可以直接寫成mvvm.album.name這種顯而易見的方式
for(let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key]
},
set(newVal) {
this._data[key] = newVal
}
})
}
// 三、數(shù)據(jù)編譯
new Compile(options.el, this)
}
// 一、數(shù)據(jù)劫持(所謂數(shù)據(jù)劫持就是給對象增加get,set)
// 為什么要做數(shù)據(jù)劫持?
// 1.觀察對象,給對象增加Object.defineProperty
// 2.vue特點(diǎn)是不能新增不存在的屬性,不存在的屬性沒有g(shù)et和set
// 3.深度響應(yīng),因?yàn)槊看钨x予一個新對象時會給這個新對象增加defineProperty
function observe(data) {
// 如果不是對象的話就直接return掉,放置遞歸溢出
if(!data || typeof data !== 'object') return
return new Observe(data)
}
// 創(chuàng)建一個Observer構(gòu)造函數(shù)
function Observe(data) {
let dep = new Dep()
// 既然要給對象的每一個屬性增加get、set,那就先遍歷一遍對象
for(let key in data) {
let val = data[key]
// 遞歸繼續(xù)向下找,實(shí)現(xiàn)深度的數(shù)據(jù)劫持
observe(val)
Object.defineProperty(data, key, {
configurable: true,
get() {
// 當(dāng)獲取值的時候就會自動調(diào)用get方法,于是在數(shù)據(jù)劫持observe修改一下get方法,將watcher添加到訂閱事件中
// Dep.target && dep.addSub(Dep.target)
if (Dep.target) {
dep.depend() // 和上面一行代碼的意思是一樣的
}
return val
},
set(newVal) {
// 如果設(shè)置的新值和以前的值一樣,就不處理
if (val === newVal) {
return
}
val = newVal
// 當(dāng)設(shè)置完新值后,也需要把新值再去數(shù)據(jù)劫持(不然新值的屬性沒有g(shù)et和set方法)
observe(newVal)
// 讓所有watcher的update方法執(zhí)行
dep.notify()
}
})
}
}
// 三、創(chuàng)建Compile構(gòu)造函數(shù)
// options中的el參數(shù),為我們指定了需要編譯哪些內(nèi)容,而我們需要做的僅僅是解析出通過v-model、v-text、{{}}等等標(biāo)識和指令,然后獲取綁定數(shù)據(jù)的值,替換掉標(biāo)識的內(nèi)容,并進(jìn)行數(shù)據(jù)的變化監(jiān)聽watcher,當(dāng)再有值發(fā)生變化時,可以及時通知其修改對應(yīng)dom元素。
function Compile(el, vm) {
// 講el掛載到實(shí)例上方便調(diào)用
vm.$el = document.querySelector(el)
// 創(chuàng)建一個新的空白的文檔片段,在el范圍里將內(nèi)容都拿到,當(dāng)然不能一個一個的拿,可以選擇移到內(nèi)存中去,然后放入文檔碎片中,節(jié)省開銷
// DocumentFragment是DOM節(jié)點(diǎn),它不是DOM樹的一部分,通常的用例是創(chuàng)建文檔片段,講元素附加到文檔片段,然后將文檔片段附加到DOM樹,在DOM樹中,文檔片段將其所有的子元素所代替。因?yàn)槲臋n片段存在于內(nèi)存中,并不在DOM樹中,所以講子元素插入到文檔片段時不會引起頁面回流(對元素位置和幾何上的計算)。因此使用文檔片段通常會帶來更多好的性能。
let fragment = document.createDocumentFragment()
while (child = vm.$el.firstChild) {
// 將el中的內(nèi)容放入到內(nèi)存中
fragment.appendChild(child)
}
// 對el里面的內(nèi)容進(jìn)行替換
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent
// 正則匹配{{}}
let reg = /\{\{(.*?)\}\}/g
// 如果既是文本節(jié)點(diǎn)又有大括號
if (node.nodeType === 3 && reg.test(txt)) {
function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placeholder) => {
// 五、數(shù)據(jù)更新視圖
// 我們需要訂閱一個事件,當(dāng)數(shù)據(jù)改變的時候需要重新刷新視圖,這就需要在replace替換的邏輯來進(jìn)行處理
// 通過new Watcher 把數(shù)據(jù)訂閱一下,數(shù)據(jù)一變就執(zhí)行改變內(nèi)容的操作
// 監(jiān)聽變化,給watcher再添加兩個參數(shù),用來取新的值給回調(diào)函數(shù)
new Watcher(vm, placeholder, replaceTxt)
return placeholder.split('.').reduce((val, key) => {
return val[key]
}, vm)
// 舉個例子解釋一下上面的代碼
// 'album.name'.split('.') => ['album','name'] => ['album','name'].reduce((val,key) => val[key])
// 這里vm還是作為初始值傳給val,進(jìn)行第一次調(diào)用,返回的是vm['album'],然后將返回的vm['album']這個對象傳給下一次調(diào)用的val
// 最后變成了vm['album']['name'] => '知足'
})
}
replaceTxt()
}
// 六、雙向數(shù)據(jù)綁定
// 如果是元素節(jié)點(diǎn)
if (node.nodeType === 1) {
// 獲取dom上的所有屬性,是個類數(shù)組
let nodeAttr = node.attributes
Array.from(nodeAttr).forEach(attr => {
let name = attr.name // v-model
let exp = attr.value // who
if (name.includes('v-')) {
node.value = vm[exp] // 獲取this.who的值
}
// 監(jiān)聽變化
new Watcher(vm, exp, function (newVal) {
node.value = newVal
})
node.addEventListener('input', e => {
let newVal = e.target.value
// 相當(dāng)于給this.who 賦了一個新值,而值的改變會調(diào)用set,set中又會調(diào)用notify,notify中調(diào)用watcher的update方法實(shí)現(xiàn)了更新
vm[exp] = newVal
})
})
}
// 如果還有子節(jié)點(diǎn),繼續(xù)遞歸replace
if (node.childNodes && node.childNodes.length) {
replace(node)
}
})
}
replace(fragment)
vm.$el.appendChild(fragment)
}
// 四、發(fā)布訂閱
// 就像買房的中介一樣,用戶(watcher)去買房,不可能天天去房地產(chǎn)開發(fā)商那邊去問有沒有房源,更多的是找一個中介(dep),然后把我們的需求和聯(lián)系方式告訴中介(dep.depend()),中介一旦有滿足需求的房源,便會打電話來通知我們dep.notify()
// 我們需要一個訂閱器Dep,它需要有收集需求和聯(lián)系方式的功能,也需要有打電話通知的功能
// 發(fā)布訂閱主要靠的就是數(shù)組關(guān)系,訂閱就是放入函數(shù),發(fā)布就是讓數(shù)組里的函數(shù)執(zhí)行 如[fn1, fn2, fn3]
// 訂閱器
function Dep() {
// 定義一個數(shù)組,用來存放函數(shù)的事件池
this.subs = []
}
Dep.prototype = {
// 收集需求和聯(lián)系方式的功能
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
},
addSub(sub) {
this.subs.push(sub)
},
// 發(fā)通知的功能
notify() {
// 綁定的方法,都有一個update方法
this.subs.forEach(sub => sub.update())
}
}
// 我們需要一個訂閱者watcher,它包含接受通知的功能,以及建立與Dep關(guān)聯(lián)的功能
// 監(jiān)聽函數(shù),通過watcher這個類創(chuàng)建的實(shí)例,都擁有update方法
// 訂閱者
function Watcher(vm, exp, fn) {
// 將fn放到實(shí)例上
this.fn = fn
this.vm = vm
this.exp = exp
// 建立關(guān)聯(lián)
Dep.target = this
let arr = exp.split('.')
// 這里取值,會觸發(fā)value的get方法,所以需要在get方法里將聯(lián)系人的方式給中介,代碼47行g(shù)et方法
let val = vm
// 取值,獲取到this.album.name,默認(rèn)就會調(diào)用get方法
arr.forEach(key => {
val = val[key]
})
// 釋放關(guān)聯(lián)
Dep.target = null
}
Watcher.prototype = {
// 接受通知的功能,收到消息后,進(jìn)行更新數(shù)據(jù)的操作
update() {
// notify的時候值已經(jīng)更改了,再通過vm,exp來獲取新的值
let arr = this.exp.split('.')
let val = this.vm
arr.forEach(key => {
// 通過get獲取到新的值
val = val[key]
})
// 將每次拿到的新值去替換{{}}的內(nèi)容
this.fn(val)
},
addDep(dep) {
dep.addSub(this)
}
}
// 數(shù)據(jù)--------------->Dom
// 1.通過compile解析指令和數(shù)據(jù),為其添加watcher
// 2.watcher觸發(fā)對應(yīng)的get方法,使其進(jìn)行依賴收集,把對應(yīng)的watcher進(jìn)行收集
// 3.當(dāng)數(shù)據(jù)發(fā)送變化的時候,觸發(fā)set方法,使其通知watcher進(jìn)行視圖更新
// Dom--------------->數(shù)據(jù)
// 1.通過compile解析指令和數(shù)據(jù)
// 2.監(jiān)聽Dom input等更新動作,當(dāng)觸發(fā)dom更新時,在對應(yīng)回調(diào)函數(shù)中更新實(shí)例vm中的數(shù)據(jù)值