Vue數據綁定原理及簡單實現
本篇文章中的代碼只是部分片段,完整代碼存放於github上https://github.com/Q-Zhan/simple-vue。
進入正文~實現數據綁定主要是要實現兩個方面的功能:數據變化導致視圖變化,視圖變化導致數據變化。後者比較容易實現,就是監聽視圖的事件,然後在回調函數中改變數據。所以重點是數據變化時如何改變視圖。
這裏的思路是通過object.defineProperty()來對數據的屬性設置一個set函數,設置後當數據改變時set函數就會被調用,我們就可以裏面進行視圖更新操作。
具體實現過程
如上圖所示,我們需要一個監聽器Observer來給所有的屬性設置set函數。如果屬性發生了變化,就要通知所有的訂閱者Watcher。而這些Watcher統一存放在消息訂閱器Dep中,這樣比較方便統一管理。Watcher接受到來自Dep的通知後就執行相應的操作去更新視圖。
Observer
監聽器的核心代碼如下:
function observe(data) { if (!data || typeof data !== ‘object‘) { return; } Object.keys(data).forEach(function(key) { // 遍歷屬性,遞歸設置set函數 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 } }) }
通過調用observe()函數來遞歸地給data對象設置set和get函數,在data的屬性被get時添加watcher,被set時通知dep,dep的notify會接著通知所有的watcher去執行更新操作。
Dep
消息訂閱器的核心代碼如下:
function Dep() { this.subs = [] // 訂閱者數組 } Dep.prototype = { addSub: function(sub) { this.subs.push(sub) }, notify: function() { this.subs.forEach(function(sub) { sub.update() }) } } Dep.target = null
消息訂閱器比較簡單,就是維護一個subs數組。當監聽新屬性時把它push進subs數組中,然後dep被通知時觸發notify函數,從而觸發subs數組中每個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) // 執行更新時的回調函數
}
},
get: function() {
Dep.target = this
var value = this.vm.data[this.exp] // 讀取data的屬性,從而執行屬性的get函數
Dep.target = null
return value
}
}
Watcher的主要功能是去觸發屬性的get函數,從而添加watcher到Dep的subs數組中。另外就是在update()中更新屬性的值並觸發更新回調函數。
使用Watcher的方法如下:
var el = document.getElementById(‘XXX‘)
observe(data)
new Watcher(vm, exp, function(value) { // vm表示某個實例,exp表示屬性名
el.innerHTML = value
})
為了使用時的整潔,我們需要把代碼稍微包裝下。
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> //這個{{name}}暫時沒用
// 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。這下我們就可以直接通過selfVue.name = "XXX"來改變數據了,並且視圖也會相應變化。
Compile
上面的例子都是寫死一個屬性去替換,而真正的使用時我們需要去解析dom節點,對類如{{}}的進行替換並綁定watcher。這個解析過程通過Compile來實現。
nodeToFragement: function(el) {
var fragment = document.createDocumentFragment()
var child = el.firstChild
// 將dom節點移到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) // 遞歸遍歷子節點
}
});
},
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節點移入DocumentFragment中去,因為DocumentFragment中操作dom節點不會引起瀏覽器的重繪,性能會比直接操作dom節點好很多。二是遞歸調用compileElement函數來遍歷所有子節點,如果子節點包含{{}}形式的則調用compileText。三是compileText函數創建新的watcher。
當然加入compile後SimpleVue也要有相應的變化:
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
}
[參考資料]:https://www.cnblogs.com/libin-1/p/6893712.html
Vue數據綁定原理及簡單實現