作者:佳杰

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

如何实现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(例如<div id="{{model.name}}"></div>),
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 = /\{\{([\s\S]*)\}\}/;
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

	<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" type="text/css" href="demo.css">
<script type="text/javascript" src="./observe.js"></script>
</head>
<body>
<template id="inner" type="text/template"> <div title="{{des}}">
<div>
<ul id="list">
<li >
<span >age:</span>
<input type="text" name="" value="{{age}}" >
<span id="age" style="float: left;">+</span>
</li>
<li>
<span>name:</span>
<input id="firstName" type="text" name="" value="{{name}}">
</li>
<li><span>{{name}}</span></li>
</ul>
</div> </div>
</template>
<script type="text/javascript">
(function(){
window.data={name:"jeffrey",age:28,des:"测试"};
var vm=new VM({
data:data,
template:document.getElementById("inner").innerHTML
/* wrapper:document.body//可以指定对应容器,也可以不指定容器,
直接获取元素,再手动插入对应dom元素*/
});
document.body.appendChild(vm.get()); document.getElementById("age").addEventListener("click",function(){
data.age++;//只需要修改属性,html就会重新渲染
}); document.getElementById("firstName").addEventListener("keyup",function(e){
data.name=this.value;//只需要修改属性,html就会重新渲染
});
})();
</script>
</body>
</html>

使用场景说明:

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

结束语

本demo只是简单实现数据绑定,很多功能并未实现,只是提供一种思路,抛砖引玉;

如果对上述代码中的Observer类的代码不是很理解,可以先了解下观察者模式以及实现原理;

最后,感谢大家的阅读!!

推荐: 翻译项目Master的自述:

1. 干货|人人都是翻译项目的Master

2. iKcamp出品微信小程序教学共5章16小节汇总(含视频)

3. 开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍

如何实现VM框架中的数据绑定的更多相关文章

  1. javascript基础修炼(9)——MVVM中双向数据绑定的基本原理

    开发者的javascript造诣取决于对[动态]和[异步]这两个词的理解水平. 一. 概述 1.1 MVVM模型 MVVM模型是前端单页面应用中非常重要的模型之一,也是Single Page Appl ...

  2. 搞懂:MVVM模型以及VUE中的数据绑定数据劫持发布订阅模式

    搞懂:MVVM模式和Vue中的MVVM模式 MVVM MVVM : model - view - viewmodel的缩写,说都能直接说出来 model:模型,view:视图,view-Model:视 ...

  3. 【转】【译】JavaScript魔法揭秘--探索当前流行框架中部分功能的处理机制

    推荐语: 今天推荐一篇华为同事的同事翻译的一篇文章,推荐的主要原因是作为一个华为员工居然晚上还能写文章,由不得小钗不佩服!!! 其中的jQuery.angular.react皆是十分优秀的框架,各有特 ...

  4. 在WPF的MVVM框架中获取下拉选择列表中的选中项

    文章概述: 本演示介绍怎样在WPF的MVVM框架中.通过数据绑定的方式获取下拉列表中的选中项.程序执行后的效果例如以下图所看到的: 相关下载(代码.屏幕录像):http://pan.baidu.com ...

  5. 【小家Spring】聊聊Spring中的数据绑定 --- BeanWrapper以及内省Introspector和PropertyDescriptor

    #### 每篇一句 > 千古以来要饭的没有要早饭的,知道为什么吗? #### 相关阅读 [[小家Spring]聊聊Spring中的数据转换:Converter.ConversionService ...

  6. 【小家Spring】聊聊Spring中的数据绑定 --- DataBinder本尊(源码分析)

    每篇一句 唯有热爱和坚持,才能让你在程序人生中屹立不倒,切忌跟风什么语言或就学什么去~ 相关阅读 [小家Spring]聊聊Spring中的数据绑定 --- 属性访问器PropertyAccessor和 ...

  7. Vue基础系列(三)——Vue模板中的数据绑定语法

    写在前面的话: 文章是个人学习过程中的总结,为方便以后回头在学习. 文章中会参考官方文档和其他的一些文章,示例均为亲自编写和实践,若有写的不对的地方欢迎大家和我一起交流. VUE基础系列目录 < ...

  8. 【案例分享】在 React 框架中使用 SpreadJS 纯前端表格控件

    [案例分享]在 React 框架中使用 SpreadJS 纯前端表格控件 本期葡萄城公开课,将由国电联合动力技术有限公司,资深前端开发工程师——李林慧女士,与大家在线分享“在 React 框架中使用 ...

  9. 制作类似ThinkPHP框架中的PATHINFO模式功能

    一.PATHINFO功能简述 搞PHP的都知道ThinkPHP是一个免费开源的轻量级PHP框架,虽说轻量但它的功能却很强大.这也是我接触学习的第一个框架.TP框架中的URL默认模式即是PathInfo ...

随机推荐

  1. Linux企业运维人员必备150个命令汇总

    命令 功能说明 线上查询及帮助命令(2个) man 查看命令帮助,命令的词典,更复杂的还有info,但不常用. help 查看Linux内置命令的帮助,比如cd命令. 文件和目录操作命令(18个) l ...

  2. ViewPager+Fragment 懒加载

    转载于: 作者:尹star链接:http://www.jianshu.com/p/c5d29a0c3f4c來源:简书   ViewPager+Fragment的模式再常见不过了,以国民应用微信为例,假 ...

  3. .net WCF简单实例

    最近看到网上招聘有许多都需要WCF技术的人员,我之前一直没接触过这个东西,以后工作中难免会遇到,所谓笨鸟先飞,于是我就一探究竟,便有了这边文章.由于是初学WCF没有深入研究其原理,只是写了一个demo ...

  4. AutoFac+ASP.NetMvc,AspNet.Core

    ASP.Net.Mvc 引用 install-package autofac install-package Mvc5 //创建一个用于注册的对象 ContainerBuilder builder = ...

  5. 6.python内置函数

    1. abs() 获取绝对值 >>> abs(-10) 10 >>> a = -10 >>> a.__abs__() 10 2. all()   ...

  6. 页面重绘(repaint)和回流(reflow)

    前言 页面显示到浏览器上的过程: 1.1.生成一个DOM树. 浏览器将获取到的HTML代码解析成1个DOM树,包含了所有标签,包括display:none和动态添加的节点. 1.2.生成样式结构体. ...

  7. [array] leetcode - 54. Spiral Matrix - Medium

    leetcode-54. Spiral Matrix - Medium descrition GGiven a matrix of m x n elements (m rows, n columns) ...

  8. 深谈auto变量

    1.c++中有一个关键字auto,c语言也有这么一个关键字,但是两者的意义大不相同. 2.c++中用auto定义的变量自动匹配赋值号右边的值的类型,具有自动匹配类型的作用,而c语言中auto只是声明一 ...

  9. UVA 11825 Hackers' Crackdown

    题目大意就是有一个图,破坏一个点同时可以破坏掉相邻点.每个点可以破坏一次,问可以完整破坏几次,点数=16. 看到16就想到状压什么的. 尝试设状态:用f[i]表示选的情况是i(一个二进制串),至少可以 ...

  10. Spring_Spring与IoC_Bean的装配

    一.Bean的装配      bean的装配,即Bean对象的创建,容器根据代码要求来创建Bean对象后再传递给代码的过程,称为Bean的装配.  二.默认装配方式 代码通过getBean()方式从容 ...