看了一些关于双向绑定的文章,现在来整理一下思路。
首先实现双向绑定有三个步骤:
- 需要一个方法来识别哪一个的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,欢迎查阅~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。