模擬Vue實現(xiàn)雙向綁定
使用Vue也有一段時間了筹麸,作為一款MVVM框架罢坝,雙向綁定是其最核心的部分恋追,所以最近動手實現(xiàn)了一個簡單的雙向綁定供常。先上最終成果圖
思路
實現(xiàn)MVVM主要包含兩個方面悼凑,一個是數(shù)據(jù)變化更新視圖偿枕,另一個則是對應的試圖變化更新數(shù)據(jù),重點在于怎么實現(xiàn)數(shù)據(jù)變了户辫,如何去更新視圖渐夸,因為視圖更新數(shù)據(jù)使用事件監(jiān)聽的形式就可以實現(xiàn),比如input
標簽通過監(jiān)聽input
事件就可以實現(xiàn)渔欢。所以重點是如何實現(xiàn)數(shù)據(jù)改變更新視圖墓塌。
其實是通過Object.defineProperty()
對屬性進行數(shù)據(jù)劫持,設置set
函數(shù)奥额,當數(shù)據(jù)改變后就回來觸發(fā)這個函數(shù)苫幢,所以要將一些需要更新的方法放在這里面就可以實現(xiàn)data
更新view
了。
實現(xiàn)功能
-
實現(xiàn)一個解析器Compile垫挨,可以掃描和解析每個節(jié)點的相關指令韩肝,并根據(jù)初始化模板數(shù)據(jù)以及初始化相應的訂閱器。
- 文本的編譯 例如
{{message}}
- 指令的編譯 例如
v-model
- 文本的編譯 例如
實現(xiàn)一個監(jiān)聽器Observer九榔,用來劫持并監(jiān)聽所有屬性哀峻,如果有變動的,就通知訂閱者哲泊。
實現(xiàn)一個訂閱者Watcher剩蟀,可以收到屬性的變化通知并執(zhí)行相應的函數(shù),從而更新視圖攻旦。
MVVM.js 整合
class MVVM {
constructor(options) {
// 先把可用的東西掛載到實例上
this.$el = options.el;
this.$data = options.data;
// 判斷有沒有要編譯的模板
if(this.$el) {
// 數(shù)據(jù)劫持 將對象的所有屬性喻旷,都添加 get 和 set 方法
new Observer(this.$data)
// 用數(shù)據(jù)和元素進行模板編譯
new Compile(this.$el, this)
}
}
}
模板的編譯(compile.js)
class Compile {
constructor(el, vm) {
// 判斷el是不是元素節(jié)點
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if(this.el) {
// 1\. 先把真實的DOM移入到內(nèi)存中(fragment),提高性能
let fragment = this.node2fragment(this.el)
// 2\. 編譯 -> 提取想要的元素節(jié)點 v-model 和 文本節(jié)點 {{}}
this.compile(fragment)
// 3\. 把fragment塞回頁面
this.el.appendChild(fragment)
}
}
// 對fragment進行編譯
compile(fragment) {
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach( node => {
// 遍歷fragment的元素節(jié)點
if(this.isElemenrNode(node)) {
// 是元素節(jié)點,需要深度遞歸檢查
this.compile(node)
// 編譯元素
this.compileElement(node)
} else {
// 是文本節(jié)點牢屋,編譯文本
this.compileText(node)
}
})
}
}
將數(shù)據(jù)進行劫持且预,添加get 和 set方法
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
// 要對data數(shù)據(jù)的所有屬性都改為set 和 get 的形式
if(!data || typeof data === 'object') {
return ;
}
// 取出對象 key 值
Object.keys(data).forEach( key => {
// 數(shù)據(jù)劫持
this.defineReactive(data, key, data[key]);
this.observe(data[key]); // 遞歸劫持
})
}
// 定義響應式(數(shù)據(jù)劫持)
defineReactive(obj, key, value) {
let that = this;
Object.defineProperty(obj, key, {
enumerable: true, // 可枚舉
configurable: true, // 屬性能夠被改變
get() { // 取值時調(diào)用的方法
return value;
},
set(newVal) { // 當給data屬性中設置值的時候,更改獲取的屬性的值
if(newVal !== value) {
value = newVal;
that.observe(newVal); // 如果是對象修改繼續(xù)劫持
}
}
})
}
}
觀察者(watcher.js)
最后烙无,給需要變化的元素添加一個觀察者锋谐,通過觀察者監(jiān)聽數(shù)據(jù)變化之后執(zhí)行對應的方法。
class Watcher {
constructor (vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先獲取一下老值
this.value = this.get()
}
getVal() {
// 獲取實例上對應的數(shù)據(jù)
expr = expr.split('.');
return expr.reduce( (prev, next) => {
return prev[next];
}, vm.$data)
}
get() {
let value = this.getVal(this.vm, this.expr);
return value;
}
// 對外暴露的方法截酷,老值和新值比對涮拗,如果變化
update() {
let newVal = this.getVal(this.vm, this.expr);
let oldVal = this.value;
if(newVal !== oldVal) {
this.cb(newVal); // 對應watch的callback
}
}
}
Watch 完成,需要new一下調(diào)用迂苛,首先需要在模板編譯的時候需要調(diào)用三热,在compile.js
:
CompileUtil = {
getVal(vm, expr) {
// 獲取實例上對應的數(shù)據(jù)
expr = expr.split('.');
return expr.reduce( (prev, next) => {
return prev[next];
}, vm.$data)
},
getTextVal(vm, expr) {
// 獲取編譯后文本的結(jié)果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1]);
})
},
text(node, vm, expr) {
// 文本處理
let updateFn = this.updater['textUpdater']
/* Wather觀察者監(jiān)聽 */
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Wathcer(vm, arguments[1], (newVal) => {
// 如果數(shù)據(jù)變化,文本需要重新獲取依賴的數(shù)據(jù)三幻,更新文本中的內(nèi)容
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})
updateFn && updateFn(node, this.getTextVal(vm, expr))
},
setVal(vm, expr, value) {
expr = expr.split('.');
return expr.reduce( (prev, next,currentIndex) => {
if(currentIndex === expr.length - 1) {
return prev[next] = value;
}
return prev[next];
}, vm.$data)
},
model(node, vm, expr) {
// 輸入框處理
let updateFn = this.updater['modelUpdater']
/* Wather觀察者監(jiān)聽 */
// 這里應該加一個監(jiān)控就漾, 數(shù)據(jù)變化,調(diào)用watch的回調(diào)
new Wathcer(vm, expr, (newVal) => {
// 當值變化后會調(diào)用callback念搬,將新值傳遞過來
updateFn && updateFn(node, this.getVal(vm, expr));
})
// 給輸入框加上input事件監(jiān)聽
node.addEventListener('input', (e) => {
let newVal = e.target.value;
this.setVal(vm, expr, newVal)
})
updateFn && updateFn(node, this.getVal(vm, expr));
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value;
},
// 輸入框更新
modelUpdater(node, value) {
node.value = value;
}
}
}
但是此時有一個問題抑堡,Watcher沒有地方調(diào)用,更新函數(shù)不會執(zhí)行朗徊,所以此時需要一個發(fā)布訂閱模式來調(diào)用監(jiān)控者首妖。
class Dep {
constructor() {
// 訂閱的數(shù)組
this.subs = [];
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach( watcher => {
watcher.update()
})
}
}
此時需要修改watcher
里 get()
這個方法:
get() {
Dep.target = this;
let value = this.getVal(this.vm, this.expr)
Dep.target = null;
return value;
}
此時要得到對象的值,需要被數(shù)據(jù)劫持攔截:
defineReactive(obj, key, value) {
let that = this;
let dep = new Dep(); // 每個變化的數(shù)據(jù)爷恳,都會定義一個數(shù)組有缆,這個數(shù)組存放所有更新的操作
Object.defineProperty(obj, key, {
enumerable: true, // 可枚舉
configurable: true,
get() {
// 當取值時調(diào)用的方法
Dep.target && dep.addSub(Dep.target); // 最開始編譯的時候不會執(zhí)行
return value;
},
set(newVal) {
// 當給data屬性中設置值的時候 更改獲取屬性的值
if(newVal != value) {
that.observe(newVal); // 如果是對象繼續(xù)劫持
value = newVal;
dep.notify(); // 通知所有人數(shù)據(jù)更新了
}
}
});
}
此時就完成了輸入框的雙向綁定。不過此時我們?nèi)?shù)據(jù)是以vm.$data.msg
來取到數(shù)據(jù)温亲,理想情況我們是vm.msg
來取到數(shù)據(jù)棚壁,為了實現(xiàn)這樣的形式,我們使用proxy
進行一下代理實現(xiàn):
proxyData(data) {
Object.keys(data).forEach( key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
})
}
這下我們就可以直接通過vm.msg = 'hello'
的形式來進行改變和獲取模板數(shù)據(jù)了铸豁。
歡迎交流指正灌曙,原文地址:https://github.com/hu970804/MVVM