看了一些關於雙向綁定的文章,如今來整理一下思路。
首先實現雙向綁定有三個步驟:html
- 須要一個方法來識別哪個的view被綁定了相應的數據
- 須要監視數據和view的變化
- 須要將全部變化傳播到綁定的對象和對應的view
爲了解決第一個問題,要在對應的dom上添加相應的data-bind-<prop_name>屬性,好比:vue
num: <input type="number" data-bind-num> <div data-bind-num></div>
爲了解決第二個問題,一方面監聽數據改變,須要這樣添加一個set()方法進行監聽:git
const Vue = { data: { num: 0 }, set(key, val) { this.data[key] = val } }
規定經過set(key, val)的方式來修改數據。
另外一邊監聽對應視圖改變就直接監聽input事件。github
爲了解決第三個問題就須要用發佈訂閱模式實現一個事件樞紐:dom
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) }
將視圖層的變化傳播到數據:this
$(`input[data-bind-num]`).on('input', function() { let val = $(this).val() === '' ? 0 : parseInt($(this).val()) Vue.set(key, val) })
至此雙向綁定就實現完成!可是這樣一個個寫事件名和屬性名有點蠢,優化一下spa
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()均可以用直接賦值代替。rest
改變data效果:
修改input效果:
文章相關代碼已經同步到 Github,歡迎查閱~