本篇文章中的代碼只是部分片段,完整代碼存放于github上https://github.com/Q-Zhan/simple-vue瓜浸。
進(jìn)入正文~實(shí)現(xiàn)數(shù)據(jù)綁定主要是要實(shí)現(xiàn)兩個(gè)方面的功能:數(shù)據(jù)變化導(dǎo)致視圖變化赎线,視圖變化導(dǎo)致數(shù)據(jù)變化。后者比較容易實(shí)現(xiàn),就是監(jiān)聽(tīng)視圖的事件,然后在回調(diào)函數(shù)中改變數(shù)據(jù)宁玫。所以重點(diǎn)是數(shù)據(jù)變化時(shí)如何改變視圖。
這里的思路是通過(guò)object.defineProperty()來(lái)對(duì)數(shù)據(jù)的屬性設(shè)置一個(gè)set函數(shù)柑晒,設(shè)置后當(dāng)數(shù)據(jù)改變時(shí)set函數(shù)就會(huì)被調(diào)用欧瘪,我們就可以里面進(jìn)行視圖更新操作。
具體實(shí)現(xiàn)過(guò)程
如上圖所示敦迄,我們需要一個(gè)監(jiān)聽(tīng)器Observer來(lái)給所有的屬性設(shè)置set函數(shù)。如果屬性發(fā)生了變化凭迹,就要通知所有的訂閱者Watcher罚屋。而這些Watcher統(tǒng)一存放在消息訂閱器Dep中,這樣比較方便統(tǒng)一管理嗅绸。Watcher接受到來(lái)自Dep的通知后就執(zhí)行相應(yīng)的操作去更新視圖脾猛。
Observer
監(jiān)聽(tīng)器的核心代碼如下:
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) { // 遍歷屬性,遞歸設(shè)置set函數(shù)
defineReactive(data, key, data[key]);
});
}
function defineReactive(data, key, val) {
observe(val)
var dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
if (Dep.target) {
dep.addSub(Dep.target) // 添加watcher
}
return val
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal;
dep.notify() // 通知dep
}
})
}
通過(guò)調(diào)用observe()函數(shù)來(lái)遞歸地給data對(duì)象設(shè)置set和get函數(shù)鱼鸠,在data的屬性被get時(shí)添加watcher猛拴,被set時(shí)通知dep,dep的notify會(huì)接著通知所有的watcher去執(zhí)行更新操作蚀狰。
Dep
消息訂閱器的核心代碼如下:
function Dep() {
this.subs = [] // 訂閱者數(shù)組
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update()
})
}
}
Dep.target = null
消息訂閱器比較簡(jiǎn)單愉昆,就是維護(hù)一個(gè)subs數(shù)組。當(dāng)監(jiān)聽(tīng)新屬性時(shí)把它push進(jìn)subs數(shù)組中麻蹋,然后dep被通知時(shí)觸發(fā)notify函數(shù)跛溉,從而觸發(fā)subs數(shù)組中每個(gè)watcher的update操作。
Watcher
function Watcher(vm, exp, cb) {
this.cb = cb
this.vm = vm
this.exp = exp
this.value = this.get()
}
Watcher.prototype = {
update: function() {
this.run()
},
run: function() {
var value = this.vm.data[this.exp]
var oldVal = this.value
if (value !== oldVal) {
this.value = value
this.cb.call(this.vm, value, oldVal) // 執(zhí)行更新時(shí)的回調(diào)函數(shù)
}
},
get: function() {
Dep.target = this
var value = this.vm.data[this.exp] // 讀取data的屬性扮授,從而執(zhí)行屬性的get函數(shù)
Dep.target = null
return value
}
}
Watcher的主要功能是去觸發(fā)屬性的get函數(shù)芳室,從而添加watcher到Dep的subs數(shù)組中。另外就是在update()中更新屬性的值并觸發(fā)更新回調(diào)函數(shù)刹勃。
使用Watcher的方法如下:
var el = document.getElementById('XXX')
observe(data)
new Watcher(vm, exp, function(value) { // vm表示某個(gè)實(shí)例,exp表示屬性名
el.innerHTML = value
})
為了使用時(shí)的整潔堪侯,我們需要把代碼稍微包裝下。
SimpleVue
function SimpleVue (data, el, exp) {
var self = this
this.data = data
Object.keys(data).forEach(function(key) {
self.proxyKeys(key)
})
observe(data)
el.innerHTML = this.data[exp]
new Watcher(this, exp, function(value) {
el.innerHTML = value
})
return this
}
SimpleVue.prototype = {
proxyKeys: function(key) {
var self = this
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: function() {
return self.data[key]
},
set: function(newVal) {
self.data[key] = newVal
}
})
}
}
使用如下:
// html
<h1 id="name">{{name}}</h1> //這個(gè){{name}}暫時(shí)沒(méi)用
// js
var el = document.querySelector('#name')
var selfVue = new SimpleVue({ name: 'hello'}, el, 'name')
setTimeout(function() {
selfVue.name = '123'
}, 2000)
需要注意的是SimpleVue原型的proxyKeys是為了將selfVue.data.name這種操作代理為selfVue.name荔仁。這下我們就可以直接通過(guò)selfVue.name = "XXX"來(lái)改變數(shù)據(jù)了伍宦,并且視圖也會(huì)相應(yīng)變化芽死。
Compile
上面的例子都是寫(xiě)死一個(gè)屬性去替換,而真正的使用時(shí)我們需要去解析dom節(jié)點(diǎn)雹拄,對(duì)類(lèi)如{{}}的進(jìn)行替換并綁定watcher收奔。這個(gè)解析過(guò)程通過(guò)Compile來(lái)實(shí)現(xiàn)。
nodeToFragement: function(el) {
var fragment = document.createDocumentFragment()
var child = el.firstChild
// 將dom節(jié)點(diǎn)移到fragment
while(child) {
fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
compileElement: function(el) {
var childNodes = el.childNodes
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/
var text = node.textContent
if (self.isTextNode(node) && reg.test(text)) {
self.compileText(node, reg.exec(text)[1])
}
if (node.childNodes && node.childNodes.length) {
self.compileElement(node) // 遞歸遍歷子節(jié)點(diǎn)
}
});
},
compileText: function(node, exp) {
var self = this
var initText = this.vm[exp]
this.updateText(node, initText)
new Watcher(this.vm, exp, function(value) {
self.updateText(node, value)
})
},
compile主要做三件事情滓玖。一是將dom節(jié)點(diǎn)移入DocumentFragment中去坪哄,因?yàn)镈ocumentFragment中操作dom節(jié)點(diǎn)不會(huì)引起瀏覽器的重繪,性能會(huì)比直接操作dom節(jié)點(diǎn)好很多势篡。二是遞歸調(diào)用compileElement函數(shù)來(lái)遍歷所有子節(jié)點(diǎn)翩肌,如果子節(jié)點(diǎn)包含{{}}形式的則調(diào)用compileText。三是compileText函數(shù)創(chuàng)建新的watcher禁悠。
當(dāng)然加入compile后SimpleVue也要有相應(yīng)的變化:
function SimpleVue (options) {
var self = this
this.vm = this
this.data = options.data
Object.keys(this.data).forEach(function(key) {
self.proxyKeys(key)
})
observe(this.data)
new Compile(options.el, this.vm)
return this
}