为什么设置optionsValue会中断更新?

时间:2018-07-02 11:20:20

标签: javascript html mvvm knockout.js

我一直在研究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());

当我运行此示例并从下拉菜单中为乘客选择其他“餐”时,“附加费”值将更新。造成这种情况的原因似乎是,我在optionsValue: 'mealVal'的{​​{1}}属性中添加了data-bind,当我删除该属性时,当添加了新的下拉选项时,“附加费”确实会更新已选择。但是,为什么添加select会中断更新?所有操作都设置了optionsValue列表的选项select属性,这对于表单提交非常有用-我不明白为什么它应该阻止Knockout自动更新。

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

我认为这是在您指定optionsValue而不指定value时,淘汰赛神奇地确定了更改选择后您在列表中选择的哪个并给出您可以从options: $root.availableMeals访问 object ,而不仅仅是访问optionsValue属性中输入的字符串值。似乎没有充分的证据。

2 个答案:

答案 0 :(得分: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中的对象时会发生什么:

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

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

optionsValue来营救!

optionsValue使我们能够解决此问题。我们没有存储可能随时替换的 object 引用,而是存储了一个 primitive 值,即mealVal中的字符串,它使我们可以检查在不同的API调用之间实现平等!淘汰赛现在可以执行以下操作:

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. 具有视图模型存储库,当新服务器数据到达时,映射到现有实例以确保保留选择状态。

如果有帮助,我可以添加代码片段以更好地说明这两种方法

答案 1 :(得分:1)

好吧,在查看了Knockout代码之后,我已经知道发生了什么-截至撰写本文时,这还没有记录。

value绑定在读取select元素的值时,不仅查看该元素的DOM值,还查看了元素的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数据expando属性”标志,如果设置了该标志,它不会获得简单的element.value,但是它会进入其自己的JavaScript内存并获取值。这样就可以返回复杂的JS对象(如我的问题示例中的餐对象),而不仅仅是返回value属性字符串。但是,如果未设置该标志,则确实会返回value属性字符串。

可以预见,writeValue扩展名的另一面是将复杂数据(如果不是字符串)写入JS内存,否则它将仅存储在value属性中option的字符串:

    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只是从option的{​​{1}}属性。

我个人认为这应该清楚地记录在案,因为这是一些意想不到的特殊行为,即使很方便也可能会造成混淆。我会要求他们将其添加到文档中。