资讯专栏INFORMATION COLUMN

如何实现VM框架中的数据绑定

BlackFlagBin / 3258人阅读

摘要:作者佳杰本文原创,转载请注明作者及出处如何实现框架中的数据绑定一数据绑定概述视图和数据之间的绑定二数据绑定目的不用手动调用方法渲染视图,提高开发效率统一处理数据,便于维护三数据绑定中的元素视图说白了就是中元素的展示数据用于保存数据的引用类型

作者:佳杰

本文原创,转载请注明作者及出处

如何实现VM框架中的数据绑定 一:数据绑定概述
视图(view)和数据(model)之间的绑定
二:数据绑定目的
不用手动调用方法渲染视图,提高开发效率;统一处理数据,便于维护
三:数据绑定中的元素
视图(view):说白了就是html中dom元素的展示
数据(model):用于保存数据的引用类型
四:数据绑定分类
view > model的数据绑定:view改变,导致model改变
model > view的数据绑定:model改变,导致view改变
五:数据绑定实现方法
view > model的数据绑定实现方法
        修改dom元素(input,textarea,select)的数据,导致model产生变化,
        只要给dom元素绑定change事件,触发事件的时候修改model即可,不细讲

model > view的数据绑定实现方法
        1.发布订阅模式(backbone.js用到);
        2.数据劫持(vue.js用到);
        3.脏值检查(angular.js用到);

六:model > view数据绑定demo讲解 (如何实现数据改变,导致UI界面重新渲染)
简易思路 
> 1.通过defineProperty来监控model中的所有属性(对每一个属性都监控)
> 2.编译template生成DOM树,同时绑定dom节点和model(例如
), defineProperty中已经给“model.name”绑定了对应的function, 一旦model.name改变,该funciton就操作上面这个dom节点,改变view 主要js模块:Observer,Compile,ViewModel 1.Observer 用到了发布订阅模式和数据监控,defineProperty用于“监控model", dom元素执行"订阅"操作,给model中 的属性绑定function;model中属性变化的时候,执行"发布"这个操作,执行之前绑定的那个function 源码如下: var Observer = function(opts) { this.id = (opts && opts.id) ? opts.id : +new Date(); this.opts = opts; this.subs = []; //观察者数组 /*this.subs包含了所有观察者,每个观察者的结构如下: { key:"person.age.range",//这个key代表model.person.age.range这个属性 /* 和key绑定的函数数组,每个函数操作一个dom节点, 一个key对应多个dom节点,所以actionList是个function数组; */ actionList:[function(){},function(){}] }*/ } Observer.prototype = { //遍历model中所有的属性,每个属性用defineKey来监控所有属性 monit: function(data, baseUrl) { var me = this; baseUrl = baseUrl || ""; var isTypeMatch = (data && typeof data === "object"); if (isTypeMatch) { Object.keys(data).forEach(function(key) { var base = baseUrl ? (baseUrl + "." + key) : key; me.defineKey(data, key, data[key], baseUrl); //定义自己 me.monit(data[key], base); //递归【定义的是下一层】 }); } }, //用到了Object.defineProperty来定义属性,这样属性改变的时候,就会自动执行里面的set方法 defineKey: function(data, key, val, baseUrl) { var me = this; var base = baseUrl ? (baseUrl + "." + key) : key; Object.defineProperty(data, key, { enumerable: true, configurable: false, get: function() { return val; }, //更新并监控新的值,执行publish函数 set: function(newVal) { if (newVal !== val) { val = newVal; //设置新值需要重新监控 me.monit(newVal, base); //(baseUrl+"."+key)作为观察者模式中的监听的那个key,也可以说是监听的那个事件 me.publish(base, newVal); } } }); }, /* 根据key来执行绑定在这个key上的所有函数,比如说person.age.range这个key, 它变动的时候,publish会执行绑定在person.age.range这个key上所有的function */ publish: function(key, newVal) { (this.subs || []).forEach(function(sub) { if (sub.key == key) { (sub.actionList || []).forEach(function(action) { action(newVal); }); } }); }, //给model中的某个key(例如person.age.range)添加绑定的function subscribe: function(key, callback) { var tgIdx; var hasExist = this.subs.some(function(unit, idx) { tgIdx = (unit.key === key) ? idx : -1; return (unit.key === key) }); if (hasExist) { if (Object.prototype.toString.call(this.subs[tgIdx].actionList)=="[object Array]"){ this.subs[tgIdx].actionList.push(callback); } else { this.subs[tgIdx].actionList = [callback]; } } else { this.subs.push({ key: key, actionList: [callback] }); } }, //取消订阅 remove: function(key) { var removeIdx; this.subs.forEach(function(sub, idx) { removeIdx = sub.key === key ? idx : -1; return sub.key === key }); if (removeIdx !== -1) { this.subs.splice(removeIdx, 1); } }, isObject: function(data) { return data && typeof data === "object" } }; 2.Compile: 模板编译器 var Compile = function(opts) { this.opts = opts; this.data = this.opts.data; this.observer = this.opts.observer; this.regExp = /{{([sS]*)}}/; this.ele = document.createElement("div"); this.ele.innerHTML = opts.template; //渲染页面 this.fragment = this.transToFrament(this.ele); this.travelAllNodes(this.fragment); this.ele.appendChild(this.fragment); }; Compile.prototype = { //把页面上的dom节点转化成文档碎片,防止dom频繁操作影响页面性能 transToFrament: function(el) { var fragment = document.createDocumentFragment(), child; // 将原生节点拷贝到fragment while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }, //遍历文档碎片节点下所有的node节点(用到了函数递归调用),执行compileNode travelAllNodes: function(ele) { this.compileNode(ele); ([].slice.call(ele.childNodes) || []).forEach(function(node) { this.compileNode(node); if (node.childNodes && node.childNodes.length) { this.travelAllNodes(node); } }.bind(this)); }, /*包含功能 1.渲染node节点 2.给key设置callback函数,函数内操作node节点 */ compileNode: function(node) { if (this.isElement(node)) { this.compileElementNode(node); } else if (this.isText(node)) { this.compileTextNode(node); } }, /* 编译element类型的node节点, 需要处理属性绑定v-bind="{{data.name}}"和 事件v-event="{{data.event}}" */ compileElementNode: function(node) { var me = this, nodeAttrs = node.attributes; [].slice.call(nodeAttrs).forEach(function(attr) { var attrName = attr.name; var attrValue = attr.value; var key = me.getKey(attrValue); me.bindKeyToNode(key, attr); attr.value = me.compileString(attrValue); //渲染node }); }, //编译文本类型的node节点,里面放了对应的"{{data.name}}"这种数据格式 compileTextNode: function(ele) { var key = this.getKey(ele.textContent); this.bindKeyToNode(key, ele); ele.textContent = this.compileString(ele.textContent); }, //解析“{{}}”,把它变成对应的数据值 compileString: function(str) { var key = this.getKey(str); return str.replace(this.regExp, this.getValueByKey(key)); }, //绑定key和node节点,key一旦改变,就会触发对应的函数,修改node节点 bindKeyToNode: function(key, node) { if (!!key.trim()) { console.log(key); var nodeType = node.nodeType; var regExp = new RegExp("{{" + key + "}}"); var originTextConetnt; if (nodeType === 2) { originTextConetnt = node.value; } else if (nodeType === 3) { originTextConetnt = node.textContent; } this.observer.subscribe(key, function(newVal) { var tgValue = originTextConetnt.replace(regExp, newVal); if (nodeType === 2) { node.value = tgValue; } else if (nodeType === 3) { node.textContent = tgValue; } }); } }, //从{{name.age.sex}}中获取name.age.sex getKey: function(str) { return str.match(this.regExp) ? str.match(this.regExp)[1] : ""; }, //获取key对应的value值 getValueByKey: function(key) { var arr = key ? key.split(".") : []; var temp = this.data; for (var i = 0; i < arr.length; i++) { if (temp) { temp = temp[arr[i]]; } else { temp = undefined; break } } return temp; }, isElement: function(ele) { return ele.nodeType === 1 ? true : false; }, isText: function(ele) { return ele.nodeType === 3 ? true : false; }, getElement: function() { return this.ele; } } 3.ViewModel:结合Observer与Compile,实现model > view的数据单向绑定 var ViewModel = function(opts) { this.opts = opts; this.data = opts.data; this.wrapper = opts.wrapper; this.template = opts.template; this.Observer = (typeof Observer != undefined) ? Observer : opts.Observer; this.Compile = (typeof Compile != undefined) ? Compile : opts.Compile; this.init(); } ViewModel.prototype = { init: function() { var opts = this.opts; this.observer = new this.Observer(opts); this.observer.monit(this.data); //监控数据变化,数据已经改变了 this.compiler = new this.Compile(Object.assign(opts, { observer: this.observer })); //编译生成节点 if (this.wrapper) { this.wrapper.appendChild(this.compiler.getElement()); } }, get: function() { return this.compiler.getElement(); } };
总结
简单地调用new ViewModel({data:data,template:template}),完成了model和view的绑定,
ViewModel内部大致执行顺序是:

1. 创建数据监控对象this.observer,该对象监控data(监控以后,data的属性改变,
   就会执行defineProperty中的set函数,set函数里面添加了publish发布函数)

2. 创建模板编译器对象this.compiler,该对象编译template,生成最终的dom树,
   并且给每个需要绑定数据的dom节点添加了subscribe订阅函数

3. 最后,改变data里面的属性,会自动触发defineProperty中的set函数,set函数调用publish函数,
   publish会根据key的名称,找到对应的需要执行的函数列表,依次执行所有函数
Git地址
https://github.com/devil1989/databind/
demo
    
    
    
        
        Document
        
        
    
    
        
        
    
    


使用场景说明:
当我们想要修改页面某个元素的信息,但又不想费劲地查找dom元素再去修改元素的值,
这种情况下,可以用demo中的数据绑定,只需修改数据的值,就实现了页面元素重新渲染
请看下面的gif动画中展示的,只要修改data.age和data.name,页面元素就自动重新渲染了

结束语

本demo只是简单实现数据绑定,很多功能并未实现,只是提供一种思路,抛砖引玉;
如果对上述代码中的Observer类的代码不是很理解,可以先了解下观察者模式以及实现原理;
最后,感谢大家的阅读!!

推荐: 翻译项目Master的自述: 1. 干货|人人都是翻译项目的Master 2. iKcamp出品微信小程序教学共5章16小节汇总(含视频) 3. 开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92544.html

相关文章

  • MVVM 中的动态数据绑定

    摘要:要实现最小化刷新,我们要将模板中的每个绑定都收集起来。思考题在最后的实现下,我们把模板改为下面这样虽然很少会有人这样写,就会出现重复的实例,该如何解决这个问题,参考早期源码学习系列之四如何实现动态数据绑定 上一篇文章我们了解了怎样实现一个简单模板引擎。但这个模板引擎只适合静态模板,因为它是将模板整体编译成字符串进行全量替换。如果每次数据改变都进行一次替换,会有两个最主要的问题: 性能...

    Meils 评论0 收藏0
  • 用原生 JS 实现 MVVM 框架2——单向绑定

    摘要:上一篇写了实现框架的一些基本概念本篇用代码来实现一个完整的框架思考假设有如下代码,里面的会和试图中的一一映射,修改的值,会直接引起试图中对应数据的变化如何实现上述呢回想下这篇讲的观察者模式和数据监听主题是什么观察者是什么观察者何时订阅主题主 上一篇写了实现 MVVM 框架的一些基本概念 本篇用代码来实现一个完整的 MVVM 框架 思考 假设有如下代码,data里面的name会和试图中的...

    Zoom 评论0 收藏0
  • Vue基本原理

    摘要:标签添加监听事件文本节点这一步我们操作页面输入框,可以看到以下效果,证明监听事件添加有效。 前言 经过几天的研究,发现学习框架的底层技术,收获颇丰,相比只学习框架的使用要来的合算;如果工作急需,快速上手应用,掌握如何使用短期内更加高效;如果有较多的时间来系统学习,建议研究一下框架的等层技术、原理。 Vue、React、Angular三大框架对比 1、Vue Vue是尤雨溪编写的一个构建...

    firim 评论0 收藏0
  • Vue基本原理

    摘要:标签添加监听事件文本节点这一步我们操作页面输入框,可以看到以下效果,证明监听事件添加有效。 前言 经过几天的研究,发现学习框架的底层技术,收获颇丰,相比只学习框架的使用要来的合算;如果工作急需,快速上手应用,掌握如何使用短期内更加高效;如果有较多的时间来系统学习,建议研究一下框架的等层技术、原理。 Vue、React、Angular三大框架对比 1、Vue Vue是尤雨溪编写的一个构建...

    ytwman 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<