JavaScript数据绑定实现一个简单的 MVVM 库


Posted in Javascript onApril 08, 2016

推荐阅读:

MVVM 是 Web 前端一种非常流行的开发模式,利用 MVVM 可以使我们的代码更专注于处理业务逻辑而不是去关心 DOM 操作。目前著名的 MVVM 框架有 vue, avalon , react 等,这些框架各有千秋,但是实现的思想大致上是相同的:数据绑定 + 视图刷新。出于好奇和一颗愿意折腾的心,我自己也沿着这个方向写了一个最简单的 MVVM 库 ( mvvm.js ),总共 2000 多行代码,指令的命名和用法与 vue 相似,在这里分享一下实现的原理以及我的代码组织思路。

思路整理

MVVM 在概念上是真正将视图与数据逻辑分离的模式,ViewModel 是整个模式的重点。要实现 ViewModel 就需要将数据模型(Model)和视图(View)关联起来,整个实现思路可以简单的总结成 5 点:

实现一个 Compiler 对元素的每个节点进行指令的扫描和提取;

实现一个 Parser 去解析元素上的指令,能够把指令的意图通过某个刷新函数更新到 dom 上(中间可能需要一个专门负责视图刷新的模块)比如解析节点 <p v-show="isShow"></p> 时先取得 Model 中 isShow 的值,再根据 isShow 更改 node.style.display 从而控制元素的显示和隐藏;

实现一个 Watcher 能将 Parser 中每条指令的刷新函数和对应 Model 的字段联系起来;

实现一个 Observer 使得能够对对象的所有字段进行值的变化监测,一旦发生变化时可以拿到最新的值并触发通知回调;

利用 Observer 在 Watcher 中建立一个对 Model 的监听 ,当 Model 中的一个值发生变化时,监听被触发,Watcher 拿到新值后调用在步骤 2 中关联的那个刷新函数,就可以实现数据变化的同时刷新视图的目的。

效果示例

首先粗看下最终的使用示例,与其他 MVVM 框架的实例化大同小异:

<div id="mobile-list">
<h1 v-text="title"></h1>
<ul>
<li v-for="item in brands">
<b v-text="item.name"></b>
<span v-show="showRank">Rank: {{item.rank}}</span>
</li>
</ul>
</div>
var element = document.querySelector('#mobile-list');
var vm = new MVVM(element, {
'title' : 'Mobile List',
'showRank': true,
'brands' : [
{'name': 'Apple', 'rank': 1},
{'name': 'Galaxy', 'rank': 2},
{'name': 'OPPO', 'rank': 3}
]
});
vm.set('title', 'Top 3 Mobile Rank List'); // => <h1>Top 3 Mobile Rank List</h1>

模块划分

我把 MVVM 分成了五个模块去实现: 编译模块 Compiler 、解析模块 Parser 、视图刷新模块 Updater 、数据订阅模块 Watcher 和 数据监听模块 Observer 。流程可以简述为:Compiler 编译好指令后将指令信息交给解析器 Parser 解析,Parser 更新初始值并向 Watcher 订阅数据的变化,Observer 监测到数据的变化然后反馈给 Watcher ,Watcher 再将变化结果通知 Updater 找到对应的刷新函数进行视图的刷新。

上述流程如图所示:

JavaScript数据绑定实现一个简单的 MVVM 库

下文就介绍下这五个模块实现的基本原理(代码只贴重点部分,完整的实现请到我的 Github 翻阅)

1. 编译模块 Compiler

Compiler 的职责主要是对元素的每个节点进行指令的扫描和提取。因为编译和解析的过程会多次遍历整个节点树,所以为了提高编译效率在 MVVM 构造函数内部先将 element 转成一个文档碎片形式的副本 fragment 编译对象是这个文档碎片而不应该是目标元素,待全部节点编译完成后再将文档碎片添加回到原来的真实节点中。

vm.complieElement 实现了对元素所有节点的扫描和指令提取:

vm.complieElement = function(fragment, root) {
var node, childNodes = fragment.childNodes;
// 扫描子节点
for (var i = 0; i < childNodes.length; i++) {
node = childNodes[i];
if (this.hasDirective(node)) {
this.$unCompileNodes.push(node);
}
// 递归扫描子节点的子节点
if (node.childNodes.length) {
this.complieElement(node, false);
}
}
// 扫描完成,编译所有含有指令的节点
if (root) {
this.compileAllNodes();
}
}

vm.compileAllNodes 方法将会对 this.$unCompileNodes 中的每个节点进行编译(将指令信息交给 Parser ),编译完一个节点后就从缓存队列中移除它,同时检查 this.$unCompileNodes.length 当 length === 0 时说明全部编译完成,可以将文档碎片追加到真实节点上了。

2. 指令解析模块 Parser

当编译器 Compiler 把每个节点的指令提取出来后就可以给到解析器解析了。每一个指令都有不同的解析方法,所有指令的解析方法只要做好两件事:一是将数据值更新到视图上(初始状态),二是将刷新函数订阅到 Model 的变化监测中。这里以解析 v-text 为例描述一个指令的大致解析方法:

parser.parseVText = function(node, model) {
// 取得 Model 中定义的初始值 
var text = this.$model[model];
// 更新节点的文本
node.textContent = text;
// 对应的刷新函数:
// updater.updateNodeTextContent(node, text);
// 在 watcher 中订阅 model 的变化
watcher.watch(model, function(last, old) {
node.textContent = last;
// updater.updateNodeTextContent(node, text);
});
}

3. 数据订阅模块 Watcher

上个例子,Watcher 提供了一个 watch 方法来对数据变化进行订阅,一个参数是模型字段 model 另一个是回调函数,回调函数是要通过 Observer 来触发的,参数传入新值 last 和 旧值 old , Watcher 拿到新值后就可以找到 model 对应的回调(刷新函数)进行更新视图了。model 和 刷新函数是一对多的关系,即一个 model 可以有任意多个处理它的回调函数(刷新函数),比如: v-text="title" 和 v-html="title" 两个指令共用一个数据模型字段。

添加数据订阅 watcher.watch 实现方式为:

watcher.watch = function(field, callback, context) {
var callbacks = this.$watchCallbacks;
if (!Object.hasOwnProperty.call(this.$model, field)) {
console.warn('The field: ' + field + ' does not exist in model!');
return;
}
// 建立缓存回调函数的数组
if (!callbacks[field]) {
callbacks[field] = [];
}
// 缓存回调函数
callbacks[field].push([callback, context]);
}

当数据模型的 field 字段发生改变时,Watcher 就会触发缓存数组中订阅了 field 的所有回调。

4. 数据监听模块 Observer

Observer 是整个 mvvm 实现的核心基础,看过有一篇文章说 O.o (Object.observe) 将会引爆数据绑定革命,给前端带来巨大影响力,不过很可惜,ES7 草案已经将 O.o 给废弃了!目前也没有浏览器支持!所幸的是还有 Object.defineProperty 通过拦截对象属性的存取描述符(get 和 set) 可以模拟一个简单的 Observer :

// 拦截 object 的 prop 属性的 get 和 set 方法
Object.defineProperty(object, prop, {
get: function() {
return this.getValue(object, prop);
},
set: function(newValue) {
var oldValue = this.getValue(object, prop);
if (newValue !== oldValue) {
this.setValue(object, newValue, prop);
// 触发变化回调
this.triggerChange(prop, newValue, oldValue);
}
}
});

然后还有个问题就是数组操作 ( push, shift 等) 该如何监测?所有的 MVVM 框架都是通过重写该数组的原型来实现的:

observer.rewriteArrayMethods = function(array) {
var self = this;
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methods = 'push|pop|shift|unshift|splice|sort|reverse'.split('|');
methods.forEach(function(method) {
Object.defineProperty(arrayMethods, method, function() {
var i = arguments.length;
var original = arrayProto[method];
var args = new Array(i);
while (i--) {
args[i] = arguments[i];
}
var result = original.apply(this, args);
// 触发回调
self.triggerChange(this, method);
return result;
});
});
array.__proto__ = arrayMethods;
}

这个实现方式是从 vue 中参考来的,觉得用的很妙,不过数组的 length 属性是不能够被监听到的,所以在 MVVM 中应避免操作 array.length

5. 视图刷新模块 Updater

Updater 在五个模块中是最简单的,只需要负责每个指令对应的刷新函数即可。其他四个模块经过一系列的折腾,把最后的成果交给到 Updater 进行视图或者事件的更新,比如 v-text 的刷新函数为:

updater.updateNodeTextContent = function(node, text) {
node.textContent = text;
}

v-bind:style 的刷新函数:

updater.updateNodeStyle = function(node, propperty, value) {
node.style[propperty] = value;
}

双向数据绑定的实现

表单元素的双向数据绑定是 MVVM 的一个最大特点之一:

JavaScript数据绑定实现一个简单的 MVVM 库

其实这个神奇的功能实现原理也很简单,要做的只有两件事:一是数据变化的时候更新表单值,二是反过来表单值变化的时候更新数据,这样数据的值就和表单的值绑在了一起。

数据变化更新表单值利用前面说的 Watcher 模块很容易就可以做到:

watcher.watch(model, function(last, old) {
input.value = last;
});'

表单变化更新数据只需要实时监听表单的值得变化事件并更新数据模型对应字段即可:

var model = this.$model;
input.addEventListenr('change', function() {
model[field] = this.value;
});‘

其他表单 radio, checkbox 和 select 都是一样的原理。

以上,整个流程以及每个模块的基本实现思路都讲完了,第一次在社区发文章,语言表达能力不太好,如有说的不对写的不好的地方,希望大家能够批评指正!

结语

折腾这个简单的 mvvm.js 是因为原来自己的框架项目中用的是 vue.js 但是只是用到了它的指令系统,一大堆功能只用到四分之一左右,就想着只是实现 data-binding 和 view-refresh 就够了,结果没找这样的 javascript 库,所以我自己就造了这么一个轮子。

虽说功能和稳定性远不如 vue 等流行 MVVM 框架,代码实现可能也比较粗糙,但是通过造这个轮子还是增长了很多知识的 ~ 进步在于折腾嘛!

目前我的 mvvm.js 只是实现了最本的功能,以后我会继续完善、健壮它,如有兴趣欢迎一起探讨和改进~

Javascript 相关文章推荐
Node.js 的异步 IO 性能探讨
Oct 08 Javascript
jQuery简单实现禁用右键菜单
Mar 10 Javascript
JQuery实现可直接编辑的表格
Apr 16 Javascript
使用Chrome浏览器调试AngularJS应用的方法
Jun 18 Javascript
js钢琴按钮波浪式图片排列效果代码分享
Aug 26 Javascript
JavaScript Promise 用法
Jun 14 Javascript
AngularJS 过滤与排序详解及实例代码
Sep 14 Javascript
微信小程序之数据双向绑定与数据操作
May 12 Javascript
AngularJS ionic手势事件的使用总结
Aug 09 Javascript
Angular2 组件交互实例详解
Aug 24 Javascript
Bootstrap popover 实现鼠标移入移除显示隐藏功能方法
Jan 24 Javascript
基于JavaScript的数据结构队列动画实现示例解析
Aug 06 Javascript
jQuery使用Selectator插件实现多选下拉列表过滤框(附源码下载)
Apr 08 #Javascript
JavaScript代码实现左右上下自动晃动自动移动
Apr 08 #Javascript
JS表单验证的代码(常用)
Apr 08 #Javascript
JavaScript事件代理和委托详解
Apr 08 #Javascript
javascript高级选择器querySelector和querySelectorAll全面解析
Apr 07 #Javascript
关于cookie的初识和运用(js和jq)
Apr 07 #Javascript
纯js实现瀑布流布局及ajax动态新增数据
Apr 07 #Javascript
You might like
用 PHP5 轻松解析 XML
2006/12/04 PHP
php将时间差转换为字符串提示
2011/09/07 PHP
Thinkphp框架 表单自动验证登录注册 ajax自动验证登录注册
2016/12/27 PHP
extjs form textfield的隐藏方法
2008/12/29 Javascript
Firefox中通过JavaScript复制数据到剪贴板(Copy to Clipboard 跨浏览器版)
2013/11/22 Javascript
JS小功能(checkbox实现全选和全取消)实例代码
2013/11/28 Javascript
IE6下拉框图层问题探讨及解决
2014/01/03 Javascript
详解Javascript动态操作CSS
2014/12/08 Javascript
jQuery实现长按按钮触发事件的方法
2015/02/02 Javascript
JS数组的常见用法实例
2015/02/10 Javascript
jQuery实现下拉框左右移动(全部移动,已选移动)
2016/04/15 Javascript
jQuery ztree实现动态树形多选菜单
2016/08/12 Javascript
JavaScript 中有关数组对象的方法(详解)
2016/08/15 Javascript
基于js实现checkbox批量选中操作
2016/11/22 Javascript
简单的网页广告特效实例
2017/08/19 Javascript
浅谈Matplotlib简介和pyplot的简单使用——文本标注和箭头
2018/01/09 Python
python下解压缩zip文件并删除文件的实例
2018/04/24 Python
python利用pandas将excel文件转换为txt文件的方法
2018/10/23 Python
python实现雪花飘落效果实例讲解
2019/06/18 Python
pandas条件组合筛选和按范围筛选的示例代码
2019/08/26 Python
使用python获取邮箱邮件的设置方法
2019/09/20 Python
Python导入数值型Excel数据并生成矩阵操作
2020/06/09 Python
5款实用的python 工具推荐
2020/10/13 Python
sublime3之内网安装python插件Anaconda的流程
2020/11/10 Python
Ubuntu配置Pytorch on Graph (PoG)环境过程图解
2020/11/19 Python
HTML5 placeholder属性详解
2016/06/22 HTML / CSS
电台实习生求职信
2014/02/25 职场文书
新闻编辑求职信
2014/07/13 职场文书
医德医风自我评价
2014/09/19 职场文书
文员转正自我鉴定怎么写
2014/09/29 职场文书
早会开场白台词大全
2015/06/01 职场文书
2016年三严三实党课学习心得体会
2016/01/06 职场文书
django学习之ajax post传参的2种格式实例
2021/05/14 Python
基于Java的MathML转图片的方法(示例代码)
2021/06/23 Java/Android
Java GUI编程菜单组件实例详解
2022/04/07 Java/Android
html5+实现plus.io进行拍照和图片等获取
2022/06/01 HTML / CSS