vm

    vm是一种利用Proxy或 Object.defineProperties或VBScript创建的特殊对象。

    里面以$带头的属性或放到$skipArray,都转换为访问器属性,也就是其他语言的setter, getter。因此如果这个属性最初没有定义,那么它就不会转换为访问器属性,修改该属性,就不会刷新视图。

    avalon定义了的vm,都可以在avalon.vmodels中查看到。我们可以在chrome控制台下看一下刚才的start vm的构造。

    平时而言,vm是一种比较重型的对象。从占用内存角度来划分,浏览器中的四种对象排行如下:

    • 超轻量 Object.create(null)
    • 轻量 一般的对象 {}
    • 重量 带有访问器属性的对象, avalon VM对象
    • 超重量 各种节点或window对象
      我们构建VM时只允许存在普通对象(不能是某个函数的实例),函数,数组,数字,字符串,布尔,其他一切不支持(undefined与null不能出现在定义VM时,只能用它们来赋值)

    VM中以$开头的属性都是框架保留使用的特殊属性,大家为数据起名字时要小心避开

    这些以$开头的属性,目前除了$id, $events, $watch, $fire, $model比较稳定外, 其他系统属性在不同版本存在增删的情况.

    • $id, vm的名字
    • $watch, 用于添加监听函数
    • $fire, 用于触发监听函数
    • $events, 用于储存监听函数
    • $model, 返回一个纯净的JS对象
    • $element, 2.0新增, 当我们用ms-controller, ms-important指定一个VM的作用域,对应元素节点会放到这个属性上.
    • $computed, 2.2.1新增,用来集中定义计算属性
      另外,avalon在VBScript或Object.defineProperty模式下不支持追加新属性与方法
    1. $id: "test",
    2. test1: "点击测试按钮没反应 绑定失败"
    3. })
    4. vm.aaa = 'newProp'
    5. vm.newMethod = function () { //不能再追加此方法
    6. vm.test1 = "绑定成功"
    7. }

    但我们可以通过以下方式,实现添加子属性。

    1. var vm = avalon.define({
    2. $id: "test",
    3. placehoder: {}
    4. });
    5. setTimeout(function () {
    6. vm.placehoder = {//我们必须要通过 = ,直接添加一个对象来添加子属性, 不能
    7. aaa: 1, //vm.placehoder.aaa =1; vm.placehoder.bbb = 2这样分散地添加子属性
    8. bbb: 2
    9. }
    10. }, 1000)

    但在firefox4与chrome50后,浏览器支持Proxy对象,可以完美监听对象的增删改查,因此可以动态添加属性或方法

    大家可以在浏览器控制台下敲入window.Proxy来知晓支持情况,也可以通过avalon.config.inProxyMode来开闭此特性. 这个模式暂时只用于avalon.modern.js文件

    VM中的数据更新,只能通过 = 赋值方式实现。但要注意在IE6-8,由于VM是一个VBScript对象,为VM添加新属性会抛错, 因此我们想批量更新属性要时格外小心了,需要用hasOwnProperty进行过滤。

    注意在IE6-8 下,err是VBscript的关键字,VM中存在这个字段,就会将VM中的其他数组变成字符串,详见这里

    监控属性

    此外, 改变监控属性的值还会触发对应的$watch监听回调.

    计算属性

    计算属性是监控属性的强化版,它必须依赖于1个或多个监控属性。通过普通的监控属性实现对视图的监听,它自身的变化也由监控属性进行驱动。

    计算属性集中定义在$computed对象中。有多种形式。

    1. //对象形式的可读写计算属性
    2. avalon.define({
    3. $id: 'test',
    4. firstName: '333',
    5. lastName: 'xxx',
    6. $computed: {
    7. //fullName依赖于firstName与lastName
    8. fullName: {
    9. get: function(){
    10. return this.firstName+' '+this.lastName
    11. },
    12. set: function(val){
    13. var arr = val.split(' ')
    14. this.firstName = arr[0]
    15. this.lastName = arr[1]
    16. }
    17. }
    18. }
    19. })

    例子(请学完组件再看)

    1. <div ms-controller="avalon">
    2. {{@test1}}
    3. <tr>
    4. <td>
    5. <ul>
    6. <li ms-for="el in @communities">{{el.community_name}}</li>
    7. </ul>
    8. </td>
    9. <td>
    10. <wbr ms-widget="{is:'ms-autocomplete', $id: 'aaa', name: 'community_id', communities :@communities}" />
    11. </td>
    12. </tr>
    13. </div>
    14. </div>
    15. avalon.component('ms-autocomplete', {
    16. template: '<div><input type="text" ms-duplex-string="@search" />' +
    17. '<ul><li ms-for="($idx,opt) in @aaa">' +
    18. '{{opt.community_name}}</li></ul></div>',
    19. defaults: {
    20. search: '',
    21. communities: [],
    22. onReady:function(e){
    23. e.vmodel.$watch('search', function(v){
    24. avalon.log('current search word is '+ v)
    25. })
    26. },
    27. $computed: {
    28. aaa: {
    29. get: function() {
    30. var ret = [];
    31. for (var i = 0; i < this.communities.length; i++) {
    32. if ((this.communities[i].community_name.indexOf(this.search) > -1)) {
    33. ret[ret.length] = this.communities[i];
    34. if(ret.length === 5){
    35. break
    36. }
    37. }
    38. }
    39. return ret;
    40. }
    41. }
    42. }
    43. }
    44. });
    45. communities = [{
    46. community_id: 3,
    47. community_name: 'This',
    48. }, {
    49. community_id: 5,
    50. community_name: 'isnot',
    51. }, {
    52. community_id: 8,
    53. community_name: 'agood',
    54. }, {
    55. community_id: 10,
    56. community_name: 'example',
    57. }, {
    58. community_id: 22,
    59. }, {
    60. community_id: 23,
    61. community_name: 'such',
    62. }, {
    63. community_id: 43,
    64. community_name: 'test',
    65. }, {
    66. community_name: 'thank',
    67. }, {
    68. community_id: 47,
    69. community_name: 'you',
    70. }, {
    71. community_id: 50,
    72. community_name: 'verymuch',
    73. }, {
    74. community_id: 51,
    75. community_name: 'youre',
    76. }, {
    77. community_id: 53,
    78. community_name: 'welcome',
    79. }, {
    80. community_id: 54,
    81. community_name: 'too',
    82. }, {
    83. community_id: 55,
    84. community_name: 'notsogood',
    85. }, {
    86. community_id: 56,
    87. community_name: 'cheerful',
    88. }];
    89. var vm = avalon.define({
    90. $id: 'avalon',
    91. test1: 'test1',
    92. communities: communities,
    93. });
    94. </script>

    操作此数组的方法会同步视图的特殊数组,它是由VM中的数组自动转换而来。方便与ms-repeat, ms-each配合使用, 能批量同步一大堆DOM节点。

    监控数组的方法与普通数组没什么不同,它只是被重写了某一部分方法,如 pop, shift, unshift, push, splice,sort, revert。其次添加了四个移除方法,remove, removeAt, removeAll, clear, 及ensure,pushArray,set方法。

    • pushArray(el), 要求传入一数组,然后将它里面的元素全部添加到当前数组的末端。
    • remove(el), 要求传入一元素,通过全等于比较进行移除。
    • removeAt(index), 要求传入一数字,会移除对应位置的元素。
    • removeAll(arrayOrFunction), 有三种用法,如果是一个函数,则过滤比较后得到真值的元素, 如果是一数组,则将此数组中与原数组相等于的元素全部移除;如果没有任何参数,则全部清空。
    • clear(),相当于removeAll()的第三种方法,清空数组的所有元素。由于需要同步视图的缘故,不能通过vm.array.length = 0的方法来清空元素。
    • ensure(el),只有当数组不存在此元素时,才添加此元素。
    • set(index, el),用于更新某一索引位置中的元素,因为简单数组元素的数组,是不会转换它的元素。
    • toJSON(), 用于取得数组的$model, 2.2.2新添加的方法

    注意,修改某个数组元素必须使用set方法. 如果是修改对象数组的某个元素的属性可以用vm.array[1].prop = 'newValue'

    1. <body ms-controller="test">
    2. <div ms-for="el in @arr">
    3. {{el}}<button type="button" ms-click="@arr.remove(el)">点我删除该行</button>
    4. </div>
    5. <script>
    6. avalon.define({
    7. $id: 'test',
    8. arr: [1,2,3,4,5,6]
    9. })
    10. </script>
    11. </body>

    非监控属性

    这包括框架添加的$id, $events, $model属性, $fire, $watch, $render方法, 及用户自己设置的以$开头的属性,放在$skipArray数组中的属性,值为函数、各种DOM节点的属性, 总之,改变它们的值不会产生同步视图的效果。

    $watch方法

    在avalon早期是, 存在一个对象能mixin进每个VM,让VM具有$watch, $unwatch, $fire, $events等方法或属性. 这有点像jQuery的on, off, trigger方法,只是为了更造近angular等MVVM框架,名字起成这样.

    它能监听子级对象的属性变化,能监听对象数组的属性变化(如[{a:1,b:2}]变成[{a:'change',b:2}]), 还有数组的长度属性变化

    1. var unwatch = vm.$watch("aaa", function observe(a, b) {
    2. expect(a).to.be(6)
    3. expect(b).to.be(2)
    4. })
    5. `

    当解除监听后,以后aaa属性的值再发生变化,那么observe方法就不会再执行. 注意unwatch不等于observe

    $watch方法供与其他操作DOM的库一起使用的,如富文本编辑器什么. 在$watch回调里更新VM自身的属性是非常危险的事,很容易引发死循环

    $fire可以传多个参数, 第一个参数为事件名,或者说是VM上已存在的属性名, 当VM中对应的属性发生变化时,框架内部就调用$fire方法, 依次传入属性名,当前属性值,过去属性值。

    数据模型

    是指VM中的$model属性,它是一个纯净的javascript对象,去掉$id, $watch等方法或属性,可以直接通过$.ajax提交给后端,当然我们 还可以通过JSON.parse(JSON.stringify(vm.$model))干掉里面的所有函数。

    注意,不要修改$model,你只能通过VM来改动$model,否则在1.5中,$model是只读的,每次都是返回一个全新的对象给你 你改了也没有用!

    vm是如何作用视图

    我们需要在页面上,使用ms-controller或ms-important来圈定每个vm的作用范围。当页面domReady时,vm就将自动将其里面的数据替换到各种指令中去,实现视图刷新效果。

    由于test这个vm拥有一个叫$element的属性,它是保存其关联的元素节点,如果定义了多少个,那么它会保留最后的那个DIV。以后它的属性变化,只会作用最后的那个DIV。

    avalon之所以使用Proxy, Object.defineProperty或VBScript来构造vm,那是因为它们创建出来的对象有一种自省机制,能让我们得知vm正在操作或访问了我们的对象。

    对于Object.defineProperty或VBScript,主要是靠将普通属性变成访问器属性。访问器属性内部是拥有两个方法,setter与getter。当用户读取对象的属性时,就将调用其getter方法,当用户为此属性赋值时,就会调用setter方法。因此,我们就不需要像angular那样,使用脏检测,就得知对象被修改了某些属性了。并且能准确得知那些属性,及时地同步视图的相应区域,实现最小化刷新视图。

    插值表达式里的内容与 ms-* 的属性值都会转换求值函数, 比如说 变成</code>function(){return <strong>vmodel</strong>.aaa}<code>ms-attr="{title: @name}" 变成 function(){ return {title: vmodel.name} } 此外,请不要出现 @aaa[@bbb].ddd, @eee[ddd], 可能导致依赖收集失败, 无法更新对应区域

    对于Proxy(智能代理),这最早发迹于firefox4,现在许多新浏览器都支持,它能监听外部用户对它的14种,比如说读写属性,调用方法,删除旧属性,添加新属性,被for in循环, 被in关键字进行存在性检测, 被new……因此之前所说的,不能监听没预先定义的属性, 这个难题被Proxy搞定了。

    当我们得知vm的属性发生变化了,如何更新视图呢?在avalon2中,这个是由来处理。

    虚拟DOM比较复杂,大家看不懂可以略过。