【问题标题】:Why does setting an optionsValue break Knockout updating?为什么设置 optionsValue 会中断 Knockout 更新?
【发布时间】:2018-12-10 14:42:13
【问题描述】:

我一直在阅读 Knockout 教程,当有什么让我困惑时,我正在玩一个教程。这是我的 HTML:

<h2>Your seat reservations</h2>

<table>
    <thead><tr>
        <th>Passenger name</th><th>Meal</th><th>Surcharge</th>
    </tr></thead>
    <tbody data-bind="foreach: seats">
        <tr>
            <td><input data-bind="value: name" /></td>
            <td><select data-bind="options: $root.availableMeals, optionsValue: 'mealVal', optionsText: 'mealName', value: meal"></select></td>
            <td data-bind="text: formattedPrice"></td>
        </tr>    
    </tbody>
</table>

<button data-bind="click: addSeat">Reserve another seat</button>

...这是我的 JavaScript:

// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);

    self.formattedPrice = ko.computed(function() {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";        
    });
}

// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealVal: "STD", mealName: "Standard (sandwich)", price: 0 },
        { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 },
        { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }
    ];    

    // Editable data
    self.seats = ko.observableArray([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);

    // Operations
    self.addSeat = function() {
        self.seats.push(new SeatReservation("", self.availableMeals[0]));
    }
}

ko.applyBindings(new ReservationsViewModel());

当我运行此示例并从下拉菜单中为乘客选择不同的“餐食”时,“附加费”值更新。此原因似乎是我在select 987654326 selectselect,而且当我删除时,在选择新的下拉选项时,“额外收费”确实更新。但是为什么添加optionsValue 会破坏更新?所做的只是设置select 列表的选项value 属性,这对于表单提交非常有用——我不明白为什么它会阻止Knockout 自动更新。

更新:经过进一步调查,我发现 formattedPrice fn 仍在被调用,但 self.meal() 现在解析为值字符串,例如 PRM 而不是全餐对象。但这是为什么呢?文档说 optionsValue 在 HTML 中设置了 value 属性,但没有说明更改视图模型的行为。

我认为发生的情况是,当您指定 options: $root.availableMeals 但未指定 optionsValue 时,Knockout 会神奇地确定您在更改选择时所做的列表中的哪个选择,并允许您访问object 来自availableMeals 而不仅仅是放入value 属性的字符串值。这似乎没有很好的记录。

【问题讨论】:

  • 在文档here 中有一小段解释optionsValue。 “通常,您只想使用 optionsValue 来确保 KO 在更新可用选项集时能够正确保留选择。”如果您自己的更新回答了您的问题,我建议将其发布为答案。如果您还有任何问题,请告诉我/我们还有什么不清楚的地方。
  • 这仍然不清楚,因为它没有解释为什么当您设置optionsValue 时,formattedPrice 函数从传入的全餐对象变成了 optionsValue 值。为什么不继续获取全餐对象?

标签: javascript html mvvm knockout.js


【解决方案1】:

我认为您了解正在发生的事情以及它为何会破坏您的代码,但仍在寻找有关何时真正需要使用 optionsValue 以及何时不需要的解释。

何时使用optionsValue 绑定

假设你的饭菜已经卖完了,你想在availableMeals 中向服务器查询更新:

const availableMeals = ko.observableArray([]);
const loadMeals = () => getMeals().then(availableMeals);
const selectedMeal = ko.observable(null);

loadMeals();

ko.applyBindings({ loadMeals, availableMeals, selectedMeal });

function getMeals() {
  return {
    then: function(cb) {
      setTimeout(cb.bind(null, [{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 }, { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 }, { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }]), 500);  
    }
  }

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<select data-bind="options: availableMeals, 
                   value: selectedMeal,
                   optionsText: 'mealName'"></select>
                   
<button data-bind="click: loadMeals">refresh meals</button>

<div data-bind="with: selectedMeal">
  You've selected <em data-bind="text: mealName"></em>
</div>

<div data-bind="ifnot: selectedMeal">No selection</div>

<p>Make a selection, click on refresh and notice the selection is lost when new data arrives.</p>

当你替换availableMeals中的对象时会发生什么:

  • Knockout 重新呈现选择框的选项
  • 敲除检查selectedMeal() === mealObject 的新值
  • Knockout 在selectedMeal 中找不到对象,默认为第一个选项
  • Knockout 将新对象的引用写入selectedMeal

问题:您失去了 UI 选择,因为它指向的对象不再在可用选项中。

optionsValue 救援!

optionsValue 允许我们解决这个问题。我们不是存储对可能随时替换的object的引用,而是存储一个primitive值,即mealVal中的字符串,它允许我们检查是否相等在不同的 API 调用之间! Knockout 现在执行如下操作:

selection = newObjects.find(o => o["mealVal"] === selectedMeal());

让我们看看实际效果:

const availableMeals = ko.observableArray([]);
const loadMeals = () => getMeals().then(availableMeals);
const selectedMeal = ko.observable(null);

loadMeals();

ko.applyBindings({ loadMeals, availableMeals, selectedMeal });

function getMeals() {
  return {
    then: function(cb) {
      setTimeout(cb.bind(null, [{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 }, { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 }, { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }]), 500);  
    }
  }

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<select data-bind="options: availableMeals, 
                   value: selectedMeal,
                   optionsText: 'mealName',
                   optionsValue: 'mealVal'"></select>
                   
<button data-bind="click: loadMeals">refresh meals</button>

<div data-bind="if: selectedMeal">
  You've selected <em data-bind="text: selectedMeal"></em>
</div>

<div data-bind="ifnot: selectedMeal">No selection</div>

<p>Make a selection, click on refresh and notice the selection is lost when new data arrives.</p>

optionsValue 的缺点

注意我是如何重写with 绑定的?突然,我们的视图模型中只有meal 的一个属性可用,这是非常有限的。如果您希望您的应用程序能够更新其数据,您必须在此处做一些额外的工作。您的两种选择:

  1. 存储您选择的字符串(散列)独立的实际对象,或者
  2. 拥有一个视图模型存储库,当新的服务器数据到达时,映射到现有实例以确保您保持选择状态。

如果有帮助,我可以添加代码 sn-ps 来更好地解释这两种方法

【讨论】:

  • 我没看到你在哪里重写了with 绑定?而且,是的,添加代码 sn-ps 确实会有所帮助。
【解决方案2】:

好的,在查看了 Knockout 代码之后,我已经弄清楚发生了什么 - 在撰写本文时,这还没有记录。

value 绑定,当它读取 select 元素的值时,不只是查看元素的 DOM 值; it calls var elementValue = ko.selectExtensions.readValue(element);

现在,selectExtensions 所做的是为select(及其子object)元素实现特殊行为。这就是魔法发生的地方,因为正如代码中的注释所说:

    // Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values
    // are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values
    // that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns.

所以,当值绑定试图读取select元素via selectExtensions.readValue(...)时,会来到这段代码:

        case 'select':
            return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;

这基本上是说“好的,找到所选索引并再次使用此函数读取该索引处的option元素。然后它读取option元素并得出以下结论:

        case 'option':
            if (element[hasDomDataExpandoProperty] === true)
                return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
            return ko.utils.ieVersion <= 7
                ? (element.getAttributeNode('value') && element.getAttributeNode('value').specified ? element.value : element.text)
                : element.value;

啊哈!所以它存储了自己的“具有 DOM 数据扩展属性”标志,如果设置了它,它不会得到简单的element.value,但它会进入自己的 JavaScript 内存并获取值。这就是它如何返回一个复杂的 JS 对象(就像我的问题示例中的饭菜对象)而不仅仅是 value 属性字符串。但是,如果没有设置该标志,它确实只返回 value 属性字符串。

writeValue 扩展,可以预见的是,如果它不是字符串,它会将复杂数据写入 JS 内存,否则它只会将其存储在 value 属性字符串中。 987654341@:

    switch (ko.utils.tagNameLower(element)) {
        case 'option':
            if (typeof value === "string") {
                ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined);
                if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node
                    delete element[hasDomDataExpandoProperty];
                }
                element.value = value;
            }
            else {
                // Store arbitrary object using DomData
                ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
                element[hasDomDataExpandoProperty] = true;

                // Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value.
                element.value = typeof value === "number" ? value : "";
            }
            break;

是的,正如我所怀疑的那样,Knockout 在幕后存储了复杂的数据,但只有当您要求它存储复杂的 JS 对象时。这就解释了为什么当你不指定 optionsValue: [someStringValue] 时,你的计算函数接收到复杂的饭菜对象,而当你指定它时,你只得到传入的基本字符串 - Knockout 只是给你来自 @987654344 的字符串@ 的 value 属性。

我个人认为这应该被清楚地记录下来,因为它有点出乎意料和特殊的行为,可能会让人困惑,即使它很方便。我会要求他们将其添加到文档中。

【讨论】:

  • 很高兴看到您深入挖掘源代码并找出确切的行为!就个人而言,我希望看到optionsValueoptionsEqualityComparer 绑定替换:传递(a, b) -&gt; boolean 的函数,用于将value 匹配到options 的元素.
猜你喜欢
  • 1970-01-01
  • 2012-01-02
  • 1970-01-01
  • 2014-04-21
  • 1970-01-01
  • 2013-10-23
  • 2023-03-10
  • 2013-02-19
  • 1970-01-01
相关资源
最近更新 更多