具有可观察对象的 Knockout 组件不更新数据

Knockout component with observable object doesn't update data

我有以下组件:

<template id="fruits-tpl">
    <p>Name: <input data-bind="value: name" /></p>
    <p>Type: <input data-bind="value: color" /></p>
</template>


ko.components.register('fruits', {
    viewModel: function(params) {
        this.name = params.name;
        this.color   = params.color;
    },
    template: { element: 'fruits-tpl' }
});

我将此组件与下面的视图模型一起使用,其中我的可观察列表中的项目具有不同的类型和不同的属性:

function Fruit(data) {
    this.name = ko.observable(data.name);
    this.color = ko.observable(data.color);
}
function Dessert(data) {
    this.name = ko.observable(data.name);
    this.packaging = ko.observable(data.packaging);
}
function Vm(){
    var data = [{name:"Apples",color:"Yellow"},{name:"Cookies",packaging:"Box"}];
    this.items = ko.observableArray([new Fruit(data[0]),new Dessert(data[1])]);
    this.items.choice = ko.observable(this.items()[0]);
}

此组件运行良好,每次我更改输入框中的文本时都会更新基础数据:

<div data-bind="component: {name: 'fruits', params: items.choice}"></div>

现在,我想将我的可观察对象的逻辑封装到组件本身中,所以我这样更改了组件:

ko.components.register('fruits', {
    viewModel: function(params) {
        this.name = ko.observable(params.name);
        this.color   = ko.observable(params.color);
    },
    template: { element: 'fruits-tpl' }
});

... 现在我有了我的可观察 items.choice,里面只有数据:

function Vm(){
    var data = [{name:"Apples",color:"Yellow"},{name:"Cookies",packaging:"Box"}];
    this.items = ko.observableArray(data);
    this.items.choice = ko.observable(this.items()[0]);
}

为什么在我的第二个示例中主视图模型中的基础数据没有更新,尽管 items.choice 仍然可以观察到?我确信我遗漏了一些概念,也许我的可观察数组中的每个项目也应该是可观察的,但我不明白是否有办法让第二个例子起作用。

第一个例子:http://jsfiddle.net/5739ht0q/2/ 第二个例子:http://jsfiddle.net/079tx0nn/

Observables 是 knockout 的核心,它允许视图和 ViewModel 之间的双向绑定。要更新组件的 JSON 字段,需要更改 'VM' 有权访问的可观察对象。

为了避免定义 Fruit 和 Dessert 函数,您可以使用 Knockout mapping 将数据数组转换为 ko.observableArray。这会将数组中每个对象的字段设置为可观察对象,以便将它们传递给组件绑定。

有几种方法可以将数据更新回主视图模型,如下所示。

但首先,让我们稍微改进一下主视图模型。

function Vm(data) {
  var self = this;
  self.items = ko.observableArray(ko.utils.arrayMap(data, function(item) {
    return ko.observable(item);
  }));
  self.items.choice = ko.observable(0);
  self.items.choice.data = ko.computed(function() {
    return self.items()[self.items.choice()];
  });
}

快速而肮脏

第一种快速但肮脏的方法是向组件传递一个已在主视图模型中定义的函数:

<div data-bind="component: {
    name: 'fruits',
    params: {index: items.choice(), data: items.choice.data(), update: items.update}
}"></div>

在组件内部我们可以调用函数来保存更改:

self.data = ko.computed(function(){
    params.update(params.index, ko.toJS(self));
});

主视图模型的更新功能已经很明显了,不用多说了。

使用订阅

第二种方法是使用可订阅对象沿着视图模型建立通信:

ko.intramodels = new ko.subscribable();

从组件发送通知:

self.data = ko.computed(function(){
  ko.intramodels.notifySubscribers(ko.toJS(self), "updateFruits");
});

主视图模型中的订阅将接收并保存更改,大致如下所示:

  ko.intramodels.subscribe(function(newValue) {
    self.items.replace(self.items()[self.items().index], newValue);
  }, self, "updateFruits");

当然这可以像上面那样手动完成,但是伟大的 Ryan Niemeyer 的 post 盒子库将是这里的最佳选择:https://github.com/rniemeyer/knockout-postbox.

我测试了这两种解决方案,但不幸的是,当我激活 - 淘汰赛 3.4 中的新功能 - 延迟更新选项时遇到了一些麻烦:ko.options.deferUpdates = true; 因为我收到了 "Maximum call stack size exceeded" 错误。

移除循环依赖

因为不想放弃和错过knockout 3.4这个新的精彩性能增强,也因为这个错误也多多少少有些意思,

Circular dependencies are a design error, please rethink some part of your implementation,

我更改了视图模型以保持依赖跟踪链只在一个方向上工作,但对整个组件数据只使用一个可观察对象:

ko.components.register('fruits', {
  viewModel: function(params) {
    var self = this;
    self.data = params.peek();
    self.item = {};
    self.item.name = ko.observable(self.data.name);
    self.item.color = ko.observable(self.data.color);
    self.update = ko.computed(function() {
       params(ko.toJS(self.item));
    });
  },
  template: {
    element: 'fruits-tpl'
  }
});

这在嵌套组件中更为明显,嵌套组件在其中具有所有数据处理和可观察创建,并且主视图模型不必知道子项内部的任何信息以及为什么——只传递和接收一个可观察对象:

<div data-bind="component:{name:'fruits',params:items.choice.data}"></div>

<template id="containers-tpl">
  <div data-bind="foreach: containers">
    <p><input data-bind="textInput: quantity"><span data-bind="text: name"></span></p>
  </div>
</template>

<template id="fruits-tpl">
  <p>Name:<input data-bind="textInput: item.name"></p>
  <p>Color:<input data-bind="textInput: item.color"></p>
  <div data-bind="component:{name:'containers',params:item.containers}"</div>
</template>

这里的要点是:

  • 将一个可观察对象传递给组件,而不仅仅是数据,
  • 打破组件内部参数的依赖链.

使用嵌套组件完成 Fiddle:http://jsfiddle.net/jo37q7uL/