看了一些關於雙向繫結的文章,現在來整理一下思路。
首先實現雙向繫結有三個步驟:
- 需要一個方法來識別哪一個的view被繫結了相應的資料
- 需要監視資料和view的變化
- 需要將所有變化傳播到繫結的物件和對應的view
為了解決第一個問題,要在對應的dom上新增相應的data-bind-<prop_name>屬性,比如:
num: <input type="number" data-bind-num>
<div data-bind-num></div>
複製程式碼
為了解決第二個問題,一方面監聽資料改變,需要這樣新增一個set()方法進行監聽:
const Vue = {
data: {
num: 0
},
set(key, val) {
this.data[key] = val
}
}
複製程式碼
規定通過set(key, val)的方式來修改資料。
另一邊監聽對應檢視改變就直接監聽input事件。
為了解決第三個問題就需要用釋出訂閱模式實現一個事件樞紐:
const EventHub = {
callbacks: {},
on(eventName, callback){
this.callbacks[eventName] = this.callbacks[eventName] || [];
this.callbacks[eventName].push(callback);
},
emit(eventName, ...rest){
this.callbacks[eventName] = this.callbacks[eventName] || [];
for(let i = 0; i < this.callbacks[eventName].length; i++){
this.callbacks[eventName][i].call(this,...rest);
}
}
}
複製程式碼
一方面將資料層的變化傳播到檢視,需要用特定名稱與dom上的屬性對應:
//觸發事件就修改檢視
eventHub.on(`num:change`, (val) => {
$(`input[data-bind-num]`).val(val)
$(`div[data-bind-num]`).text(val)
})
//通過set()修改data來觸發對應的change事件
set(key, val) {
this.data[key] = val
EventHub.emit(`num:change`, val)
}
複製程式碼
將檢視層的變化傳播到資料:
$(`input[data-bind-num]`).on(`input`, function() {
let val = $(this).val() === `` ? 0 : parseInt($(this).val())
Vue.set(key, val)
})
複製程式碼
至此雙向繫結就實現完成!但是這樣一個個寫事件名和屬性名有點蠢,優化一下
const fn = (prop_name) => {
return {
dataBind: `data-bind-${prop_name}`,//對應dom的data屬性名
eventName: `${prop_name}:change`//對應資料的change事件名稱
}
}
//給所有data繫結change事件,給所有data對應的view繫結input事件
Object.keys(Vue.data).map((key) => {
//data修改改變view
EventHub.on(fn(key).eventName, (val) => {
$(`input[${fn(key).dataBind}]`).val(val)
$(`div[${fn(key).dataBind}]`).text(val)
})
//view改變data
$(`input[${fn(key).dataBind}]`).on(`input`, function() {
let val = $(this).val() === `` ? `` : parseInt($(this).val())
Vue.set(key, val)
})
})
複製程式碼
這樣實現的雙向繫結依賴於用set()來改變資料,而我們都希望通過 vm.property = value
這種方式直接來修改資料,這就需要用到Object.defineProperty()
來劫持各個資料的getter
,setter
。
//給各個資料新增監聽器,用資料劫持替換原先的set(key,value)
const Observer = {
mapProp(obj) {
if(!obj || typeof obj !== `object`) {
return
}
Object.keys(obj).map((key) => {
this.defineReactive(obj, key, obj[key])
})
},
defineReactive(obj, key, val) {
this.mapProp(val)
Object.defineProperty(obj, key, {
enumerable: true, // 可列舉
configurable: false, // 不能再define
get() {
return val
},
set(newVal) {
console.log(`資料 ${key} 從${val}->${newVal}`)
//當資料變化就貴觸發對應的change事件
EventHub.emit(fn(key).eventName, newVal)
val = newVal
}
})
}
}
複製程式碼
這樣只需要呼叫一次Observer.mapProp(Vue.data)
就會監聽所有data,原先的set()都可以用直接賦值代替。
改變data效果:
修改input效果:
文章相關程式碼已經同步到Github,歡迎查閱~