基于原生JS和订阅发布者模式的数据双向绑定

介绍

双向绑定相信大家都耳熟能详,即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对象就是订阅者,其中关系如图所示

原生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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值