博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
数据动态绑定的简单实现——基于ES5对象的getter/setter机制
阅读量:4085 次
发布时间:2019-05-25

本文共 5949 字,大约阅读时间需要 19 分钟。

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

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

 

语法设计

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

我们希望使用类似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/setterLue.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/setterLue.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

改写Lue的_init原型方法,使其在初始化时即对DOM进行编译:

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);    this._compile(this.$el);                //编译DOM节点};

至此,我们便实现了一个基于getter/setter,模仿Vue的简单的双向绑定。整个体系搭建并不复杂,只需要注意其中三个核心的部分:getter/setter,Directive以及binding。细心的读者不难发现,在本文的实现中,如果线程频繁触发数据变更,会导致DOM频繁更新,非常影响性能。在真正的生产环境中,DOM的更新不是数据变更后立马更新,而是被加入到批处理队列,等待主线程运行完后再进行批处理。

整个Lue实例结构如下:

完整代码

推荐阅读

 

转载

你可能感兴趣的文章
FMS 客户端带宽计算、带宽限制
查看>>
在线视频聊天(客服)系统开发那点事儿
查看>>
SecurityError Error 2148 SWF 不能访问本地资源
查看>>
Flex4的可视化显示对象
查看>>
Flex:自定义滚动条样式/隐藏上下箭头
查看>>
烈焰SWF解密
查看>>
Qt 静态编译后的exe太大, 可以这样压缩.
查看>>
3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇
查看>>
乘法逆元
查看>>
STL源码分析----神奇的 list 的 sort 算法实现
查看>>
Linux下用math.h头文件
查看>>
Linux中用st_mode判断文件类型
查看>>
Ubuntu修改host遇到unable to resolve host
查看>>
路由选择算法
查看>>
Objective-C 基础入门(一)
查看>>
Objective-C 基础入门(三) 读写文件与回调
查看>>
C++ STL标准库与泛型编程(一)概述
查看>>
C++ STL标准库与泛型编程(四)Deque、Queue、Stack 深度探索
查看>>
C++ STL标准库 算法
查看>>
JVM内存模型_Minor GC笔记
查看>>