2. 数据绑定

2.1 数据绑定的原理

数据绑定是一种很便捷的特性,一些RIA框架带有双向绑定功能,比如Flex和Silverlight,当某个数据发生变更时,所绑定的界面元素也发生变更,当界面元素的值发生变化时,数据也跟着变化,这种功能在处理表单数据的填充和收集时,是非常有用的。

在HTML中,原生是没有这样的功能的,但有些框架做到了,它们是怎么做到的呢?我们来做个简单的试试,顺便探讨一下其中原理。

先看数据到界面上的的绑定,比如:

<input vm-value="name"/>
var person = {
name: "Tom"
};

如果我们给name重新赋值,person.name = "Jerry",怎么才能让界面得到变更?

从直觉来说,我们需要在name发生改变的时候,触发一个事件,或者调用某个指定的方法,然后才好着手做后面的事情,比如:

var person = {
name: "Tom",
setName: function(newName) {
this.name = newName;
//do something
}
};

这样我们可以在setName里面去给input赋值。推而广之,为了使得实体包含的多个属性都可以运作,可以这么做:

var person = {
name: "Tom",
gender: 5
set: function(key, value) {
this[key] = value;
//do something
}
};

或者合并两个方法,只判断是否传了参数:

Person.prototype.name = function(value) {
if (arguments.length == 0) {
return this._name;
}
else {
this._name = value;
}
}

这种情况下,赋值的时候就是person.name("Tom"),取值的时候就是var name = person.name()了。

有一些框架是通过这种方式来变通实现数据绑定的,对数据的写入只能通过方法调用。但这种方式很不直接,我们来想点别的办法。

在C#等一些语言里,有一种东西叫做存取器,比如说:

class Person
{
private string name; public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
}

用的时候,person.Name = "Jerry",就会调用到set里,相当于是个方法。

这一点非常好,很符合我们的需要,那JavaScript里面有没有类似存取器的特性呢?老早以前是没有的,但现在有了,那就是Object.defineProperty,它的第三个参数就是可选的存取函数。比如说

var person = {}; 

// Add an accessor property to the object.
Object.defineProperty(person, "name", {
set: function (value) {
this._name = value;
//do something
},
get: function () {
return this._name;
},
enumerable: true,
configurable: true
});

赋值的时候,person.name = "Tom",取值的时候,var name = person.name,简直太美妙了。注意这里define的时候,是定义在实例上的,如果想要定义到类型里面,可以在构造器里面定义。

现在我们从数据到DOM的绑定可以解决掉了,至少我们能够在变量被更改的时候去做一些自己的事情,比如查找这个属性被绑定到哪些控件了,然后挨个对其赋值。框架怎么知道属性被绑定到哪些控件了呢?这个直接在第二部分的实现过程中讨论。

再看控件到数据的绑定,这个其实很好理解。无非就是给控件添加change之类的事件监听,在这里面把关联到的数据更新掉。到这里,我们在原理方面已经没有什么问题了,现在开始准备把它写出来。

2.2 数据绑定的实现

我们的框架启动之后,要先把前面所说的这种绑定关系收集起来,这种属性会分布于DOM的各个角落,一个很现实的做法是,递归遍历界面的每个DOM节点,检测该属性,于是我们代码的结构大致如下所示。

function parseElement(element) {
for (var i=0; i<element.attributes.length; i++) {
parseAttribute(element.attributes[i]);
} for (var i=0; i<element.children.length; i++) {
parseElement(element.children[i]);
}
}

但是我们这时候面临一个问题,比如你的输入框绑定在name变量上,这个name应该从属于什么?它是全局变量吗?

我们在开始做这个框架的时候强调了一个原则:业务模块不允许定义全局变量,框架内部也尽量少有全局作用域,到目前为止,我们只暴露了thin一个全局入口,所以在这里不能破坏这个原则。

因此,我们要求业务开发人员去定义一个视图模型,把变量包装起来,所包装的不限于变量,也可以有方法。比如下面,我们定义了一个实体叫Person,带两个变量,两个方法,后面我们来演示一下怎么把它们绑定到HTML界面。

thin.define("Person", [], function() {
function Person() {
this.name = "Tom";
this.age = 5;
} Person.prototype = {
growUp: function() {
this.age++;
}
}; return Person;
});

模型方面都准备好了,现在来看界面:

<div vm-model="Person">
<input type="text" vm-value="name"/>
<input type="text" vm-value="age"/>
<input type="button" vm-click="growUp" value="Grow Up"/>
</div>

为了使得结构更加容易看,我们把界面的无关属性比如样式之类都去掉了,只留下不能再减少的这么一段。现在我们可以看到,在界面的顶层定义一个vm- model属性,值为实体的名称。两个输入框通过vm-value来绑定到实例属性,vm-init绑定界面的初始化方法,vm-click绑定按钮的点 击事件。

好了,现在我们可以来扫描这个简单的DOM结构了。想要做这么一个绑定,首先要考虑数据从哪里来?在绑定name和code属性之前,毫无疑问,应当先实例化一个Person,我们怎么才能知道需要把Person模块实例化呢?

当扫描到一个DOM元素的时候,我们要先检测它的vm-model属性,如果有值,就取这个值来实例化,然后,把这个值一直传递下去,在扫描其他属 性或者下属DOM元素的时候都带进去。这么一来,parseElement就变成一个递归了,于是它只好有两个参数,变成了这样:

function parseElement(element, vm) {
var model = vm; if (element.getAttribute("vm-model")) {
model = bindModel(element.getAttribute("vm-model"));
} for (var i=0; i<element.attributes.length; i++) {
parseAttribute(element, element.attributes[i], model);
} for (var i=0; i<element.children.length; i++) {
parseElement(element.children[i], model);
}
}

看看我们打算怎么来实例化这个模型,这个bindModel方法的参数是模块名,于是我们先去use一下,从工厂里生成出来,然后new一下,先这么return出去吧。

function bindModel(modelName) {
thin.log("model" + modelName); var model = thin.use(modelName, true);
var instance = new model(); return instance;
}

现在我们开始关注parseAttribute函数,可能的attribute有哪些种类呢?我列举了一些很常用的:

init,用于绑定初始化方法

click,用于绑定点击

value,绑定变量

enable和disable,绑定可用状态

visible和invisible,绑定可见状态

然后就可以实现我们parseAttribute函数了:

function parseAttribute(element, attr, model) {
if (attr.name.indexOf("vm-") == 0) {
var type = attr.name.slice(3); switch (type) {
case "init":
bindInit(element, attr.value, model);
break;
case "value":
bindValue(element, attr.value, model);
break;
case "click":
bindClick(element, attr.value, model);
break;
case "enable":
bindEnable(element, attr.value, model, true);
break;
case "disable":
bindEnable(element, attr.value, model, false);
break;
case "visible":
bindVisible(element, attr.value, model, true);
break;
case "invisible":
bindVisible(element, attr.value, model, false);
break;
case "element":
model[attr.value] = element;
break;
}
}

注意到最后还有个element类型,本来可以不要这个,但我们考虑到将来,一切都是组件化的时候,界面上打算不写id,也不依靠选择器,而是用某个标志来定位元素,所以加上了这个,文章最后的示例中使用了它。

这么多绑定,不打算都讲,用bindValue函数来说明一下吧:

function bindValue(element, key, vm) {
thin.log("binding value: " + key); vm.$watch(key, function (value, oldValue) {
element.value = value || "";
}); element.onkeyup = function () {
vm[key] = element.value;
}; element.onpaste = function () {
vm[key] = element.value;
};
}

我们假定每个模型实例上带有一个$watch方法,用于监控某变量的变化,可以传入一个监听函数,当变量变化的时候,自动调用这个函数,并且把新旧两个值传回来。

在这个代码里,我们使用$watch方法给传入的key添加一个监听,监听器里面给监听元素赋值。我们这里偷懒了一下,假定所有的绑定元素都是输入 框,所以直接给element.value设置值,为了防止值为空导致显示undefined,把值跟空字符串用短路表达式做了个转换。

接下来,也对element的几个可能导致值变化的事件进行了监听,在里面把模型上对应的值更新掉。这样双向绑定就做好了。

然后回头来看$watch的实现。很显然这里也要一个map,我们给它取名为$watchers,存放属性的绑定关系,对于每个属性,它的值需要保存一份,供getter获取,同时还有一个数组,存放了该属性绑定的处理函数。当属性发生变更的时候,去挨个把它们调用一下。

var Binder = {
$watch: function (key, watcher) {
if (!this.$watchers[key]) {
this.$watchers[key] = {
value: this[key],
list: []
}; Object.defineProperty(this, key, {
set: function (val) {
var oldValue = this.$watchers[key].value;
this.$watchers[key].value = val; for (var i = 0; i < this.$watchers[key].list.length; i++) {
this.$watchers[key].list[i](val, oldValue);
}
}, get: function () {
return this.$watchers[key].value;
}
});
} this.$watchers[key].list.push(watcher);
}
};

但是vm怎么就有$watcher呢,每个地方都去判断一下非空然后再去创建其实挺麻烦的,所以,这个属性我们可以直接在实例化模型的时候创建出来。

function bindModel(name) {
thin.log("binding model: " + name); var model = thin.use(name, true);
var instance = new model().extend(Binder);
instance.$watchers = {}; return instance;
}

看看这里的写法,为什么$watchers要额外设置,而$watch就可以放在Binder里面来extend呢?

先解释extend干了什么,它做的是一个对象的浅拷贝,也就是说,把Binder的属性和方法都复制给了创建出来的model实例,注意,这个所 谓的复制,如果是简单类型,那确实复制了,如果是引用类型,那复制的其实只是一个引用,所以如果$watchers也放在Binder里,不同的 instance就共享一个$watchers,逻辑就是错误的。那为什么$watcher又可以放在这里复制呢?因为它是函数,它的this始终指向当 前的执行主体,也就是说,如果放在instance1上执行,指向的就是instance1,放在instance2上执行,指向的就是 instance2,我们利用这一点,就可以不用让每个实例都创建一份$watcher方法,而是共用同一个。

同理,我们可以把enable,visible,init,click这些都做起来,init的执行时间放在扫描完vm-model那个element之下的所有DOM节点之后。

嗯,我们是不是可以试一下了?来写个代码:

<!DOCTYPE html>
<html>
<head>
<title>Simple binding demo</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="binding">
<meta name="author" content="xu.fei@outlook.com">
<script type="text/javascript" src="../js/thin.js"></script>
</head>
<body>
<div vm-model="test.Person">
<input type="text" vm-value="name"/>
<input type="text" vm-value="age"/>
<input type="text" vm-value="age"/>
<input type="button" vm-click="growUp" value="Grow Up"/>
</div> <div vm-model="test.Person" vm-init="init">
<input type="text" vm-value="name"/>
<input type="text" vm-value="age"/>
<input type="button" vm-click="growUp" value="Grow Up"/>
</div>
<script type="text/javascript">
thin.define("test.Person", [], function () {
function Person() {
this.name = "Tom";
this.age = 5;
} Person.prototype = {
init: function () {
this.name = "Jerry";
this.age = 3;
}, growUp: function () {
this.age++;
}
}; return Person;
});
</script>
</body>
</html>

或者访问这里:http://xufei.github.io/thin/demo/simple-binding.html

以刚才文章提到的内容,还不能完全解释这个例子的效果,因为没看到在哪里调用parseElement的。说来也简单,就在thin.js里面,直 接写了一个thin.ready,在那边调用了这个函数,去解析了document.body,于是测试页面里面才可以只写绑定和视图模型。

我们还有一个更实际一点的例子,结合了另外一个系列里面写的简单DataGrid控件,做了一个很基础的人员管理界面:http://xufei.github.io/thin/demo/binding.html

2.3 小结

到此为止,我们的绑定框架勉强能够运行起来了!虽然很简陋,而且要比较新的浏览器才能跑,但毕竟是跑起来了。

注意Object.defineProperty仅在Chrome等浏览器中可用,IE需要9以上才比较正常。在司徒正美的avalon框架中,巧 妙使用VBScript绕过这一限制,利用vbs的property和两种语言的互通,实现了低版本IE的兼容。我们这个框架的目标不是兼容,而是为了说 明原理,所以感兴趣的朋友可以去看看avalon的源码。

原文链接:http://www.ituring.com.cn/article/48463

从零开始编写自己的JavaScript框架(二)的更多相关文章

  1. 从零开始编写自己的JavaScript框架(一)

    1. 模块的定义和加载 1.1 模块的定义 一个框架想要能支撑较大的应用,首先要考虑怎么做模块化.有了内核和模块加载系统,外围的模块就可以一个一个增加.不同的JavaScript框架,实现模块化方式各 ...

  2. 从零开始编写自己的C#框架(1)——前言

    记得十五年前自学编程时,拿着C语言厚厚的书,想要上机都不知道要用什么编译器来执行书中的例子.十二年前在大学自学ASP时,由于身边没有一位同学和朋友学习这种语言,也只能整天混在图收馆里拼命的啃书.而再后 ...

  3. 从零开始编写自己的C#框架(17)——Web层后端首页

    后端首页是管理员登陆后进入的第一个页面,主要是显示当前登陆用户信息.在线人数.菜单树列表.相关功能按键和系统介绍.让管理员能更方便的找到息想要的内容. 根据不同系统的需要,首页会显示不同的内容,比如显 ...

  4. 从零开始编写自己的C#框架(15)——Web层后端登陆功能

    对于一个后端管理系统,最重要内容之一的就是登陆页了,无论是安全验证.用户在线记录.相关日志记录.单用户或多用户使用帐号控制等,都是在这个页面进行处理的. 1.在解决方案中创建一个Web项目,并将它设置 ...

  5. 从零开始编写自己的C#框架(2)——开发前准备工作

    没想到写了个前言就受到很多朋友的支持,大家的推荐就是我最大的动力(推荐得我热血沸腾,大家就用推荐来猛砸我吧O^-^O),谢谢大家支持. 其实框架开发大家都知道,不过要想写得通俗点,我个人觉得还是挺吃力 ...

  6. 从零开始编写自己的C#框架(8)——后台管理系统功能设计

    还是老规矩先吐下槽,在规范的开发过程中,这个时候应该是编写总体设计(概要设计)的时候,不过对于中小型项目来说,过于规范的遵守软件工程,编写太多文档也会拉长进度,一般会将它与详细设计合并到一起来处理,所 ...

  7. 从零开始编写自己的C#框架(9)——数据库设计与创建

    对于千万级与百万级数据库设计是有所区别的,由于本项目是基于中小型软件开发框架来设计,记录量相对会比较少,所以数据库设计时考虑的角度是:与开发相结合:空间换性能:空间换开发效率:减少null异常.... ...

  8. 从零开始编写自己的C#框架 ---- 系列文章

    目录: 从零开始编写自己的C#框架(1)——前言从零开始编写自己的C#框架(2)——开发前的准备工作从零开始编写自己的C#框架(3)——开发规范从零开始编写自己的C#框架(4)——文档编写说明从零开始 ...

  9. [转帖] 从零开始编写自己的C#框架(27)——什么是开发框架

    从零开始编写自己的C#框架(27)——什么是开发框架 http://www.cnblogs.com/EmptyFS/p/4105713.html 没写过代码 一直不清楚 框架的含义 不过看了一遍 也没 ...

随机推荐

  1. 2019大疆PC软件开发笔试——开关和灯泡两个电路板

    题目描述: 小A是一名DIY爱好者,经常制作一些有趣的东西. 今天,小A突然想要来做这样一个东西.小A现在有两块同样大小为n×m,有n×m块大小为1×1小电路板拼成的矩形电路板,假设叫做电路板A和电路 ...

  2. PAT甲题题解-1061. Dating (20)-字符串处理,水水

    #include <iostream> #include <cstdio> #include <algorithm> #include <string.h&g ...

  3. 《Linux内核设计与实现》Chapter 18 读书笔记

    <Linux内核设计与实现>Chapter 18 读书笔记 一.准备开始 一个bug 一个藏匿bug的内核版本 知道这个bug最早出现在哪个内核版本中. 相关内核代码的知识和运气 想要成功 ...

  4. Alpha冲刺——day4

    Alpha冲刺--day4 作业链接 Alpha冲刺随笔集 github地址 团队成员 031602636 许舒玲(队长) 031602237 吴杰婷 031602220 雷博浩 031602634 ...

  5. Linux命令(十四) 查看工作目录文件 ls

    目录 1.命令简介 2.常用参数介绍 3.实例 4.直达底部 命令简介 ls 命令是 Linux 下最常用的命令. ls 就是 list 的缩写.默认情况下 ls 命令用来打印出当前目录的清单, 如果 ...

  6. UML时序图学习

    定义 时序图主要用于展示对象之间交互的顺序. 时序图将交互关系表示为一个二维图.纵向是时间轴,时间沿竖线向下延伸.横向轴代表了在协作中各独立对象的类元角色.类元角色用生命线表示.当对象存在时,角色用一 ...

  7. 原理分析dubbo分布式应用中使用zipkin做链路追踪

    zipkin是什么 Zipkin是一款开源的分布式实时数据追踪系统(Distributed Tracking System),基于 Google Dapper的论文设计而来,由 Twitter 公司开 ...

  8. code first 如何创建索引字段

    比较简单的办法: protected override void Seed(Context context) { CreateIndex(context, "ProductName" ...

  9. NOIP2018划水记

    // 6次NOIP里最爽的一次 去年的NOIP的游记:https://www.cnblogs.com/GXZlegend/p/7880740.html Day 0 特意请了一天假复习NOIP 实际上是 ...

  10. Linux中dpkg工具update-alternatives实现符号链接软件版本的切换(转)

    一.功能作用 update-alternatives是dpkg的实用工具,用来维护系统命令的符号链接,以决定系统默认使用什么命令. 在Debian系统中,我们可能会同时安装有很多功能类似的程序和可选配 ...