介绍
双向绑定相信大家都耳熟能详,即ViewModel中的data部分和View之间的双向关系,数据可以驱动视图,视图页面的改变也可以反向来更改数据。这篇文章先做一个利用原生JS与订阅发布者模式来实现的简单双向绑定
最后达到的效果be like:
思想和代码整理于https://www.html-js.com/article/Study-of-twoway-data-binding-JavaScript-talk-about-JavaScript-every-day
原理
订阅发布者模式这里就不在过多赘述,简单来说定义了对象间一种一对多的关系,让多个观察者监听同一主题对象,当一个对象改变时,所有依赖于它的对象都将得到通知。
订阅者会把自己的事件处理函数都注册到一个统一的调度中心,当发布者发布消息后,调度中心会调用订阅了对应事件的订阅者的事件处理函数。
那么问题来了,谁是订阅者?谁是发布者?
当JS对象的数据发生改变时,依赖这个数据的视图也会发生变化,因此这个时候JS对象数据变化就是作为发布者,视图就是订阅者。
同时当用户触发一些事件比如input事件的时候,会改变JS对象里的数据,此时input事件就是发布者,JS对象就是订阅者,其中关系如图所示
…
talk is cheap,show me the code,ok接下来是代码
代码解析
首先我们要有调度中心,去收集所有依赖
let pubSub = {
callbacks:{},
on:function(msg,callback){
if(!this.callbacks[msg]) this.callbacks[msg] = [];
this.callbacks[msg].push(callback);
},
publish:function(msg){
let functionList = this.callbacks[msg];
if(!functionList) return;
functionList.forEach(f => {
f.apply(this,arguments);
})
}
}
这里pubSub 就是调度中心,callbacks存储所有订阅者所订阅消息对应的回调,on函数第一个参数msg即订阅者订阅的消息,第二个参数callback即是当发布者发布事件时所要执行的回调。publish函数自然也就是发布者发布事件时执行所存储对应回调
我们先从视图的角度看其所订阅的事件,我们希望做到的效果类似于
<input bind-xx = 'a'/>
该input元素的value双向绑定了xx对象的a值,因此该元素订阅了一个事件,当JS对象值发生变化时会去改变该元素的value,我们需要一个标识去找有哪些dom元素绑定了我们JS对象的里的属性,这个简单使用document.querySelectorAll即可
let key = 'bind-' + object_id;
let msg = 'change:' + object_id;
pubSub.on(msg,(_,prop_name,new_val) => {
let doms = document.querySelectorAll("[" + key + "=" + prop_name + "]"),tag_name;
for(let dom of doms)
{
tag_name = dom.tagName.toLowerCase();
if(tag_name == 'select' || tag_name == 'input' || tag_name == 'textarea')
dom.value = new_val;
else
dom.innerHTML = new_val;
}
})
这里key即是我们元素的属性名,标识该元素绑定了JS对象属性,msg即是事件的标识。pubSub.on函数像调度中心注册事件对应的回调函数,回调函数里prop_name表示绑定object_id对象的哪个属性。这个回调函数逻辑也很简单,就是在JS对象属性发生变化后,dom元素作为订阅者,调度中心触发该回调,更改dom元素的value
结合之前的分析dom元素除了是订阅者,也是发布者,当用户执行触发一些事件如input事件时候会发让调度中心发布事件
document.addEventListener('input',event =>{
let tgt = event.target;
let prop_name = tgt.getAttribute(key);
if(prop_name && prop_name!='')
//prop_name表示绑定object_id对象的哪个属性
pubSub.publish(msg,prop_name,tgt.value);
})
这里的key即是前面定义的那个key。
视图元素相关的订阅发布就是上述这两部分内容,接下来看JS对象这边的订阅与发布
function User(user_id){
let binder = new DataBinder(user_id);
let user = {
attributes: {},
set:function(prop_name,val){
this.attributes[prop_name] = val;
binder.publish("change:"+user_id,prop_name,val,this);
},
get:function(prop_name){
return this.attributes[prop_name];
}
}
binder.on("change:"+user_id,(_,prop_name,val,init) => {
if(init != user) //避免循环调用
user.set(prop_name,val)
})
return user;
}
这里dataBinder构造函数会返回我们之前定义好的pubSub。
我们给user对象手动定义了set和get函数,这里虽然不是数据劫持但作用还是类似的,我们使用user.set(prop_name, value)和user.get(prop_name)来实现属性的赋值与获取。set函数除了赋值以外会还作为一个发布者让调度中心触发对应回调,这是理所应当的,使用set函数即是改变了属性值,所有依赖该属性的元素都应受到影响
同时这里也应订阅属性变化,当视图触发input事件时,执行对应回调来改变JS对象属性,注意要避免循环调用,本身的set函数不应触发该回调,否则就死循环了
这样我们就基于原生JS和订阅发布者模式实现了一个很简单的数据双向绑定,但这种方法使用obj.set(prop_name, value)改变值,同时要在user对象内部手动定义set、get函数,过于耦合。
我们更希望能直接通过obj[prop_name] = value 这种方式去取值并且希望能更加解耦,因此结合数据劫持去做会更好,这也就是Vue1.x和2.x里数据双向绑定的基本思想
本文代码已上传至https://github.com/voiddiddvue/data-binding