当前位置: 首页 > 后端技术 > Node.js

搭建一个使用Proxy和Reflect实现双向数据绑定的微框架(基于ES6)

时间:2023-04-03 23:28:18 Node.js

写在前面:本文介绍如何使用Proxy和Reflect实现双向数据绑定。我是Vue的早期玩家,写这个小框架我当时并没有参考Vue等源码。之前了解过其他的实现,但是没有直接参考其他的代码。如有雷同,纯属巧合。代码下载地址:这里下载概览。关于Proxy和Reflect的资料推荐阮老师的教程:http://es6.ruanyifeng.com/这里就不做过多介绍了。双向数据绑定的实现方式有很多种,也可以参考本专栏之前的其他实现方式。我之所以选择使用Proxy和Reflect,是因为它可以节省大量的代码和简化逻辑,让我可以把更多的经验放在其他内容的构建上。另一方面,这个项目直接基于ES6。使用这些内容也符合面向未来的JS编程规范。第三点是最后一点。由于这个小框架是我自己在一个安静的下午在PolarBear咖啡馆写的,所以我暂且将它命名为Polar。希望以后能继续完善这个小框架,给它添加更多有趣的功能。首先我们可以看到整体的功能演示:一个gif动图,如果看不到请点击【链接在这里】代码分析我们要做这么一个小框架,核心是监听变化的数据,并在数据发生变化时执行一些操作来保持数据的一致性。我的想法是这样的:把所有的数据信息都放在一个属性对象(this._data)中,然后用Proxy为这个属性对象包装set。在代理函数中,我们更新属性对象的具体内容,同时通知所有的监听器或者,然后返回一个新的代理对象(this.data),之后我们再对新的代理对象进行操作。对于输入等表单,我们需要监听输入事件,直接在回调函数中设置我们代理的数据对象,从而触发我们的代理函数。我们还应该支持事件机制。这里我们以最常用的点击方式为例。让我们开始第一部分。我们希望以后在使用这个库的时候,可以这样调用:

姓名:{{name}}年龄:{{age}}
备注:{{note}}
button1
是,它类似于Vue,所以这种调用方式我们应该很熟悉。我们需要创建一个Polar类,这个类的构造函数应该执行一些初始化操作:constructor(configs){this.root=this.el=document.querySelector(configs.el);this._data=configs.data;这。_data.__bindings={};//创建代理对象this.data=newProxy(this._data,{set});this.methods=configs.methods;this._compile(this.root);}其中一部分内容是根据属性直接赋值我们传入的configs,另一部分是创建代理对象的过程。最后一个_compile方法可以理解为私有初始化方法。其实我把剩下的内容几乎都放在了_compile方法里,方便理解,只是后面可能需要改。我们还看不出我们agent的set怎么写,因为这个时候我们还要继续梳理思路:假设我们像
name:{{name}}
name:{{name}}
,这个时候我们需要做什么,或者说,我们如何让dom节点和数据对应起来,随着数据的变化而变化。请参阅上面的__bindings。这个对象用来存储所有绑定的dom节点信息,__bindings本身就是一个对象,每一个绑定到对应dom节点的数据名就是它的属性,对应一个数组,数组中的每一个内容就是一个binding这样,在我们写的set代理函数中,我们可以通过一个一个的调用来更新内容:dataSet.__bindings[key].forEach(function(item){//dosomethingtoupdate...});我这里创建了构造调用的函数,用于创建存储绑定信息的对象:functionDirective(el,polar,attr,elementValue){this.el=el;//元素本身domNodethis.polar=polar;//对应的polar实例this.attr=attr;//元素的绑定属性值,比如如果是文本节点,可以是nodeValuethis.el[this.attr]=this.elementValue=elementValue;//Initialization}这样我们的set可以这样写:var数据集=接收器||目标;dataSet.__bindings[key].forEach(function(item){item.el[item.attr]=item.elementValue=value;});returnresult;}接下来可能还有一个问题:我们的{{name}}其实只是一个节点的一部分,不是节点,我们是不是也可以这样写:
name:{{name}}age:{{age}}
?关于这两个问题,前者的答案是我们将{{name}}换成文本节点,而为了处理后者,我们需要将两个绑定数据之间和前后的内容改成新的文本节点,然后这些文本节点组成一个文本部分点串(这里多说一句,html5的normalize方法可以将多个文本节点合并为一个,如果不小心调用了,那我们的程序就GG了)所以我们先在_compile函数中:var_this=this;varnodes=root.children;varbindDataTester=newRegExp("{{(.*?)}}","ig");for(leti=0;i的情况这其实是一条指令,我们只需要在识别到这条指令后做一些处理即可:if(node.hasAttribute(("p-model"))&&node.tagName.toLocaleUpperCase()=="INPUT"||node.tagName.toLocaleUpperCase()=="TEXTAREA"){node.addEventListener("input",(function(){varattributeValue=node.getAttribute("p-model");if(_this._data.__bindings[attributeValue])_this._data.__bindings[attributeValue].push(newDirective(node,_this,"value",_this.data[attributeValue]));else_this._data.__bindings[attributeValue]=[newDirective(node,_this,"value",_this.data[attributeValue])];returnfunction(event){_this.data[attributeValue]=event.target.value}})());}注意上面的调用在创建IIFE时,只返回函数的一小部分实际上绑定到该函数。最后,我们处理事件的情况:button1其实这样比处理p-model要简单,但是为了支持函数的情况parameters,我们已经对传入的Parameters进行了处理,另外,我其实一直都是把event作为参数传递,这可能不是一个好的做法,因为在使用的时候要多加注意。if(node.hasAttribute("p-click")){node.addEventListener("click",function(){varattributeValue=node.getAttribute("p-click");varargs=/\(.*\)/.exec(attributeValue);//允许参数if(args){args=args[0];attributeValue=attributeValue.replace(args,"");args=args.replace(/[\(\)\'\"]/g,'').split(",");}elseargs=[];返回函数(事件){_this.methods[attributeValue].apply(_this,[event,...args]);}}());}现在我们把所有的代码都分析完了,是不是很爽啊,代码不包括注释大概100行左右,所有的源码都可以在这里下载,当然这不能算作一个框架,但是可以学习,如果这学期有时间,我们会继续完善,欢迎大家一起讨论。一起学习,一起进步。做技术应该直截了当。有问题欢迎指出~末尾第三点:我还是学生,做这些内容只是出于兴趣。