Как я могу использовать нокаут $parent/$root pseudovariables изнутри .computed() наблюдаемый?

Внутри выражения knockout.js я могу использовать $data, $parent и $root псевдовариантность. Как я могу получить эквивалент этих псевдовариабелей, когда я использую ko.computed наблюдаемый, объявленный в JavaScript?

У меня есть родительская viewmodel с набором дочерних элементов, а родительская модель просмотра имеет selectedChild наблюдаемую. Учитывая это, я могу использовать выражения для привязки данных, чтобы добавить класс CSS к тому, какой выбран выбранный ребенок:

<ul data-bind="foreach: children">
    <li data-bind="text: name,
                   css: {selected: $data === $root.selectedChild()},
                   click: $root.selectChild"></li>
</ul>
<script>
vm = {
    selectedChild: ko.observable(),
    children: [{name: 'Bob'}, {name: 'Ned'}],
    selectChild: function(child) { vm.selectedChild(child); }
};
ko.applyBindings(vm);
</script>

Но мои модели просмотра будут более сложными, и мне бы хотелось, чтобы я был выбран? чтобы иметь возможность делать больше, чем просто добавлять один класс CSS к одному элементу. Я действительно хочу сделать вычисленное свойство isSelected на дочерней модели, поэтому я могу добавить другие рассчитанные свойства, которые зависят от него.

Я пробовал просто писать JavaScript, который ссылается на $data и $root, на случай, если нокаут может определить эти переменные и каким-то образом их охватить, когда он вызовет мою функцию оценщика computed:

{
  name: 'Bob',
  isSelected: ko.computed(function(){ return $data === $root.selectedChild(); })
}

Но нет такой удачи: внутри моего оценщика function оба $data и $root являются undefined.

Я также попытался использовать ko.contextFor внутри моего оценщика, так как он дает доступ к $data и $root. К сожалению, внутри моей функции оценщика contextFor также всегда возвращает undefined. (В любом случае, я не возлагал большие надежды на эту стратегию - неясно, насколько хорошо нокаут сможет отслеживать зависимости, если бы мне пришлось заходить так за спиной.)

Я всегда мог вручную установить свойство на каждой дочерней модели просмотра, которая ссылается на родительскую модель. Но я знаю, что у нокаута есть возможность сделать это для меня, и я хотел бы хотя бы выяснить, могу ли я использовать его механизмы, прежде чем я начну писать свои собственные.

Кажется, что должно быть возможно перевести указанное выражение привязки к вычисленному наблюдаемому - в конце концов, что уже делает нокаут:

Другой опрятный трюк заключается в том, что декларативные привязки просто реализуются как вычисленные наблюдаемые.

Но как мне поступить с псевдокаруамами $data и $root, когда я пишу свои собственные вычисленные наблюдаемые?

Ответ 1

Псевдоматрицы доступны только в контексте привязки данных. Сама модель представления в идеале не должна знать или иметь какие-либо зависимости от вида, отображающего ее.

Итак, при добавлении вычисленных наблюдаемых в модель представления вы не знаете, как это будет связано (например, то, что будет $root). Модель просмотра или часть модели представления могут даже быть связаны отдельно для нескольких областей страницы на разных уровнях, поэтому псевдопеременные будут отличаться в зависимости от элемента, с которого вы начинаете.

Это зависит от того, что вы пытаетесь выполнить, но если вы хотите, чтобы ваш ребенок имел isSelected вычисленный наблюдаемый, который указывает, совпадает ли этот элемент с выбранным элементом в модели родительского представления, тогда вам нужно будет найти способ сделать родителя доступным для ребенка.

Один из вариантов - передать родителя в функцию-конструктор вашего ребенка. Вам даже не нужно добавлять указатель к родительскому объекту в качестве свойства дочернего элемента и просто использовать его непосредственно в ваших вычисленных наблюдаемых.

Что-то вроде:

var Item = function(name, parent) {
   this.name = ko.observable(name);  
   this.isSelected = ko.computed(function() {
       return this === parent.selectedItem();        
   }, this);
};

var ViewModel = function() {
   this.selectedItem = ko.observable();
   this.items = ko.observableArray([
       new Item("one", this),
       new Item("two", this),
       new Item("three", this)
       ]);
};

Пример здесь: http://jsfiddle.net/rniemeyer/BuH7N/

Если все, о чем вы заботитесь, это выбранный статус, вы можете настроить его, чтобы передать ссылку на selectedItem, наблюдаемую на дочерний конструктор, например: http://jsfiddle.net/rniemeyer/R5MtC/

Если модель родительского представления хранится в глобальной переменной, вы можете не передавать ее дочернему элементу и использовать его прямо так: http://jsfiddle.net/rniemeyer/3drUL/. Я предпочитаю передавать ссылку на ребенка.

Ответ 2

По моему опыту, подход в ответе @RP Niemeyer хорош, если Item жить в течение всего срока действия приложения. Но если нет, это может привести к утечкам памяти, поскольку Item вычисленный наблюдаемый устанавливает обратную зависимость от ViewModel. Опять же, это нормально, если вы никогда не избавитесь от каких-либо объектов Item. Но если вы попытаетесь избавиться от Item, они не получат сбор мусора, потому что нокаут по-прежнему будет иметь ссылку на обратную зависимость.

Вы можете удостовериться в том, что утилиту dispose() вычислено, возможно, в методе cleanup() на Item, который вызывается, когда элемент уходит, но вы должны помнить об этом при удалении Item s.

Вместо этого, почему бы не сделать Item немного менее умным и сообщить ViewModel, когда он выбран? Просто сделайте Item isSelected() обычный старый наблюдаемый, а затем в ViewModel подпишитесь на selectedItem и обновите внутри этой подписки.

Или используйте @RP Niemeyer pub/sub solution. (Чтобы быть справедливым, это решение появилось после его ответа здесь.) Однако вам все равно придется очищать, потому что он также создает обратные зависимости. Но по крайней мере там меньше сцепления.

См. ответ на мой недавний вопрос в этой же теме для более подробной информации.

Ответ 3

Используйте $context вместо $parent, когда внутри привязки foreach.