您的位置:首页 > Web前端 > JavaScript

迷你MVVM框架 avalonjs 入门教程

2015-05-16 18:54 621 查看

迷你MVVM框架 avalonjs 入门教程

关于AvalonJs开始的例子扫描视图模型数据模型绑定属性与动态模板作用域绑定(ms-controller, ms-important)模板绑定(ms-include)数据填充(ms-text, ms-html)类名切换(ms-class, ms-hover, ms-active)事件绑定(ms-on,……)显示绑定(ms-visible)插入绑定(ms-if)双工绑定(ms-duplex,原来的ms-model)样式绑定(ms-css)数据绑定(ms-data)布尔属性绑定(ms-checked, ms-selected, ms-readonly, ms-disabled, ms-enabled)字符串属性绑定(ms-title, ms-src, ms-href……)万能属性绑定(ms-attr)万能绑定(ms-bind)数组循环绑定(ms-each,ms-repeat)对象循环绑定(ms-with)UI绑定(ms-widget)$watch过滤器AMD加载器路由系统通过AJAX加载新数据到已存在的VM中功能扩展

关于AvalonJS

avalon是一个迷你的MVVM框架,虽然从发布到现在,它臌胀了不少,但它现在还是比knockout小许多。avalon开发过程一直遵循三个原则:1,复杂即错误,2,数据结构优于算法,3,出奇制胜。这三大原则保证avalon具有良好的维护性,扩展性,与众不同。简单说一下其他三大MVVM的实现思路:knockout:最早冒出来的JS MVVM库,通过转换VM中所有要监听的东西为函数,然后执行它们,得到某一时刻中,一共有多少函数被执行,将它们放到栈中,最底的就是最先被执行的,它上面的就是此函数所依赖的函数,从而得到依赖关系。 然后设计一个观察者模式,从上面的依赖检测中,将依赖函数作为被依赖者(最先执行的那个的)的订阅者,以后我们对被依赖者进行赋值时,就会通先订阅者更新自身,从而形成一个双向绑定链。 并且,knockout会将视图中的绑定属性进行转换,分解出求值函数与视图刷新函数,视图刷新函数依赖于求值函数,而求值函数亦依赖于我们VM中的某些属性(这时,它们都转换为函数),在第一次扫描时,它们会加入对应属性的订阅者列队中,从而VM中的某个属性改变,就会自动刷新视图。评价:实现非常巧妙,是avalon0.1-0.3的重要学习对象,但将属性变成一个函数,让人用点不习惯,许多用法都有点笨笨的。 虽然是一个轻盈的库,但扩展性不强,里面的实现异常复杂,导致能参与源码的人太少。emberjs: 一个大而全的框架,包罗万象。一开始是使用Object.defineProperty+观察者实现,但IE8的问题,让它不得不启用上帝setter, 上帝getter。没有自动收集依赖的机制,没有监控数组,计算属性需要自己指定依赖。VM可继承。 VM与视图的双向绑定依赖于其强大无比上万行的Handlebars 模板。听说是外国目前最好用的MV*框架。因为作者既是jQuery的核心成员,也是Rails的核心成员,虽然由于技术能力没实现自动收集依赖,但框架的其他方面做得非常易上手,人性化。评价:太大了,优缺点同python的Django框架。angular: google组织开发的框架,体现其算法至上的时候到了。里面一共有两个parser, 一个是ngSanitize/sanitize.js下的HTML parser, 一个是ng/parse.js(它要配合compile.js使用)的JS parser。第一个parser负责绑定抽取,第二个负责从Ctrl函数,工厂函数,服务函数及$watch回调中分解出无数setter, getter, 确认它们的依赖关系,放进观察者模式中。它的观察者无比强大,由于它的VM能继承,于是通过继承链实现四通发达的消息广播。它还实现了一个基于LRU的缓存系统,因为google最喜欢以空间换时间了,另一方面说明它要缓存的东西太多了,非常吃内存。公司内部用angular实现的grid,200行在PC中就拖不动了。它还用到许多时髦的东东,如HTML5 history API, 迷你版Q Promise。内部是极其复杂。 不过最大的问题是,它是基于parser,静态编译,这意思着什么呢?不抗压缩!为此,它引进了IOC,官网上给出的简单例子其实在项目完全不可用,我们需要使用另一种更复杂的写法,方便编泽器从它们得到不被压缩的部分, 让它在压缩情况也能正常运行。由于基于编译,许多行为都不是即时的,可预见的。用户写的那些控制器函数,都是为编译做准备。由于基于编译,它不得不要求我们对具有兼容问题的一些全局函数,方法进行屏蔽,用它的给出的服务替代它们,如window对应$window, document对应$document, location对应$location, setTimout对应$timeout……如果不遵循这规则,它可能运行不了,你需要手动使用$digest手动触发。 不过对于一些复杂的回调,$digest也奈何不了,但又不报错,基本无法调试,只能撞大运般地一点点改……评价:上手难度非常高,团队中没有JS高手慎用现在的avalon是我在完全消化了knockout发展起来的,准确来说,是0.4版,通过Object.defineProperties与VBScript实现了与普通对象看起来没什么两样的VM,VM里面充满了访问器属性,而访问器属性肯定对应一个setter,一个getter, 我们就在setter, getter中走knockout的老路,实现自动收集依赖,然后放进一个简单的观察者模式中,从而实现双向绑定。将绑定属性分解为求值函数与视图刷新函数,早前,avalon也与knockout一样使用一个简单的parser,然后通过with实现,0.82一个新的parser 上马,同样的迷你,但生成的求值函数,更方便依赖收集,并且没有with语句,性能更佳。angular也不是一无是处,我也从它那里抄来了{{}}插值表达式,过滤器机制,控制器绑定什么的。avalon在内部使用了许多巧妙的设计,因此能涵盖angular绝对大多数功能,但体积却非常少。此外,在性能上,现在除了chrome外,它都比knockout快,angular则是最慢的。 在移动端上,avalon这个优势会被大大放大化的。关于avalon的几点:兼容IE6没有AJAX与动画模块,需要配合jQuery等库使用avalon会自动同步视图,因此不要在VM中进行DOM操作迷你MVVM框架在github的仓库https://github.com/RubyLouvre/avalon, 如果你要兼容IE6,那么下其中的avalon.js, 如果你只打算兼容IE10与标准浏览器,那么下avalon.mobile.js。官网地址http://rubylouvre.github.io/mvvm/

开始的例子

我们从一个完整的例子开始认识 avalon :
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="avalon.js"></script>
</head>
<body>
<div ms-controller="box">
<div style=" background: #a9ea00;" ms-css-width="w" ms-css-height="h"  ms-click="click"></div>
<p>{{ w }} x {{ h }}</p>
<p>W: <input type="text" ms-duplex="w" data-duplex-event="change"/></p>
<p>H: <input type="text" ms-duplex="h" /></p>
</div>
<script>
avalon.define("box", function(vm) {
vm.w = 100;
vm.h = 100;
vm.click = function() {
vm.w = parseFloat(vm.w) + 10;
vm.h = parseFloat(vm.h) + 10;
}
})
</script>
</body>
</html>
上面的代码中,我们可以看到在JS中,没有任何一行操作DOM的代码,也没有选择器,非常干净。在HTML中, 我们发现就是多了一些以ms-开始的绑定属性与{{}}插值表达式,有的是用于渲染样式, 有的是用于绑定事件。在ms-model中,我们会发现它会反过来操作VM,VM的改变也会影响视图的其他部分。

扫描

不过上面的代码并不完整,它能工作,是因为框架默认会在DOMReady时扫描DOM树,将视图中的绑定属性与{{}}插值表达式抽取出来,转换为求值函数与视图刷新函数。上面的JS代码相当于:
avalon.ready(function() {
avalon.define("box", function(vm) {
vm.w = 100;
vm.h = 100;
vm.click = function() {
vm.w = parseInt(vm.w) + 10;
vm.h = parseInt(vm.h) + 10;
}
})
avalon.scan()
})
avalon.scan是一个非常重要的方法,它有两个可选参数,第一个是扫描的起点元素,默认是HTML标签,第2个是VM对象。
//源码
avalon.scan = function(elem, vmodel) {
elem = elem || root
var vmodels = vmodel ? [].concat(vmodel) : []
scanTag(elem, vmodels)
}
现在扫描的顺序是 ms-skip --> ms-important --> ms-controller --> ms-if ... 只要元素存在ms-skip 这个绑定属性时,就忽略扫描此元素及子孙。然后是ms-important, ms-controller这 两个与作用域有关的绑定,如果它们指向的VM在avalon.vmodels 不存在时,规则同ms-skip。 最后ms-if,如果ms-if的表达式的结果为true,那么走如下步骤:如果没有插入到DOM树,插入它,并扫描此元素。如果值为假,就移除此元素,并停止扫描此元素的其他绑定属性及子孙。

视图模型

我们是通过avalon.define函数返回一个视图对象VM,并且avalon.define(vmName, function(vm){})中的vm并不等于VM,工厂函数中的vm是用于转换为VM的。生成的VM比用户指定的属性还多了许多属性。默认的,除了函数外,其他东西都转换为监控属性,计算属性与监控数组。如果不想让它转换,可以让此属性以 $开头,框架就不会转换它们。    如果实在不方便改名,又不想被转换,比如是一个jQuery对象或一个DOM节点,如果转换,肯定拖死框架,我们可以放到vm.$skipArray = [propName1, propName2]中去,这样也忽略转换。视图里面,我们可以使用ms-controller, ms-important指定一个VM的作用域。此外,在ms-each, ms-with中,它们会创建一个临时的VM,用于放置$key,$val, $index, $last, $first, $remove等变量或方法。另外,avalon不允许在VM定义之后,再追加新属性与方法,比如下面的方式是错误的:
var vm = avalon.define("test", function(vm) {
vm.test1 = '点击测试按钮没反应 绑定失败';
});
vm.one = function() {
vm.test1 = '绑定成功';
};
//这里有两个错误,
//1在命名上没有区分avalon.define的返回值与它回调中的参数,
//2one方法的定义位置不对(这是考虑到兼容IE6-8,要求所有浏览器保持行为一致)
此外,不要在avalon.define方法里面执行函数或方法,因此框架会对define执行了两次,第1次用于取得用户对vm对象(factory的传参)的设置,第2次用于重置里面的vm对象为真正的VM。看源码:
avalon.define = function(name, factory) {
var args = avalon.slice(arguments)
if (typeof name !== "string") {
name = generateID()
args.unshift(name)
}
factory = args[1]
var scope = {
$watch: noop
}
factory(scope) //第1次!!!!!!!!!!!!!!
var model = modelFactory(scope) //偷天换日,将scope换为model
stopRepeatAssign = true
factory(model)  //第2次!!!!!!!!1
stopRepeatAssign = false
model.$id = name
return VMODELS[name] = model
}
因此下面的写法会执行两次alert
function check(){ alert("!!!!!!!!!!!")}
var model = avalon.define("xxx", function(vm){
vm.bool = true;
check()//这个方法不应该写在这里,请放在avalon.define外面
vm.array = [1,2,3]
})
如果VM中的某函数是作为事件回调而存在,如ms-click=aaa, aaa的vm请替换为avalon.define返回的变量,确保能正确运行。
var model = avalon.define("xxx", function(vm){
vm.percent = 10
vm.aaa = function(){
model.percent ++;
}
})
所有定义好的VM都会储放在avalon.vmodels中!我们再看看如何更新VM中的属性(重点):
<script>
var model = avalon.define("update", function(vm) {
vm.aaa = "str"
vm.bbb = false
vm.ccc = 1223
vm.time = new Date
vm.simpleArray = [1, 2, 3, 4]
vm.objectArray = [{name: "a"}, {name: "b"}, {name: "c"}, {name: "d"}]
vm.object = {
o1: "k1",
o2: "k2",
o3: "k3"
}
vm.simpleArray = [1, 2, 3, 4]
vm.objectArray = [{name: "a", value: "aa"}, {name: "b", value: "bb"}, {name: "c", value: "cc"}, {name: "d", value: "dd"}]
vm.object = {
o1: "k1",
o2: "k2",
o3: "k3"
}
})
setTimeout(function() {
//如果是更新简单数据类型(string, boolean, number)或Date类型
model.aaa = "这是字符串"
model.bbb = true
model.ccc = 999999999999
var date = new Date
model.time = new Date(date.setFullYear(2005))
}, 2000)

setTimeout(function() {
//如果是数组,注意保证它们的元素的类型是一致的
//只能全是字符串,或是全是布尔,不能有一些是这种类型,另一些是其他类型
//这时我们可以使用set方法来更新(它有两个参数,第一个是index,第2个是新值)
model.simpleArray.set(0, 1000)
model.simpleArray.set(2, 3000)
model.objectArray.set(0, {name: "xxxxxxxxxxxxxxxx", value: "xxx"})
}, 2500)
setTimeout(function() {
model.objectArray[1].name = "5555"
}, 3000)
setTimeout(function() {
//如果要更新对象,直接赋给它一个对象,注意不能将一个VM赋给它,可以到VM的$model赋给它(要不会在IE6-8中报错)
model.object = {
aaaa: "aaaa",
bbbb: "bbbb",
cccc: "cccc",
dddd: "dddd"
}
}, 3000)
</script>
<div ms-controller="update">
<div>{{aaa}}</div>
<div>{{bbb}}</div>
<div>{{ccc}}</div>
<div>{{time | date("yyyy - MM - dd mm:ss")}}</div>
<ul ms-each="simpleArray">
<li>{{el}}</li>
</ul>
<div>  <select ms-each="objectArray">
<option ms-value="el.value">{{el.name}}</option>
</select>
</div>
<ol ms-with="object">
<li>{{$key}}                {{$val}}</li>
</ol>
</div>
这里还有个例子,大家认真看看。

数据模型

当我们要用AJAX与后端交互时,如果直接把VM传上去太大了,这时我们需要把它对应的纯数组的JS对象。在VM中有个叫$model的属性,这是一个对象,就是数据模型M了。当我们更改VM时,框架就会自动同步M

绑定属性与动态模板

在开始之前,我们看一下静态模板是怎么工作的:我之前写了一个叫ejs的静态模板引擎:
<script type="tmpl" id="table_tmpl">
<&= title() &>
<table border=1>
<&- for(var i=0,tl = @trs.length,tr;i<tl;i++){  -&>
<&- tr = @trs[i]; -&>
<tr>
<td><&= tr.name;; &></td> <td><&= tr.age; &></td> <td><&= tr.sex || "男" &></td>
</tr>
<& } &>
</table>
< 怎么可能不支持图片 &>
<img src="<&= @href &>">
</script>
它是以一个script标签做容器,里面的整个叫模板。模板里面有许多以 <& 与 &>划分出来的区块,用于插入JS代码,以@开头的变量是对应于数据包中的某个属性。几乎所有静态模板的实现原理都是一样的,将这个模板变成一个函数,然后里面分成静态部分与动态部分,静态部分就是上面的HTNMl部分,转换为一个个字符串,动态部分就是插入的JS代码, 它们基本上原封不动地成为函数体的逻辑。然后我们传入一个对象给这个函数,最后得到一个符合HTML格式的字符串,最后用它贴到页面上某个位置就行了。静态模板有几个缺点,首先它容易混入大量的JS逻辑,对于菜鸟来说,他们特别喜欢在里面放入越来越多JS代码。这个在JSP年代,已经证明是bad practice。为此出现了logic-less的 mustache。 其次,它更新视图总是一大片一大片地处理,改动太大。最后,是由于第2点引发的问题,它对事件绑定等不友好,因为一更新,原来的节点都被消灭了,需要重新绑定。幸好,jQuery普及了事件代理,这问题才没有暴露出来。再次,字符串模块没有对样式的操作,流程的操作进行封装,没有计算属性,监控数组的东西,很容易诱导用户在页面上写大量业务逻辑,导致代码无法维护。下面就是一个PHP+原生JS+JQ的例子:再看动态模板,几乎所有MVVM框架都用动态模板(当然也有例外,如emberjs)。动态模板以整个DOM树为容器,它通过扫描方式进行第一次更新视图。 在静态模板,通过<& 与 &>划分的部分,转换为绑定属性与{{}}插值表达式(这是一种文本绑定,在avalon中,我们可以通过|html过滤器,转换html绑定) 这样就有效阻止用户在页面上写逻辑。虽然动态模板也支持ms-if, ms-each等表示逻辑关系的绑定,但它的值最复杂也只能是一个表达式。在绑定属性中,属性名用于指定操作行为,如切换类名,控制显示,循环渲染,绑定事件,数据填充什么的,而属性值是决定这些操作是否执行,与渲染结果。 由于双向绑定的关系,它不像静态模板那样,每次都要自己将数据包放进函数,得到结果,然后innerHTML刷新某个区域。它是在用户为VM的某个属性进行重新赋值,将视图中对应的某个文本节点, 特性节点或元素节点的值进行重刷。因此不会影响事件绑定。在avalon中,这些视图刷新函数都有个element属性,保持对应的元素节点,每次同步时,都会检测此元素节点是否在DOM树,不在DOM树就取消订阅此刷新函数,节约内存,防止无效操作。因此,你们可以看区别了吧。绑定属性与插值表达式就是对应静态模板中的JS逻辑部分,由于只允许为表达式或单个属性值,复杂度被控制了,强制用户将它们转移到VM中。 VM作为一个数据源,对应静态模板的数据包,并且多了一个自动触发功能,进化成一个消息中心。
<p ms-controller="test" ms-click="click">{{ a }}</p>

<script>
avalon.define("test", function(vm) {
vm.a = '123';
vm.click = function() {
vm.a = new Date - 0
}
})
</script>

作用域绑定(ms-controller, ms-important)

avalon提供ms-controller, ms-important来指定VM在视图的作用范围。比如有两个VM,它们都有一个firstName属性,在DIV中,如果我们用 ms-controller="VM1", 那么对于DIV里面的{{firstName}}就会解析成VM1的firstName中的值。有关它们的详细用法,可见这里

模板绑定(ms-include)

如果单是把DOM树作为一个模板远远不够的,比如有几个地方,需要重复利用一套HTML结构,这就要用到内部模板或外部模板了。内部模板是,这个模板与目标节点是位于同一个DOM树中。我们用一个MIME不明的script标签或者noscript标签(0.94后支持,建议使用它)保存它,然后通过ms-include="id"引用它。
<script type="text/avalon" id="tpl">
here, {{ 1 + 1 }}
</script>

<div  ms-include="'tpl'"></div>
注意,ms-include的值要用引号括起,表示这只是一个字符串,这时它就会搜索页面的具有此ID的节点,取其innerHTML,放进ms-include所在的元素内部。否则这个tpl会被当成一个变量, 框架就会在VM中检测有没有此属性,有就取其值,重复上面的步骤。如果成功,页面会出现here, 2的字样。如果大家想在模板加载后,加工一下模板,可以使用data-include-loaded来指定回调的名字。如果大家想在模板扫描后,隐藏loading什么的,可以使用data-include-rendered来指定回调的名字。下面是它们的实现
var vmodels = data.vmodels
var rendered = getBindingCallback(elem.getAttribute("data-include-rendered"), vmodels)
var loaded = getBindingCallback(elem.getAttribute("data-include-loaded"), vmodels)

function scanTemplate(text) {
if (loaded) {
text = loaded.apply(elem, [text].concat(vmodels))
}
avalon.innerHTML(elem, text)
scanNodes(elem, vmodels)
rendered && checkScan(elem, function() {
rendered.call(elem)
})
}
外部模板,通常用于多个页面的复用,因此需要整成一个独立的文件。这时我们就需要通过ms-include-src="src"进行加载。比如有一个HTML文件tmpl.html,它的内容为:
<div>这是一个独立的页面</div>
<div>它是通过AJAX的GET请求加载下来的</div>
然后我们这样引入它
<div  ms-include-src="'tmpl.html'"></div>
有关它的高级应用的例子可见这里利用ms-include与监控数组实现一个树

数据填充(ms-text, ms-html)

这分两种:文本绑定与HTML绑定,每种都有两个实现方式
<script>

avalon.define("test", function(vm) {
vm.text = "<b> 1111  </b>"
})

</script>
<div ms-controller="test">
<div><em>用于测试是否被测除</em>xxxx{{text}}yyyy</div>
<div><em>用于测试是否被测除</em>xxxx{{text|html}}yyyy</div>
<div ms-text="text"><em>用于测试是否被测除</em>xxxx yyyy</div>
<div ms-html="text"><em>用于测试是否被测除</em>xxxx yyyy</div>
</div>
默认情况下,我们是使用{{ }} 进行插值,如果有特殊需求,我们还可以配置它们
avalon.config({
interpolate:[""]
})

类名切换(ms-class, ms-hover, ms-active)

avalon提供了多种方式来绑定类名,有ms-class, ms-hover, ms-active, 具体可看这里

事件绑定(ms-on)

avalon通过ms-on-click或ms-click进行事件绑定,并在IE对事件对象进行修复,并统一了所有浏览器对return false的处理。具体可看这里avalon并没有像jQuery设计一个近九百行的事件系统,连事件回调的执行顺序都进行修复(IE6-8,attachEvent添加的回调在执行时并没有按先入先出的顺序执行),只是很薄的一层封装,因此性能很强。ms-clickms-dblclickms-mouseoutms-mouseoverms-mousemovems-mouseenterms-mouseleavems-mouseupms-mousedownms-keypressms-keyupms-keydownms-focusms-blurms-changems-on-*
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>有关事件回调传参</title>
<script src="avalon.js" type="text/javascript"></script>
<script>

avalon.ready(function() {
var a = avalon.define("simple", function(vm) {
vm.firstName = "司徒"
vm.lastName = "正美"
vm.array = ["aaa", "bbb", "ccc"]
vm.argsClick = function(e, a, b) {
alert(a+ "  "+b)
}
vm.loopClick = function(a) {
alert(a)
}
});
avalon.scan();
})

</script>
</head>
<body>
<fieldset ms-controller="simple">
<legend>例子</legend>
<div ms-click="argsClick($event, 100, firstName)">点我</div>
<div ms-each-el="array" >
<p ms-click="loopClick(el)">{{el}}</p>
</div>
</fieldset>
</body>
</html>
另外,这里有一些结合ms-data实现事件代理的技巧,建议事件绑定接口支持事件代理,最简单就是table上可以绑定td的点击事件

显示绑定(ms-visible)

avalon通过ms-visible="bool"实现对某个元素显示隐藏控制,它用是style.display="none"进行隐藏。

插入绑定(ms-if)

这个功能是抄自knockout的,ms-if="bool",同样隐藏,但它是将元素移出DOM。这个功能直接影响到CSS :empty伪类的渲染结果,因此比较有用。

双工绑定(ms-duplex)

这功能抄自angular,原名ms-model起不得太好,姑且认为利用VM中的某些属性对表单元素进行双向绑定。打算启用一个新名字叫ms-duplex这个绑定,它除了负责将VM中对应的值放到表单元素的value中,还对元素偷偷绑定一些事件,用于监听用户的输入从而自动刷新VM。具体如下:text, password, textarea默认是通过input事件进行监听,旧式IE是通过propertychange实现,换言之,每改一个字符串都触发。如果想在失去焦点时才触发,可以在元素上使用data-duplex-event="change"进行调整。 它要求VM对应的属性为一个字符串或数字,不过触发一次之后,属性就会变成字符串。radio默认是通过change事件进行监听,旧式IE是通过chick实现, 它要求VM对应的属性为一个布尔。在存在复数个name值相同,value值一样的radio的情况下,我们要实现radio之间切换,则需要用到ms-duplex-text,这时VM对应的属性为一个字符串,为其他一个radio的value值。checkbox默认是通过change事件进行监听, 它要求VM对应的属性为一个字符串数组。有时,我们需要将checkbox 当成radio用,就需要用ms-duplex-radio,点我看此例子select默认是通过change事件进行监听, 它要求VM对应的属性为一个字符串或字符串数组(视multiple的值)。注意:ms-duplex与ms-checked不能在同时使用于一个元素节点上。大家可以通过这个页面进行学习。

样式绑定(ms-css)

用法为ms-css-name="value"

数据绑定(ms-data)

用法为ms-data-name="value", 用于为元素节点绑定HTML5 data-*属性。

布尔属性绑定

这主要涉及到表单元素几个非常重要的布尔属性,即disabed, readyOnly, selected , checked, 分别使用ms-disabled, ms-enabled, ms-readonly, ms-checked, ms-selected。ms-disabled与ms-enabled是对立的,一个true为添加属性,另一个true为移除属性。

字符串属性绑定

这主要涉及到几个非常常用的字符串属性,即href, src, alt, title, value, 分别使用ms-href, ms-src, ms-alt, ms-title, ms-value。它们的值的解析情况与其他绑定不一样,如果值没有{{}}插值表达式,那么就当成VM中的一个属性,并且可以与加号,减号混用, 组成表达式,如果里面有表达式,整个当成一个字符串。
              xxxxxxxx

万能属性绑定(ms-attr)

ms-attr-name="value",这个允许我们在元素上绑定更多种类的属性,如className, tabIndex, name, colSpan什么的。

万能绑定(ms-bind)

已废弃。替换方案见这里

数组循环绑定(ms-each, ms-repeat)

用法为ms-each-xxx="array", 其中xxx可以随意改,如item, el, 它是用于在子元素中进行引用。array对应VM中的一个普通数组或一个监控数组。详见这里。ms-each与ms-repeat的不同之处在于,前者循环它的孩子,后者循环它自身。注意,ms-each, ms-repeat会生成一个或两个新VM插入当前的vmodels中。如果是数组的元素是简单类型,那么会生成一个,如果是对象,那么就生成两个。我们还可以通过data-each-rendered, data-repeat-rendered来指定这些元素都插入DOM被渲染了后执行的回调,this指向元素节点,有一个参数表示为当前的操作,是add, del, move, index还是clear
})</script><div ms-controller="test"><select ms-each-el="array"><option ms-value="el.value">{{$index}}、{{el}}</option></select></div>
     <style>.id2013716 {width: 200px;float:left;}</style><script>var a = avalon.define("array", function(vm) {vm.array = ["1", "2", "3", "4"]})setTimeout(function() {a.array.set(0, 7)}, 1000);var b = avalon.define("complex", function(vm) {vm.array = [{name: "xxx", sex: "aaa", c: {number: 2}}, {name: "yyy", sex: "bbb", c: {number: 4}}]//});setTimeout(function() {b.array[0].c.number = 9b.array[0].name = "zzz"}, 1000)setTimeout(function() {a.array.push(5, 6, 7, 8, 9)}, 1000)setTimeout(function() {a.array.unshift("a", "b", "c", "d")}, 2000)setTimeout(function() {a.array.shift()}, 3000)setTimeout(function() {a.array.pop()}, 4000)setTimeout(function() {a.array.splice(1, 3, "x", "y", "z")}, 5000)</script><fieldset class="id2013716" ms-controller="array"><legend>例子</legend><ul ms-each-el="array"><li>数组的第{{$index+1}}个元素为{{el}}</li></ul></fieldset><fieldset  class="id2013716" ms-controller="complex"><legend>例子</legend><ul ms-each-el="array"><li>{{name+" "+sex}}它的内容为 number:{{c.number}}</li></ul></fieldset>

对象循环绑定(ms-with)

语法为 ms-with="obj" 子元素里面用$key, $val分别引用键名,键值[code]UI绑定(ms-widget)它的格式为ms-widget="uiName, id?, optsName?"uiName,必选,一定要全部字母小写,表示组件的类型id 可选 这表示新生成的VM的$id,方便我们从avalon.vmodels[id]中获取它操作它,如果它等于$,那么表示它是随机生成,与不写这个效果一样,框架会在uiName加上时间截,生成随机IDoptName 可选, 配置对象的名字。指在已有的VM中定义一个对象(最好指定它为不可监控的外),作为配置的一部分(因为每个UI都有它的默认配置对象,并且我们也可以用data- uiName? -xxx来做更个性化的处理 )。如果不指optName默认与uiName同名。框架总是找离它(定义ms-widget的那个元素节点)最近的那个VM来取这个配置项。如果这个配置项里面有widget+"Id"这个属性,那么新生成的VM就是用它作为它的$id下面是一个完整的实例用于教导你如何定义使用一个UI。例子首先,以AMD规范定义一个模块,文件名为avalon.testui.js,把它放到与avalon.js同一目录下。内容为:
define(["avalon"], function(avalon) {//    必须 在avalon.ui上注册一个函数,它有三个参数,分别为容器元素,data, vmodelsavalon.ui["testui"] = function(element, data, vmodels) {//将它内部作为模板,或者使用文档碎片进行处理,那么你就需要用appendChild方法添加回去var innerHTML = element.innerHTML//由于innerHTML要依赖许多widget后来添加的新属性,这时如果被扫描肯定报“不存在”错误//因此先将它清空avalon.clearHTML(element)var model = avalon.define(data.testuiId, function(vm) {avalon.mix(vm, data.testuiOptions)//优先添加用户的配置,防止它覆盖掉widget的一些方法与属性vm.value = 0; // 给input一个个默认的数值vm.plus = function(e) { // 只添加了这个plusmodel.value++;}})avalon.nextTick(function() {//widget的VM已经生成,可以添加回去让它被扫描element.innerHTML = innerHTMLavalon.scan(element, [model].concat(vmodels))})return model//必须返回新VM}avalon.ui["testui"].defaults = {aaa: "aaa",bbb: "bbb",ccc: "ccc"}return avalon//必须返回avalon})
然后页面这样使用它
<!DOCTYPE html><html><head><meta charset="utf-8"/><script src="avalon.js"></script></head><body><script>require("avalon.testui", function() {avalon.define("test", function(vm) {vm.$opts = {name: "这是控件的内容"}})avalon.scan()})</script><div ms-controller="test" ms-widget="testui,ddd,$opts" ><input ms-duplex="value" /><button type="button" ms-click="plus">ClickMe</button></div></body></html>

$watch

这是一个位于VM的方法,用于监听VM的某人属性的变化,回调中有两个传参,新属性值与旧属性值,里面的this指向VM,详见这里

过滤器

avalon从angular中抄来管道符风格的过滤器,但有点不一样。 它只能用于{{}}插值表达式。如果不存在参数,要求直接跟|filter,如果存在参传,则要用小括号括起,参数要有逗号,这与一般的函数调用差不多,如|truncate(20,"……")avalon自带以下几个过滤器html没有传参,用于将文本绑定转换为HTML绑定uppercase大写化lowercase小写化truncate对长字符串进行截短,truncate(number, truncation), number默认为30,truncation为“...”camelize驼峰化处理escape对类似于HTML格式的字符串进行转义,把尖括号转换为> <currency对数字添加货币符号,以及千位符, currency(symbol)number对数字进行各种格式化,这与与PHP的number_format完全兼容, number(decimals, dec_point, thousands_sep),[code] decimals 可选,规定多少个小数位。dec_point 可选,规定用作小数点的字符串(默认为 . )。thousands_sep 可选,规定用作千位分隔符的字符串(默认为 , ),如果设置了该参数,那么所有其他参数都是必需的。
date对日期进行格式化,date(formats)
      'yyyy': 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010)'yy': 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10)'y': 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199)'MMMM': Month in year (January-December)'MMM': Month in year (Jan-Dec)'MM': Month in year, padded (01-12)'M': Month in year (1-12)'dd': Day in month, padded (01-31)'d': Day in month (1-31)'EEEE': Day in Week,(Sunday-Saturday)'EEE': Day in Week, (Sun-Sat)'HH': Hour in day, padded (00-23)'H': Hour in day (0-23)'hh': Hour in am/pm, padded (01-12)'h': Hour in am/pm, (1-12)'mm': Minute in hour, padded (00-59)'m': Minute in hour (0-59)'ss': Second in minute, padded (00-59)'s': Second in minute (0-59)'a': am/pm marker'Z': 4 digit (+sign) representation of the timezone offset (-1200-+1200)format string can also be one of the following predefined localizable formats:'medium': equivalent to 'MMM d, y h:mm:ss a' for en_US locale (e.g. Sep 3, 2010 12:05:08 pm)'short': equivalent to 'M/d/yy h:mm a' for en_US locale (e.g. 9/3/10 12:05 pm)'fullDate': equivalent to 'EEEE, MMMM d,y' for en_US locale (e.g. Friday, September 3, 2010)'longDate': equivalent to 'MMMM d, y' for en_US locale (e.g. September 3, 2010'mediumDate': equivalent to 'MMM d, y' for en_US locale (e.g. Sep 3, 2010)'shortDate': equivalent to 'M/d/yy' for en_US locale (e.g. 9/3/10)'mediumTime': equivalent to 'h:mm:ss a' for en_US locale (e.g. 12:05:08 pm)'shortTime': equivalent to 'h:mm a' for en_US locale (e.g. 12:05 pm)
例子:生成于{{ new Date | date("yyyy MM dd:HH:mm:ss")}}生成于{{ "2011/07/08" | date("yyyy MM dd:HH:mm:ss")}}生成于{{ "2011-07-08" | date("yyyy MM dd:HH:mm:ss")}}生成于{{ "01-01-2000" | date("yyyy MM dd:HH:mm:ss")}}生成于{{ "03 04,2000" | date("yyyy MM dd:HH:mm:ss")}}生成于{{ "3 4,2000" | date("yyyy MM dd:HH:mm:ss")}}生成于{{ 1373021259229 | date("yyyy MM dd:HH:mm:ss")}}生成于{{ "1373021259229" | date("yyyy MM dd:HH:mm:ss")}}值得注意的是,new Date可传的格式类型非常多,但不是所有浏览器都支持这么多,详看这里多个过滤器一起工作
<div>{{ prop | filter1 | filter2 | filter3(args, args2) | filter4(args)}}</div>
如果想自定义过滤器,可以这样做
               avalon.filters.myfilter = function(str, args, args2){//str为管道符之前计算得到的结果,默认框架会帮你传入,此方法必须返回一个值/* 具体逻辑 */return ret;}

AMD 加载器

avalon装备了AMD模范的加载咕咕,这涉及到两个全局方法 require与definerequire(deps, callback)deps 必需。String|Array。依赖列表,可以是具体路径或模块标识,如果想用字符串表示多个模块,则请用“,”隔开它们。callback 必需。Function。回调,当用户指定的依赖以及这些依赖的依赖树都加载执行完毕后,才会安全执行它。模块标识一个模块标识就是一个字符串,通过它们来转换成到对应JS文件或CSS文件的路径。有关模块标识的CommonJS规范,可以见 这里具体约定如下:每个模块标识的字符串组成只能是合法URL路径,因此只能是英文字母,数字,点号,斜扛,#号。如果模块标识是 以"./"开头,则表示相对于它的父模块的目录中找。如果模块标识是 以"../"开头,则表示相对于它的父模块的父目录中找。如果模块标识不以点号或斜扛开始,则有以下三种情况如果此模块标识在 $.config.alias存在对应值,换言之某一模块定义了一个别名,则用此模块的具体路径加载文件。如果此模块标识 以http://、https://、file:/// 等协议开头的绝对路径,直接用它加载文件。否则我们将在引入框架种子模块(avalon.js)的目录下寻找是否有同名JS文件,然后指向它。对于JS模块,它可以省略后缀名,即“.js”可有可无;但对于CSS需要使用css!插件机制。框架种子模块的目录保存于 $.config.base属性中。ready!是系统占位符,用于表示DOM树是否加载完毕,不会进行路径转换。如果想禁止使用avalon自带的加载器,可以在第一次调用require方法之前,执行如下代码:
与jquery更好的集成,比如一些旧系统,直接在页面引入jquery库与其大量jquery插件,改成动态加载方式成本非常大。怎么样才能与jquery和平共存,亦能让AMD加载发挥作呢?先引入jquery库, 然后将avalon.modules.jquery 加个预设值(exports: jquery用于shim机制, state: 2 表明它已经加载完毕)就行了。例子
<!DOCTYPE html><html><head><title></title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><script src="jquery.js" type="text/javascript"></script><script src="avalon.js" type="text/javascript"></script></head><body><div ms-controller="main" ms-click="click"><p><a href="#" >点击我</a></p></div><script type="text/javascript">avalon.modules.jquery = {exports: jQuery,state: 2}require('jquery,ready!', function($) {avalon.log('加载jq了啊……')$.ajaxSetup({headers: {ajaxRequest: true},beforeSend: function(o) {avalon.log(typeof o)avalon.log(typeof o.id)},complete: function(data) {avalon.log('ajax 成功执行啦,阿门!')}})$('body').bind("click", function(e) {alert("document");avalon.log(typeof e.target.$vmodel)$.post('./h.js', {}, function(res) {avalon.log(typeof res)})});})</script></body></html>
[code]<!DOCTYPE html><html><head><title></title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><!-->这里没有东西</--><script src="avalon.js" type="text/javascript"></script></head><body><div ms-controller="main" ms-click="click"><p><a href="#" >点击我</a></p></div><script type="text/javascript">/* 0.982之前可以avalon.config({alias: {jquery: {exports: "jQuery",//这是原来jQuery库的命名空间,必须写上src: "jquery.js"}}})*///下面是兼容requirejs的方法,推荐使用这个avalon.config({paths: {jquery: "http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"},shim: {jquery: {exports: "jQuery"//这是原来jQuery库的命名空间,必须写上}}})require('jquery,ready!', function($) {avalon.log('加载jq了啊……')$.ajaxSetup({headers: {ajaxRequest: true},beforeSend: function(o) {avalon.log(typeof o)avalon.log(typeof o.id)},complete: function(data) {avalon.log('ajax 成功执行啦,阿门!')}})$('body').bind("click", function(e) {alert("document");avalon.log(typeof e.target.$vmodel)$.post('./h.js', {}, function(res) {avalon.log(typeof res)})});})</script></body></html>
例子加载单个模块。
             // 由于lang.js与mass.js是位于同一目录下,可以省略./require("lang", function(lang) {alert(lang.String.toUpperCase("aa"))});
例子加载多个模块。需要注意的是,涉及DOM操作时必须要待到DOM树建完才能进入,因此我们在这里指定了一个标识,叫"ready!", 它并不一个模块,用户自定义模块,也不要起名叫"ready!"。
             require("jquery,node,attr,ready!", function($) {alert($.fn.attr + "");alert($.fn.prop + "");});
例子加载多个模块,使用字符串数组形式的依赖列表。
             require(["jquery", "css", "ready!"], function($, css) {$("#js_require_ex3").toggle();});
例子加载CSS文件。
             require(["jquery", "ready!", "css!http//sdfds.xdfs.css"], function($) {$("#js_require_ex3").toggle();});
例子使用别名机制管理模块的链接。
             var path = location.protocol + "//" + location.host + "/doc/scripts/loadtest/"/* 0.982之前可以require.config({alias: {"aaa": path + "aaa.js","bbb": path + "bbb.js","ccc": path + "ccc.js","ddd": path + "ddd.js"}})*///下面是兼容requirejs的方法,推荐使用这个require.config({paths: {"aaa": path + "aaa.js","bbb": path + "bbb.js","ccc": path + "ccc.js","ddd": path + "ddd.js"}})require("aaa,bbb,ready", function(a, b, $) {var parent = $("#loadasync2")parent.append(a);parent.append(b);$("#asynctest2").click(function() {require("ccc,ddd", function(c, d) {parent.append(c);parent.append(d);})})});
例子加载不按规范编写的JS文件,可以让你不用改jQuery的源码就加载它。相当于其他加载器的shim插件。 与别名机制不同的是,现在它对应一个对象,src为完整路径,deps为依赖列表,exports为其他模块引用它时,传送给它们的参数
  !function() {var path = "http://files.cnblogs.com/shuicaituya/"require.config({pashs: {"jquery":  path + "jquery.js"},shim:{jquery:   {deps: [], //没有依赖可以不写exports: "jQuery"}}});require("jquery", function($) {alert($)alert("回调调起成功");})}()
define方法用于定义一个模块,格式为:define( id?, deps?, factory )
 id可选。String。模块ID。它最终会转换一个URL,放于 $.modules中。deps可选。String|Array。依赖列表。factory必需。Function|Object。模块工厂。它的参数列参为其依赖模块所有返回的值,如果某个模块没有返回值,则对应位置为undefined
注意, define方法不能写在script标签的innerHTML中,只能写在JS文件里。例子加载不按规范编写的JS文件,可以让你不用改jQuery的源码就加载它。相当于其他加载器的shim插件。 与别名机制不同的是,现在它对应一个对象,src为完整路径,deps为依赖列表,exports为其他模块引用它时,传送给它们的参数
//aaa.js 没有依赖不用改define("aaa", function() {return 1})//bbb.js  没有依赖不用改define("bbb", function() {return 2});//ccc.jsdefine("ccc", ["$aaa"], function(a) {return 10 + a})//ddd/ddd.jsdefine("ddd", ["$ddd"], function(c) {return c + 100});

路由系统

它需要依赖于另一个独立的组件mmRouter,用法请见这里

通过AJAX加载新数据到已存在的VM中

AJAX可以使用jQuery或mmRequest, mmRequest体积更少,覆盖jQuery ajax模块的90%功能,并且在现代浏览器中使用了XMLHttpRequest2实现,性能更佳。
var model = avalon.define("test", function(vm){vm.ajaxData = {} //这是一个占位符vm.arrayData = [1,2,3,4]})$.ajax({type: "GET",url: "xxx",success: function(data){var newData = fn(data) //fn是你自己定义一个方法,对data进行扁平化,最好变成一重的对象//如果你的vm.ajaxData如果是个空对象,可以直接赋值model.ajaxData = newData//如果它不是一个空对象,那么需要使用avalon.mix,先已有的数据,新的数据,全部拷贝到一个全新的空对象中,再赋值//  newData = avalon.mix({}, model.ajaxData.$model, newData )//这是关键,防止影响原来的$model// model.ajaxData = newData//  model.arrayData.push.apply(model.arrayData, data.newData)//添加更多元素//  model.arrayData =  data.newData//直接替换}})

扩展功能

avalon现在有三个扩展点,一是在avalon.fn上添加新的原型方法,这是用于处理DOM的,二是在avalon.bindingHandlers上添加新的绑定(ms-xxx),三是在avalon.filters添加新的过滤器。添加原型方法就不用多说,建议尽可能返回this,实现链式操作,this[0]为它包含的元素节点。添加过滤器也很简,翻看源码看看lowercase如何实现就行了。添加新绑定难一点,框架要求对应的处理函数有两个参数,data与vmodels, data拥有如下几个属性:element: 绑定了ms-xxx的元素,如<div ms-xxx-yyy='zzz'>innerHTML</div>,ms-xxx绑定所在的DIV元素。value:是指mx-xxx绑定的这个特性节点的值,即上面的zzz。param:是指mx-xxx绑定名以“-”分开几截,除了最前面的两部分外的东西,如这里的“yyy”。vmodels是指,从DOM树最顶点到添加此绑定的元素所路过的ms-controller的值(它们都对应一个VM)。注意,ms-each, ms-with也产生VM。现在avalon拥有如此多绑定:利用avalon 实现一个简单的成绩单, 教你如何使用ms-each数组循环绑定与$watch回调
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: