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 相关文章推荐
响应鼠标变换表格背景或者颜色的代码
Mar 30 Javascript
纯css+js写的一个简单的tab标签页带样式
Jan 28 Javascript
jquery form 隐藏的input 选择
Apr 29 Javascript
JavaScript检测浏览器cookie是否已经启动的方法
Feb 27 Javascript
BootStrap的Datepicker控件使用心得分享
May 25 Javascript
JavaScript ES6的新特性使用新方法定义Class
Jun 28 Javascript
小程序实现页面顶部选项卡效果
Nov 06 Javascript
vue的三种图片引入方式代码实例
Nov 19 Javascript
关于vue3.0中的this.$router.replace({ path: '/'})刷新无效果问题
Jan 16 Javascript
node.js爬虫框架node-crawler初体验
Oct 29 Javascript
多种类型jQuery网页验证码插件代码实例
Jan 09 jQuery
JavaScript执行机制详细介绍
Dec 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
在win7中搭建Linux+PHP 开发环境
2014/10/08 PHP
Ajax提交表单时验证码自动验证 php后端验证码检测
2016/07/20 PHP
用javascript实现分割提取页面所需内容
2007/05/09 Javascript
ASP.NET jQuery 实例12 通过使用jQuery validation插件简单实现用户注册页面验证功能
2012/02/03 Javascript
详解强大的jQuery选择器之基本选择器、层次选择器
2012/02/07 Javascript
JS弹出层单纯的绝对定位居中示例代码
2014/02/18 Javascript
JS实现一个按钮的方法
2015/02/05 Javascript
js组件SlotMachine实现图片切换效果制作抽奖系统
2016/04/17 Javascript
JS实现图文并茂的tab选项卡效果示例【附demo源码下载】
2016/09/21 Javascript
JS和canvas实现俄罗斯方块
2017/03/14 Javascript
bootstrap 路径导航 分页 进度条的实例代码
2018/08/06 Javascript
详解如何快速配置webpack多入口脚手架
2018/12/28 Javascript
vue项目前端埋点的实现
2019/03/06 Javascript
详解使用React.memo()来优化函数组件的性能
2019/03/19 Javascript
vue使用Proxy实现双向绑定的方法示例
2019/03/20 Javascript
vue的路由映射问题及解决方案
2019/10/14 Javascript
[10:49]2014国际邀请赛 叨叨刀塔第二期为真正的电竞喝彩
2014/07/21 DOTA
[43:58]DOTA2上海特级锦标赛C组败者赛 Newbee VS Archon第二局
2016/02/27 DOTA
[41:21]夜魇凡尔赛茶话会 第三期02:看图识人
2021/03/11 DOTA
python字典序问题实例
2014/09/26 Python
python结合API实现即时天气信息
2016/01/19 Python
详谈pandas中agg函数和apply函数的区别
2018/04/20 Python
python 求定积分和不定积分示例
2019/11/20 Python
python3下pygame如何实现显示中文
2020/01/11 Python
Python 实现黑客帝国中的字符雨的示例代码
2020/02/20 Python
Pandas —— resample()重采样和asfreq()频度转换方式
2020/02/26 Python
python 基于卡方值分箱算法的实现示例
2020/07/17 Python
python tkinter实现连连看游戏
2020/11/16 Python
多重CSS背景动画实现方法示例
2014/04/04 HTML / CSS
顶级宝石首饰网络零售商:Angara
2016/10/25 全球购物
如何填写个人简历自我评价
2013/12/10 职场文书
高中毕业自我鉴定
2013/12/19 职场文书
医学专业毕业生求职信
2014/06/20 职场文书
个人先进材料范文
2014/12/30 职场文书
2019银行员工个人工作自我鉴定
2019/06/27 职场文书
Python 第三方库 openpyxl 的安装过程
2022/12/24 Python