为什么设置 optionsValue 会中断 Knockout 更新?
Why does setting an optionsValue break Knockout updating?
我一直在浏览 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 一个不同的 "Meal" 来自乘客的下拉菜单时,"Surcharge" 值 不是 更新。这样做的原因似乎是我将 optionsValue: 'mealVal'
添加到 select
的 data-bind
属性中,当我删除它时,"Surcharge" 确实在新下拉列表时更新选项是 selected。但是为什么添加 optionsValue
会破坏更新?所做的只是设置 select
列表的选项 value
属性,这对表单提交非常有用 - 我不明白为什么它应该阻止 Knockout 自动更新。
更新: 经过进一步调查,我发现 formattedPrice
fn 仍在被调用,但 self.meal()
现在正在解析为值字符串,例如 PRM
而不是整个膳食对象。但这是为什么呢?文档说 optionsValue
在 HTML 中设置了 value
属性,但没有说明任何关于更改视图模型行为的内容。
我认为发生的事情是,当您指定 options: $root.availableMeals
但不指定 optionsValue
时,Knockout 会神奇地确定您创建的列表中的哪个 selection当 selection 更改并允许您从 availableMeals
访问 object 而不仅仅是放入 value
属性的字符串值时.这似乎没有很好的记录。
我认为您了解发生了什么以及为什么它会破坏您的代码,但仍在寻找关于何时实际需要使用 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 重新呈现 select 框的选项
- Knockout 检查
selectedMeal() === mealObject
的新值
- Knockout没有找到
selectedMeal
中的对象,默认第一个选项
- Knockout 将新对象的引用写入
selectedMeal
问题:您丢失了 UI selection,因为它指向的对象不再在可用选项中。
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
的一个属性可用,这是非常有限的。如果您希望您的应用程序能够更新其数据,那么您必须在此处执行一些额外的工作。您的两个选择:
- 独立存储 selection 和 实际对象的字符串(散列),或者
- 拥有视图模型存储库,当新服务器数据到达时,映射到现有实例以确保保持 select离子状态。
如果有帮助,我可以添加代码片段来更好地解释这两种方法
好的,在查看了 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;
啊哈!所以它存储自己的 "has DOM data expando property" 标志,如果设置它,它不会获得简单的 element.value
,但它会进入自己的 JavaScript 内存并获取值。这就是它如何 return 一个复杂的 JS 对象(就像我的问题示例中的 meal 对象)而不仅仅是 value
属性字符串。但是,如果未设置该标志,它确实只是 return 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
的 value
属性。
我个人认为这应该清楚地记录下来,因为它有点出乎意料且特殊,可能会造成混淆,即使它很方便。我会要求他们将其添加到文档中。
我一直在浏览 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 一个不同的 "Meal" 来自乘客的下拉菜单时,"Surcharge" 值 不是 更新。这样做的原因似乎是我将 optionsValue: 'mealVal'
添加到 select
的 data-bind
属性中,当我删除它时,"Surcharge" 确实在新下拉列表时更新选项是 selected。但是为什么添加 optionsValue
会破坏更新?所做的只是设置 select
列表的选项 value
属性,这对表单提交非常有用 - 我不明白为什么它应该阻止 Knockout 自动更新。
更新: 经过进一步调查,我发现 formattedPrice
fn 仍在被调用,但 self.meal()
现在正在解析为值字符串,例如 PRM
而不是整个膳食对象。但这是为什么呢?文档说 optionsValue
在 HTML 中设置了 value
属性,但没有说明任何关于更改视图模型行为的内容。
我认为发生的事情是,当您指定 options: $root.availableMeals
但不指定 optionsValue
时,Knockout 会神奇地确定您创建的列表中的哪个 selection当 selection 更改并允许您从 availableMeals
访问 object 而不仅仅是放入 value
属性的字符串值时.这似乎没有很好的记录。
我认为您了解发生了什么以及为什么它会破坏您的代码,但仍在寻找关于何时实际需要使用 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 重新呈现 select 框的选项
- Knockout 检查
selectedMeal() === mealObject
的新值
- Knockout没有找到
selectedMeal
中的对象,默认第一个选项 - Knockout 将新对象的引用写入
selectedMeal
问题:您丢失了 UI selection,因为它指向的对象不再在可用选项中。
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
的一个属性可用,这是非常有限的。如果您希望您的应用程序能够更新其数据,那么您必须在此处执行一些额外的工作。您的两个选择:
- 独立存储 selection 和 实际对象的字符串(散列),或者
- 拥有视图模型存储库,当新服务器数据到达时,映射到现有实例以确保保持 select离子状态。
如果有帮助,我可以添加代码片段来更好地解释这两种方法
好的,在查看了 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;
啊哈!所以它存储自己的 "has DOM data expando property" 标志,如果设置它,它不会获得简单的 element.value
,但它会进入自己的 JavaScript 内存并获取值。这就是它如何 return 一个复杂的 JS 对象(就像我的问题示例中的 meal 对象)而不仅仅是 value
属性字符串。但是,如果未设置该标志,它确实只是 return 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
的 value
属性。
我个人认为这应该清楚地记录下来,因为它有点出乎意料且特殊,可能会造成混淆,即使它很方便。我会要求他们将其添加到文档中。