视图模型(View Models)和数据绑定

数据绑定和 视图模型(ViewModel) 是对 Ext JS 的强有力的补充.
以声明式的书写方式, 它们使您能够用很少的代码做更多的事情, 以便让你集中精力关注其它更重要的逻辑.

视图模型(View Model) 是一个管理数据对象的类. 需要这些数据的组件可以绑定到 View Model, 而且可以在数据发生变化的时候通知组件更新界面. 视图模型(View Model) 和 视图控制器(ViewController)类似, 只被引用了它的 view 所拥有. 因为 ViewModels 和 view 关联, 视图模型可以链接到父视图模型(view的父容器的视图模型). 这样,子视图就可以用从父视图模型“继承”过来的数据.

组件有一个 bind 配置, 此配置允许它们把自身的一些配置项与 ViewModel 的数据关联起来. 使用 bind, 如果数据发生变化, 组件相应的配置项会触发调用 setter 方法 - 而不需要用自定义事件监听函数来处理.

在这篇指南中, 我们会通过一些例子来展示视图模型(View Models)和数据绑定的强大之处.

组件绑定

理解绑定和 ViewModels 的最好的途径应该是看看各种绑定组件的方式. 因为组件是数据绑定的主要"消费者", 在这一点上组件和 Ext JS 开发者有相似之处. 为了实现绑定,我们需要一个 ViewModel, 我们现在先引用它, 待会再定义 ViewModel 类.

绑定 和 配置项(configs)

绑定组件, 其实是把 Ext.app.ViewModel 的数据 和组件的配置项(config) 联系起来的过程. 一个组件的任何配置都可以绑定,只要它有一个 setter 方法. 比如, 因为 Ext.panel.Panel 有个 setTitle() 方法 , 所以你可以绑定title 配置项.

在这个例子中, 我们会根据 ViewModel 的data来设置 panel 的width. 我们把数据绑定到 width 上,因为 Ext.panel.PanelsetWidth() 方法.

Ext.create('Ext.panel.Panel', {
    title: 'Simple Form',

    viewModel: {
        type: 'test'  // we will define the "test" ViewModel soon
    },

    bind: {
        html: '<p>Hello {name}</p>',
        width: '{someWidth}'
    }
});

绑定值的语法和 Ext.Template非常像. 绑定的字段放在大括号内. 你也可以用 formatters 格式化值, 类似Ext.Template. 不过,和 Ext.Template不同的是, 如果绑定模板是单个的标记(比如‘{someWidth}’), 那么绑定的值是原样传递的, 不会转换为字符串

稍后我们会看到 namesomeWidth的数据是如何定义的. 上面的例子只是简单演示组件如果使用数据.

绑定 布尔类型(Boolean) 配置项

有很多配置项的值是布尔类型(Boolean), 比如 visible (或 hidden), disabled, checked, 和 pressed. 只有绑定模板支持布尔否定形式的“内联”表达式. 其它的形式则应该归为 方程(formulas) (见下文), 只不过布尔反转很常见, 所以对它有特殊的规定. 例如:

Ext.create('Ext.panel.Panel', {
    title: 'Simple Form',

    viewModel: {
        type: 'test'
    },

    items: [{
        xtype: 'button',
        bind: {
            hidden: '{!name}'  // 否定
        }
    }]
});

此处再次强调了单个标记模板中的值, 是不会转换为字符串的. 上面的例子中, 因为 “name”是个字符串值, 使用 “!”否定反转,变成了 boolean 值,然后传递给了 button 的 setHidden 方法.

绑定和优先级

绑定配置项总是会覆盖掉静态写的配置项值. 也就是说, 绑定数据的优先级比静态配置值要高, 但是为了获取数据可能会延迟绑定。

Ext.create('Ext.panel.Panel', {
    title: 'Simple Form',

    viewModel: {
        type: 'test'
    },

    bind: {
        title: 'Hello {name}'
    }
});

只要传递了值给绑定的“name”, “Simple Form” 的 title 就会被替换.

绑定和子组件

绑定的一个很有用的部分就是, 拥有 viewModel 的容器中, 它的所有子组件也可以访问该容器的数据.

这个例子中, 你可以看到 form 的子 items 绑定到了父容器的 viewModel.

Ext.create('Ext.panel.Panel', {
    title: 'Simple Form',

    viewModel: {
        type: 'test'
    },

    layout: 'form',
    defaultType: 'textfield',

    items: [{
        fieldLabel: 'First Name',
        bind: '{firstName}' // 使用父容器的 "test" ViewModel
    },{
        fieldLabel: 'Last Name',
        bind: '{lastName}'
    }]
});

双向绑定

bind 配置项支持双向绑定, 也就是 view 和 viewModel 之间数据是实时同步的. View 里面的数据变更都会自动写回 viewModel. 而且也会自动更新绑定了此数据的其它组件. 注意: 并不是所有的配置项都会在发生变更时把它们的值 反映(publish) 给 ViewModel.
只有定义在 publishtwoWayBindable 数组中的 配置项才会把变更 反映(publish) 给 ViewModel. 也可以用 publishState 函数把值的变更反映给 ViewModel.

上面的例子中, 因为“firstName”和 “lastName” 属性绑定到了 文本框(text fields), 文本框中内容的改变会写回 ViewModel. 为了了解它们是如果工作的, 我们来完善这个例子并定义这个 ViewModel.

Ext.define('MyApp.view.TestViewModel', {
    extend: 'Ext.app.ViewModel',

    alias: 'viewmodel.test', // 下面会关联到这个 viewModel/type

    data: {
        firstName: 'John',
        lastName: 'Doe'
    },

    formulas: {
        // 我们很快会解释关于 formulas 的详细内容
        name: function (get) {
            var fn = get('firstName'), ln = get('lastName');
            return (fn && ln) ? (fn + ' ' + ln) : (fn || ln || '');
        }
    }
});

Ext.define('MyApp.view.TestView', {
    extend: 'Ext.panel.Panel',
    layout: 'form',

    // Always use this form when defining a view class. This
    // allows the creator of the component to pass data without
    // erasing the ViewModel type that we want.
    viewModel: {
        type: 'test'  // 引用别名 "viewmodel.test"
    },

    bind: {
        title: 'Hello {name}'
    },

    defaultType: 'textfield',
    items: [{
        fieldLabel: 'First Name',
        bind: '{firstName}'
    },{
        fieldLabel: 'Last Name',
        bind: '{lastName}'
    },{
        xtype: 'button',
        text: 'Submit',
        bind: {
            hidden: '{!name}'
        }
    }]
});

Ext.onReady(function () {
    Ext.create('MyApp.view.TestView', {
        renderTo: Ext.getBody(),
        width: 400
    });
});

当上面的 panel 显示出来之后, 我们可以看到 输入框 改变内容会反映到 panel 的 标题(title)上, 也会影响“Submit” 按钮的隐藏/显示(hidden) 状态.

绑定和控件状态

有时候组件的状态, 比如, checkbox 的勾选(checked) 状态, 或者 表格(grid) 选择行, 需要关联到其它的组件. 如果给一个组件赋予了一个 reference 来标识它, 这个组件就会把它自身的某些属性 反映(publish) 给 ViewModel.

下面的例子里, 有一个 label 是“Admin Key”的 文本框(textfield), 它的 disabled 配置项绑定到了 checkbox 的 勾选(checked) 状态上. 结果就是 checkbox 勾选之后, 文本框才变成可输入. 这种行为是非常适合动态表单:

Ext.create('Ext.panel.Panel', {
    title: 'Sign Up Form',

    viewModel: {
        type: 'test'
    },

    items: [{
        xtype: 'checkbox',
        boxLabel: 'Is Admin',
        reference: 'isAdmin'
    },{
        xtype: 'textfield',
        fieldLabel: 'Admin Key',
        bind: {
            disabled: '{!isAdmin.checked}'
        }
    }]
});

绑定描述符(Bind Descriptors)

目前为止我们见过绑定描述符的3种基本形式:

  • {firstName} - “直接绑定”到 ViewModel 的某个值. 值原样传递, 类型不变.

  • Hello {name} - “绑定模板” 总是通过插入各种绑定表达式的文本值来产生一个字符串. 绑定模板也可以用 formatters, 类似Ext.Template, 比如: ‘Hello {name:capitalize}’(作用是首字母大写).

  • {!isAdmin.checked} - 直接绑定的否定形式, 用于绑定布尔(boolean)值.

除了这些基本的形式之外,还有一些专门的绑定描述符的形式,可供使用.

多值绑定(Multi-Bind)

如果给绑定描述符的是一个对象或者数组, ViewModel 会生成一个同样结构的对象或数组, 里面的属性值都替换成了绑定结果. 比如:

Ext.create('Ext.Component', {
    bind: {
        data: {
            fname: '{firstName}',
            lname: '{lastName}'
        }
    }
});

这段代码, 给组件的 “data” 配置绑定了一个对象, 对象中两个属性值来自 ViewModel.

Record 绑定

但需要绑定一个特定的 record, 比如 id是42的 “User”记录, 绑定描述符需要是一个对象, 对象中有一个“reference” 属性. 例如:

Ext.create('Ext.Component', {
    bind: {
        data: {
            reference: 'User',
            id: 42
        }
    }
});

在这种情况下, 组件的 tpl 会接收到 User 这条记录(record). 需要 requires Ext.data.Session.

关联(Association) 绑定

和 Record 绑定 类似, 也可以绑定到 record 的 关联(关系/外键)(association) 上, 比如和 User 关联的 Address 记录:

Ext.create('Ext.Component', {
    bind: {
        data: {
            reference: 'User',
            id: 42,
            association: 'address'
        }
    }
});

在这种情况下, 组件的 tpl 会接收到 User 这条 record 关联的“address”record. 同样需要 requires Ext.data.Session.

与绑定相关的选项(Bind Options)

当你需要描述绑定设置的时候, 需要用到绑定描述符的最后一种形式. 下面的例子演示了 如何对一个值只绑定一次,之后就自动断开绑定关系.

Ext.create('Ext.Component', {
    bind: {
        data: {
            bindTo: '{name}',
            single: true
        }
    }
});

bindTo 属性是绑定描述符对象中的第二个保留关键字(第一个保留关键字是 reference). 如果对象中有 bindTo 属性, 意味着 bindTo的值是真正的描述符, 而其它属性是用来配置绑定设置的.

另一个可用的绑定选项是 deep. 这个选项表示, 当绑定到一个对象的时候, 对象中任意属性发生变化都会通知绑定, 而并不只是在对象的引用发生变化的时候. 这个通常对绑定 data 配置项比较有用, 因为这个配置项经常接收对象.

Ext.create('Ext.Component', {
    bind: {
        data: {
            bindTo: '{someObject}',
            deep: true
        }
    }
});

创建视图模型(ViewModels)

现在我们已经尝试过了组件如何使用 ViewModels, 也看到过了 ViewModels 的样子, 是时候学习更多关于 ViewModels 的内容了.

如前所述, ViewModel 用来管理底层 data 对象. 它是被绑定语句所使用的对象的内容. 利用 JavaScript 原型链继承, 子 ViewModels 会从父 ViewModels那里继承数据. View Model 内部机制 里面有详细介绍, 但总而言之, 子ViewModel 的 data 对象把父 ViewModel 的 data 对象作为它自身的 原型(prototype).

方程式(Formulas)

除了保持(hold)数据和提供绑定, ViewModels 还提供了一种方便的方法, 叫做 formulas, 来从其他数据计算得到某些数据. Formulas 让你可以在 ViewModel 中封装数据依赖, 让你的焦点关注在 view 的类结构上.

换句话说, ViewModel 里的 data 并没有变化, 但是可以使用 formulas 进行转换,从而显示成不同的结果. 这个和 model 中 field 的 convert 配置很像.

在之前的例子中我们看到了一个简单的“name”这个 formula. 这里的“name” formula 只是一个简单的函数,用于拼接 ViewModel 里“firstName” 和 “lastName”的值.

Formulas 也可以使用其它 formulas 的结果,就像 formulas 的结果就是 data 的一个属性一样. 例如:

Ext.define('MyApp.view.TestViewModel2', {
    extend: 'Ext.app.ViewModel',

    alias: 'viewmodel.test2',

    formulas: {
        x2y: function (get) {
            return get('x2') * get('y');
        },

        x2: function (get) {
            return get('x') * 2;
        }
    }
});

“x2” 这个 formulas 使用了 “x” 属性乘以2得到 “x2”. “x2y”这个 formulas 使用“x2” 和“y”. 这个定义意味着, 如果“x” 发生改变, “x2”会重新计算, 然后“x2y”也会重新计算. 但是如果“y” 变了, 只有 “x2y”会被重新计算.

显式绑定 Formulas

在上面的例子中, formula 所依赖的东西是由函数来检查的, 但这并不总是最好的解决方案. 可以用显式的绑定声明, 将所有的值用一个简单对象返回.

Ext.define('MyApp.view.TestViewModel2', {
    extend: 'Ext.app.ViewModel',

    alias: 'viewmodel.test2',

    formulas: {
        something: {
            bind: {
                x: '{foo.bar.x}',
                y: '{bar.foo.thing.zip.y}'
            },

            get: function (data) {
                return data.x + data.y;
            }
        }
    }
});

双向 Formulas

如果一个 formula 是可逆的, 当我们可以定义一个 set 函数,当值发生改变的时候, 调用set 函数(即双向绑定). 因为“this” 指向的是 ViewModel, 可以调用this.set() 来给 ViewModel 相应的属性赋值.

下面 TestViewModel 修改后的版本展示了如何定义“name” 为双向 formula.

Ext.define('MyApp.view.TestViewModel', {
    extend: 'Ext.app.ViewModel',

    alias: 'viewmodel.test',

    formulas: {
        name: {
            get: function (get) {
                var fn = get('firstName'), ln = get('lastName');
                return (fn && ln) ? (fn + ' ' + ln) : (fn || ln || '');
            },

            set: function (value) {
                var space = value.indexOf(' '),
                    split = (space < 0) ? value.length : space;

                this.set({
                    firstName: value.substring(0, split),
                    lastName: value.substring(split + 1)
                });
            }
        }
    }
});

建议

强大的 ViewModels, formulas, 和数据绑定, 很容易导致滥用这些机制,导致创建出来一个很难理解很难调试的应用程序, 或者更新慢、内存泄漏. 为了避免这些问题, 却又能充分利用 ViewModels, 这里有一些推荐的技术:

  • 尽可能使用下面的形式来配置 viewModel. 主要原因是因为 配置系统(Config System) 合并 config 值的方式. 使用这种形式, “type” 属性在合并过程中会被保留.

    Ext.define('MyApp.view.TestView', {
        //...
        viewModel: {
            type: 'test'
        },
    });
  • 定好名字, 特别是级别高的 ViewModels. 在 JavaScript 我们会依靠文本搜索,所以取可以见名知义的名字. 代码里某个属性用得越多, 取个有意义甚至独一无二的名字就越重要.

  • 除非很必要, 对象中的数据不要嵌套太深. ViewModel 存储多个顶级(top-level)对象,比存储嵌套层次太深的对象,比对(检测数据变化)的次数要少. 此外, 相比较于 许多组件依赖大对象 来说, 这种方式可以使数据绑定的依赖性更明显. 有时候是会共享对象, 但是记住 ViewModel 只是一个管理数据的对象, 你也可以使用它的属性.

  • 使用子 ViewModels, 以便在组件销毁时可以清理数据 . 如果你把所有数据都放在了顶层 ViewModels, 数据可能永远不会被清除, 即使子视图已被销毁. 相反, 应该创建子视图的 ViewModels, 然后把数据存在子视图的 ViewModel 中.

  • 只在必要的时候创建子 ViewModels. 每一个 ViewModel 实例的创建都会耗时、占内存. 如果子视图不需要特有的数据, 它可以简单地使用父容器的 ViewModel. 不过还是要看看上一条建议, 因为创建子 ViewModels, 比把什么都塞进父 ViewModel 要好, 也不容易出现内存泄漏问题.

  • 使用 formulas 代替重复绑定.如果你了解了 formulas 是如何结合多个绑定值的, 你可能会发现使用 formula 可以减少很多重复的绑定. 例如, 有一个 formula 里有3个依赖, 有4个用户使用, 这导致 ViewModel 需要跟踪 3 + 4 = 7 个依赖. 相比较于 4 个用户 分别使用 3 个依赖, 那么最后就产生了 3 * 4 = 12 个依赖. 需要跟踪的依赖少了, 意味着内存占用少了, 耗时也少了.

  • formulas 链不要太深. 这与其说是一个运行时间成本问题, 还不如看作是代码可读性问题. formulas 链会掩盖数据和组件之间的联系, 使得难以理解正在发生的事情.

  • 双向 formulas 必须稳定. 假设 formula “foo” 通过 “bar”的值计算而来. 当“foo”被 set 赋值了, formula 会反转 get 函数 给“bar”赋值. 如果“foo”的 get 函数得到的值和刚刚被 set 的值是一模一样的, 那么这个结果就是稳定的. 如果不稳定, 这个过程会持续循环下去, 直到达到稳定状态, 或者无限期继续下去. 这两种情形都不是令人满意的.

延伸阅读

更多关于 viewModels 的内容, 请看ViewModel 内部机制.

Last updated