数据动态绑定的简单实现——基于ES5对象的getter/setter机制

https://zhuanlan.zhihu.com/p/25003235?refer=e-mill%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A

 

双向绑定的简单实现——基于“脏检测”中,我们使用“脏检测”的机制,实现了一个简单的双向绑定计数器。尽管逻辑比较清晰简单,性能也还可以,但每次都遍历DOM节点,也是会有一些性能浪费的。ES5提供了Object.defineProperty与Object.defineProperties两个API,允许我们为对象的属性增设getter/setter函数。利用它们,我们可以很方便地监听数据变更,并且在变更时加入自己的逻辑。

本文我将利用ES5对象的getter/setter机制,模仿Vue的原理,来实现一个简单的数据动态绑定(暂且称为Lue吧)。

语法设计

本次我基于Vue的三个指令:v-model、v-bind和v-click,来实现数据双向绑定(不考虑深层次对象的数据绑定)。DOM依然沿用上篇文章中的结构:

<div id="app">
    <form>
        <input type="text" v-model="count" />
        <button type="button" v-click="increment">increment</button>
    </form>
    <p v-bind="count"></p>
</div>

我们希望使用类似Vue的语法创建一个Lue实例:

var app=new Lue({ el:"#app", data:{ count:0, }, methods:{ increment:function(){ this.count++; } } }) 

开始

开始的开始,我们需要创建一个Lue类:

function Lue(options){ this._init(options); } 

其中包含一个_init初始化函数,定义如下:

Lue.prototype._init=function(options){ this.$options=options; //传入的实例配置 this.$el=document.querySelector(options.el); //实例绑定的根节点 this.$data=options.data; //实例的数据域 this.$methods=options.methods; //实例的函数域 }; 

绑定数据对象的改造

为了实现双向绑定,首先我们需要使用Object.defineProperty对data中的数据对象进行改造,添加getter/setter函数,使其在赋值和取值时能够被监听。

/**对象属性重定义
 * @param key 数据对象名称,本例为"count"
 * @param val 数据对象的值
 */
Lue.prototype.convert=function(key,val){ Object.defineProperty(this.$data,key,{ enumerable:true, configurable:true, get:function(){ console.log(`获取${val}`); return val; }, set:function(newVal){ console.log(`更新${newVal}`); val=newVal; } }) }; 

对data中的数据对象进行遍历调用convert:

//遍历数据域,添加getter/setter
Lue.prototype._parseData=function(obj){ var value; for(var key in obj){ //排除原型链上的属性,仅仅遍历对象本身拥有的属性 if(obj.hasOwnProperty(key)){ value=obj[key]; //如果属性值为对象,则递归解析。本文暂不做实现 //if(typeof value ==='object'){ //this._parseData(value); //} this.convert(key,value); } } }; 

在控制台做如下测试,可以看到已经成功添加了getter与setter:

绑定函数的改造

对于methods域中的函数,由于API要求我们的函数作用域与vm.$data一致,因此需要对其中的函数进行改造:

//对绑定的函数进行改造
//@params {attrVal } "v-click"节点的值,如"alert('hello')"
Lue.prototype._parseFunc=function(attrVal){ var args=/\(.*\)/.exec(attrVal); if(args) { //如果函数带参数,将参数字符串转换为参数数组 args=args[0]; attrVal=attrVal.replace(args,""); args=args.replace(/[\(\)\'\"]/g,'').split(","); } else args=[]; return this.$methods[attrVal].bind(this.$data,args); }; 

上述两个改造流程必须发生在初始化阶段,因此我们需要更改一下之前定义的_init函数:

Lue.prototype._init=function(options){ this.$options=options; //传入的实例配置 this.$el=document.querySelector(options.el); //实例绑定的根节点 this.$data=options.data; //实例的数据域 this.$methods=options.methods; //实例的函数域 this._parseData(this.$data); }; 

至此,对于Lue实例的数据与函数的初始化就完成了。下面需要考虑的是,当数据发生变化时,如何更新DOM元素呢?

最容易想到的一个做法是遍历所有含有v-bind指令的DOM模板,利用相应的绑定数据在内存中拼装成一个fragment,然后再将新的fragment替换旧的DOM结构。但是这个方案存在两个问题:

  • 修改未绑定至DOM的数据时,也会引发DOM的重新渲染。
  • 修改某个数据会导致所有DOM重新渲染,而非只更新数据变动了的相关DOM 。

为了解决这个问题,我们需要引入Directive。

Directive(指令)

Directive的作用就是建立一个DOM节点和对应数据的映射关系。它的定义和原型方法如下:

function Directive(name,el,vm,exp,attr){ this.name=name; //指令名称,例如文本节点,该值设为"text" this.el=el; //指令对应的DOM元素 this.vm=vm; //指令所属Lue实例 this.exp=exp; //指令对应的值,本例如"count" this.attr=attr; //绑定的属性值,本例为"innerHTML" this.update(); //首次绑定时更新 } 
Directive.prototype.update=function(){ //更新DOM节点的预设属性值 this.el[this.attr]=this.vm.$data[this.exp]; }; 
如此便实现了更改某个数据,只触发其对应DOM节点的更新。

下面我们需要考虑的问题是,如何让数据对象的setter在触发时,调用与之相关的directive?

首先我们需要在实例化时建立一个_binding对象,该对象集合了真正与DOM绑定的那些数据对象(data中声明的对象的子集)。因此我们又一次修改_init函数:

Lue.prototype._init=function(options){ this.$options=options; //传入的实例配置 this.$el=document.querySelector(options.el); //实例绑定的根节点 this.$data=options.data; //实例的数据域 this.$methods=options.methods; //实例的函数域 //与DOM绑定的数据对象集合 //每个成员属性有一个名为_directives的数组,用于在数据更新时触发更新DOM的各directive this._binding={}; this._parseData(this.$data); }; 

_binding对象中属性的一个例子如下:

this._binding={ count:{ _directives:[] //该数据对象的相关指令数组 } } 

然后我们改写遍历数据域的函数与绑定数据时的setter函数:

//遍历数据域,添加getter/setter
Lue.prototype._parseData=function(obj){ var value; for(var key in obj){ //排除原型链上的属性,仅仅遍历对象本身拥有的属性 if(obj.hasOwnProperty(key)){ this._binding[key]={ //初始化与DOM绑定的数据对象 _directives:[] }; value=obj[key]; //如果属性值为对象,则递归解析 if(typeof value ==='object'){ this._parseData(value); } this.convert(key,value); } } }; 
set:function(newVal){ console.log(`更新${newVal}`); if(val!==newVal){ val=newVal; //遍历该数据对象的directive并依次调用update binding._directives.forEach(function(item){ item.update(); }) } } 

如此,我们便能实现在数据变更后,进行精准的DOM节点更新。

编译DOM节点

实现双向绑定的最后一步,就是编译带有v-model、v-click与v-bind指令的DOM节点。我们加入一个名为_compile的原型函数:

//解析DOM的指令
Lue.prototype._compile=function(root){ var _this=this; //获取指定作用域下的所有子节点 var nodes=root.children; for(var i=0;i<nodes.length;i++){ var node=nodes[i]; //若该元素有子节点,则先递归编译其子节点 if(node.children.length){ this._compile(node); } if(node.hasAttribute("v-click")) { node.onclick = (function () { var attrVal=nodes[i].getAttribute("v-click"); var args=/\(.*\)/.exec(attrVal); if(args) { //如果函数带参数,将参数字符串转换为参数数组 args=args[0]; attrVal=attrVal.replace(args,""); args=args.replace(/[\(|\)|\'|\"]/g,'').split(","); } else args=[]; return function () { _this.$methods[attrVal].apply(_this.$data,args); } })() } if(node.hasAttribute(("v-model")) && (node.tagName=="INPUT" || node.tagName=="TEXTAREA")){ //如果是input或textarea标签 node.addEventListener("input", (function (key) { var attrVal=node.getAttribute("v-model"); //将value值的更新指令添加至_directives数组 _this._binding[attrVal]._directives.push(new Directive( "input", node, _this, attrVal, "value" )) return function () { _this.$data[attrVal] = nodes

转载于:https://www.cnblogs.com/zaifeng0108/p/7226350.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值