MVVM
簡單地說就是數據驅動視圖,視圖改變(事件)也可以改變數據,就是雙向綁定的概念。
實現
為了監聽數據的改變,從而響應到視圖上,用的是Vue雙向綁定的核心Object.definePrototype(obj,key,{...})
,編寫Observer.js來實現。若set被觸發,通知所有的訂閱者(dep中存儲的是所有訂閱者實例對象),且在get中引入Dep.target,僅在添加watcher時主動賦值,防止之后多次添加watcher,并且get返回當前的val值。
Object.defineProperty(data,key,{
enumerable: true, // 可枚舉,可被Object.value()等遍歷方法所遍歷
configurable: true, // 可再重新定義,即可被修改,可被刪除, 默認為false
get: function(){
// console.log(Dep.target)
// Dep.target現在其實為null,但是在watcher.js中每次get都將watch自身添加到全局的Dep.target中
// 等到value獲取完畢,再將Dep.target清空
// Dep.target作為閉包,在函數中保持了dep的存在
// 如果沒有Dep.target,dep會被清除,在set中就無法通過dep.nptify()來出發watcher了
// Dep.target && dep.addSub(Dep.target)
if(Dep.target){
dep.addSub(Dep.target) // 添加一個訂閱者
}
return val
},
set: function(newVal){
console.log('值變化')
if(newVal === val) return
val = newVal
// 通知所有訂閱者
dep.notify()
}
})
訂閱者watcher.js關聯著模板編譯,每個生成的訂閱者都包含一個修改模板的callback,一旦對應發布者Observer.js中的set函數執行,所有對應訂閱者接收到通知,就會執行該訂閱者被創建時包含的callback,修改視圖。
update: function(){
this.run() // 屬性值變化收到通知
},
run: function(){
// 數據改變時
var value = this.vm.data[this.exp] // 取到最新的值 || this.get()
var oldVal = this.value // 存儲老值
if(value !== oldVal){
this.value = value
this.cb.call(this.vm,value,oldVal) // 執行compile中的回調,更新視圖
}
}
在Vue中實現數據綁定有兩種途徑,一種是雙大括號{{}}
,一種是v-model
,為了將數據綁定到頁面,同時為了給包含這兩種情況的模板添加訂閱者,編寫compile.js來實現。compile.js接收MVVM實例中掛載的根節點和該實例對象。先將所有節點剪切到fragment文檔片段中,再通過遍歷所有節點的方式,碰到包含數據綁定的節點,就創建一個新的watcher,并包含改變視圖的callback從而與watcher.js關聯,碰到其他類似事件綁定的節點,則給其綁定事件監聽器,從而實現v-on的事件綁定效果。另外使用文檔片段的好處是避免了頁面的頻繁的回流重繪,文檔節點使用完畢后返回頁面只需渲染一次即可。
核心代碼
init(){
if(this.el){
this.fragment = this.nodeToFragment(this.el)
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
}
},
// 節點全部轉為文檔片段
nodeToFragment(el){
// 創建空的文檔片段
var fragment = document.createDocumentFragment()
var child = el.firstChild
while(child){
// 子節點推入文檔片段,appendChild會有剪切的效果!!?。。? fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
compileElement(el){
// 創建好的文檔片段拿過來編譯
var childNodes = el.childNodes
var self = this
// dom數組不是真正的數組,沒有遍歷方法
// 多層嵌套slice處理效率過低導致執行失效
// [].slice.call(childNodes).forEach(function(node){})
// 也好像不是????在控制臺測試了一下都運行的飛快啊...
Array.prototype.forEach.call(childNodes,function(node){
// 處理{{}}的正則
var reg = /\{\{(.*)\}\}/
var text = node.textContent
if(self.isElementNode(node)){
self.compile(node)
}else if(self.isTextNode(node) && reg.test(text)){
// 檢測到雙括號
self.compileText(node,reg.exec(text)[1])
}
// 遞歸編譯,編譯所有節點
if(node.childNodes && node.childNodes.length){
self.compileElement(node)
}
})
}