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 相关文章推荐
jquery复选框CHECKBOX全选、反选
Aug 30 Javascript
jQuery :first选择器使用介绍
Aug 09 Javascript
JavaScript判断用户是否对表单进行了修改的方法
Mar 18 Javascript
JavaScript的代码编写格式规范指南
Dec 07 Javascript
jQuery 3 中的新增功能汇总介绍
Jun 12 Javascript
JS实现页面载入时随机显示图片效果
Sep 07 Javascript
Node.js简单入门前传
Aug 21 Javascript
vue axios post发送复杂对象问题
Jun 04 Javascript
8 个有用的JS技巧(推荐)
Jul 03 Javascript
在vue中对数组值变化的监听与重新响应渲染操作
Jul 17 Javascript
vue 实现把路由单独分离出来
Aug 13 Javascript
jQuery实现鼠标拖动图片功能
Mar 04 jQuery
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
国内php原创论坛
2006/10/09 PHP
几种有用的变型 PHP中循环语句的用法介绍
2012/01/30 PHP
通过源码解析Laravel的依赖注入
2018/01/22 PHP
PHP有序表查找之二分查找(折半查找)算法示例
2018/02/09 PHP
用jquery与css打造个性化的单选框和复选框
2010/10/20 Javascript
JS面向对象编程浅析
2011/08/28 Javascript
document.createElement()用法
2013/03/13 Javascript
js验证电话号码与手机支持+86的正则表达式
2014/01/23 Javascript
JQuery与Ajax调用新浪API获取短网址的代码
2014/02/07 Javascript
JavaScript中的公有、私有、特权和静态成员用法分析
2014/11/20 Javascript
javascript验证邮件地址和MX记录的方法
2015/06/16 Javascript
js关于getImageData跨域问题的解决方法
2016/10/14 Javascript
浅析JS中对函数function的理解(基础篇)
2016/10/14 Javascript
浅谈angular2的http请求返回结果的subcribe注意事项
2017/03/01 Javascript
利用JavaScript对中文(汉字)进行排序实例详解
2017/06/18 Javascript
浅谈node.js 命令行工具(cli)
2018/05/10 Javascript
JavaScript中this用法学习笔记
2019/03/17 Javascript
vue路由中前进后退的一些事儿
2019/05/18 Javascript
JS实现贪吃蛇游戏
2019/11/15 Javascript
Vue实现计算器计算效果
2020/08/17 Javascript
Python实现的金山快盘的签到程序
2013/01/17 Python
浅谈tensorflow中几个随机函数的用法
2018/07/27 Python
python实现简单名片管理系统
2018/11/30 Python
Python实现点阵字体读取与转换的方法
2019/01/29 Python
pytorch forward两个参数实例
2020/01/17 Python
HTML5本地存储之Database Storage应用介绍
2013/01/06 HTML / CSS
《秋姑娘的信》教学反思
2014/02/28 职场文书
审计局2014法制宣传日活动总结
2014/11/01 职场文书
优秀少先队员事迹材料
2014/12/24 职场文书
家庭贫困证明
2015/06/16 职场文书
纪委立案决定书
2015/06/24 职场文书
大学毕业典礼致辞
2015/07/29 职场文书
2016党校培训心得体会
2016/01/07 职场文书
一篇文章带你搞懂Python类的相关知识
2021/05/20 Python
java后台调用接口及处理跨域问题的解决
2022/03/24 Java/Android
python通过新建环境安装tfx的问题
2022/05/20 Python