作者:佳杰

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

如何实现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. JAVA入门[6]-Mybatis简单示例

    初次使用Mybatis,先手写一个hello world级别的例子,即根据id查询商品分类详情. 一.建表 create table Category ( Id INT not null, Name ...

  2. java开发收藏

    一.java工具 1.jenkins 项目集成工具 2.launch4j java打包成exe工具 二.json库 1.jsoniter 比以下都快 2.dsljson 3.fastjson 4.gs ...

  3. Java并发编程实践读书笔记(1)线程安全性和对象的共享

    2.线程的安全性 2.1什么是线程安全 在多个线程访问的时候,程序还能"正确",那就是线程安全的. 无状态(可以理解为没有字段的类)的对象一定是线程安全的. 2.2 原子性 典型的 ...

  4. 《ActiveMQ in Action》【PDF】下载

    内容介绍TheApache ActiveMQ message broker is an open source implementation ofthe Java Message Service sp ...

  5. Oracle数据库(一)概述、基础与简单操作

    数据库: 数据库(Database)是按照数据结构来组织.存储和管理数据的仓库. 数据库分类: 关系型数据库 非关系型数据库 数据库 类型 特性 优点 缺点 关系型数据库 SQLite.Oracle. ...

  6. iOS控制器跳转动画

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 MyViewController *myVC = [[MyViewController alloc]init];  //创建动画  C ...

  7. Keras的安装与配置

    Keras是由Python编写的基于Tensorflow或Theano的一个高层神经网络API.具有高度模块化,极简,可扩充等特性.能够实现简易和快速的原型设计,支持CNN和RNN或者两者的结合,可以 ...

  8. ffmpeg常用命令---转

    1.分离视频音频流 ffmpeg -i input_file -vcodec copy -an output_file_video //分离视频流 ffmpeg -i input_file -acod ...

  9. NumPy学习笔记 一

    NumPy学习笔记 一 <NumPy学习笔记>系列将记录学习NumPy过程中的动手笔记,前期的参考书是<Python数据分析基础教程 NumPy学习指南>第二版.<数学分 ...

  10. 编译TensorFlow源码

      编译TensorFlow源码 参考: https://www.tensorflow.org/install/install_sources https://github.com/tensorflo ...