Магический режим игнорирует обновления пользовательского объекта

См. эту ручку для демонстрации проблемы (на основе слайд-шоу из руководства). Нажав на стрелки «Далее» и «Предыдущий», вы заметите, что усы imgIndex обновляются правильно, но усы выражения, такие как <p>{{ curImageCaption() }}</p>, не распознают, когда их значения изменяются.

То есть объект видоизменен таким образом, что значение усов изменится, если выражения будут переоценены, но ractive, похоже, этого не понимает. Есть ли способ заставить это работать, кроме написания адаптеров? Я неправильно понимаю, как работает магический режим? Интересно то, что даже если я явно вызываю ractive.update() внутри обработчиков событий, ractive все равно не отвечает.

ОБНОВЛЕНИЕ С НОВОЙ ИНФОРМАЦИЕЙ

Немного повозившись, я придумал этот хак, который заставляет его работать. Хитрость состоит в том, чтобы изменить, например, <p>{{ curImageCaption() }}</p> на <p>{{ curImageCaption(imgIndex) }}</p> -- добавив к выражению усов простой примитив, который ractive понимает, как правильно смотреть.

Мне кажется, я понимаю, что происходит сейчас, но необходимость явного добавления аргументов в выражение усов, содержащее изменяющиеся примитивы, сводит на нет большую часть цели наличия отдельного объекта предметной области, то есть теперь вы кодируете свой объект предметной области, имея в виду ractive, использование изменяющихся примитивов — своего рода базовый механизм публикации/подписки для уведомления об изменениях.

Было бы неплохо создать настоящий механизм pub/sub для моих пользовательских объектов, на который ractive затем явно подписывается. Проблема в том, как я отметил в OP, даже когда ractive уведомляется об изменении через ractive.update(), он все еще не знает, что должен пересчитать усы, если только я не использую хак поддельного аргумента. Поэтому неясно, какой callback ractive должен регистрироваться, чтобы все работало.

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

ОБНОВЛЕНИЕ 2 - Достойное решение

Вот доказательство концепции не слишком хакерского обходного пути.

Идея состоит в том, чтобы использовать свойства ECMA5 для украшения вашего объекта пользовательского домена, предоставляя свойства, которые делегируют существующие методы, которые вы хотите использовать, но которые не работают внутри шаблонов ractive. Свойства, ооо, работают просто отлично.

Таким образом, вместо <p>{{ curImageCaption() }}</p> мы просто пишем <p>{{ imageCaption }}</p>, а затем украшаем наш объект пользовательского домена следующим образом:

Object.defineProperty(mySlideshow, "imageCaption", {
  configurable: true,
  get: function() { return this.curImageCaption() },
  set: function() { }
});

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

ПРИМЕЧАНИЕ. Одним из недостатков этого метода является то, что вам придется вызывать ractive.update() вручную в обработчиках событий. Я хотел бы знать, есть ли способ обойти это. И если нет, то насколько сильно это повлияет на производительность? Сводит ли это на нет всю цель хирургических обновлений Ractive?

Обновление 3. Лучшее достойное решение?

Эта ручка использует еще один подход, в котором наша модель пользовательского домена связывается с ractive через общий объект диспетчера. (объект, реализующий notify()). Я думаю, что это мой любимый подход до сих пор....

Это похоже на официальные ractive-адаптеры, но мы используем DI для передачи нашего неофициального ractive-адаптера. в наш объект домена, а не обертывать наш объект. На первый взгляд может показаться, что мы «кодируем на ractive», но на самом деле это верно лишь отчасти. Даже если бы мы использовали другой фреймворк, нам нужно было бы использовать какой-то механизм уведомления для передачи изменений в нашу модель представления, чтобы представления могли на это реагировать. Этот подход DI, кажется, требует меньше шаблонного кода, чем официальные адаптеры ractive, хотя я не понимаю их достаточно хорошо, чтобы знать это наверняка. Это не такое универсальное решение, как официальные адаптеры.

Код с пера для потомков

HTML

<div id='output'></div>

<script id='template' type='text/ractive'>
  <div class='slideshow'>
    <div class='main'>
      <a class='prev' on-tap='prev'><span>&laquo;</span></a>
      <div class='main-image' style='background-image: url({{ curImageSrc() }});'></div>
      <a class='next' on-tap='next'><span>&raquo;</span></a>
    </div>

    <div class='caption'>
      <p>{{ curImageCaption() }}</p>
      <p>Image index: {{ imgIndex }} </p>
    </div>
  </div>
</script>

JS

// Fix JS modular arithmetic to always return positive numbers
function mod(m, n) { return ((m%n)+n)%n; }

function SlideshowViewModel(imageData) {
  var self = this;
  self.imgIndex = 0;
  self.next = function() { self.setLegalIndex(self.imgIndex+1); }
  self.prev = function() { self.setLegalIndex(self.imgIndex-1); }
  self.curImage = function() { return imageData[self.imgIndex]; }
  self.curImageSrc = function() { return self.curImage().src; }
  self.curImageCaption = function() { return self.curImage().caption; }
  self.setLegalIndex = function(newIndex) { self.imgIndex = mod(newIndex, imageData.length); } 
}

var mySlideshow = new SlideshowViewModel(
  [
    { src: imgPath('problem.gif'), caption: 'Trying to work out a problem after the 5th hour' },
    { src: imgPath('css.gif'), caption: 'Trying to fix someone else\'s CSS' },
    { src: imgPath('ie.gif'), caption: 'Testing interface on Internet Explorer' },
    { src: imgPath('w3c.gif'), caption: 'Trying to code to W3C standards' },
    { src: imgPath('build.gif'), caption: 'Visiting the guy that wrote the build scripts' },
    { src: imgPath('test.gif'), caption: 'I don\'t need to test that. What can possibly go wrong?' }
  ]
);

var ractive = new Ractive({
  el: '#output',
  template: '#template',
  data: mySlideshow,
  magic: true
});

ractive.on( 'next', function(event) {
  ractive.data.next(); 
});
ractive.on( 'prev', function(event) {
  ractive.data.prev(); 
});


function imgPath(name) { return 'http://learn.ractivejs.org/files/gifs/' + name; }

person Jonah    schedule 15.12.2013    source источник
comment
Если у вас есть ответ на свой вопрос, вы все равно должны опубликовать его как ответ (или, возможно, несколько ответов, если они очень разные!) вместо того, чтобы редактировать его в вопросе. Это позволяет читателю понять, что ответ найден, и показать, где заканчивается проблема и начинается решение.   -  person IMSoP    schedule 20.12.2013
comment
Я планирую это сделать, но сначала хотел бы услышать от Рича Харриса отзывы о предложенных мной ответах. То есть я пока не считаю их ответами — пока только идеи.   -  person Jonah    schedule 20.12.2013
comment
Ответ не обязательно должен быть идеальным. На вопрос может быть несколько ответов одновременно, и лучший из них получит наибольшее количество голосов и будет принят. (Кроме того, кто такой Рич Харрис и почему последнее слово за ним?)   -  person IMSoP    schedule 20.12.2013
comment
Он автор ractive.js   -  person Jonah    schedule 20.12.2013
comment
Ах, достаточно справедливо. Тем не менее, это сайт сообщества, и для этого сайта ваши ответы вполне могут быть действительными ответами, даже если они не окажутся окончательными ответами, которые будущие читатели пометят как действительные для справки. Этот пост выглядит так, как будто вы ожидаете ветку форума с возвратом и возвратом (вы называете свой первый абзац OP) и, в конечном итоге, появление ответа, который на самом деле не соответствует формату этого сайта, ИМХО.   -  person IMSoP    schedule 20.12.2013
comment
Да, я согласен, что пост превратился в небольшой беспорядок. Как я уже сказал, я планирую почистить его, но жду еще пару дней, так как ожидаю, что Рич вмешается, и я смогу сделать более качественное и последовательное окончательное редактирование после его вклада.   -  person Jonah    schedule 20.12.2013
comment
Справедливо, я перестану совать свой нос и оставлю это тебе. Удачи в поиске элегантного решения :)   -  person IMSoP    schedule 20.12.2013


Ответы (2)


Я попытаюсь объяснить, что происходит под капотом, прежде чем представить возможное решение:

Обертывание объектов в магическом режиме

В магическом режиме, когда Ractive встречает развернутый дескриптор данных объекта, он оборачивает его, заменяя его дескриптором доступаget()/set() функции . (Дополнительная информация о MDN, для тем, кто заинтересован.) Таким образом, когда вы выполняете self.imgIndex = 1, вы фактически запускаете функцию set(), которая знает, как уведомить всех зависимых свойства imgIndex.

Ключевое слово здесь — «встречи». Единственный способ, которым Ractive знает, что ему нужно обернуть imgIndex, это если мы сделаем ractive.get('imgIndex'). Это происходит внутри, потому что у нас {{imgIndex}} усов.

Вот почему свойство индекса обновляется.

Отслеживание зависимостей с вычисляемыми значениями

В обычном шаблоне вы можете иметь то, что в основном составляет вычисляемые значения, используя метод get():

<p>{{ curImageCaption() }}</p>
ractive = new Ractive({
  el: 'body',
  template: template,
  data: {
    images: images,
    imgIndex: 0,
    curImageCaption: function () {
      var imgIndex = this.get( 'imgIndex' );
      return this.get( 'images' )[ imgIndex ].caption;
    }
  }
});

Здесь, поскольку мы вызываем ractive.get() внутри функции curImageCaption, Ractive знает, что ему нужно перезапускать функцию каждый раз, когда изменяется images или imgIndex.

На самом деле вы задаете резонный вопрос: почему получение значения self.imgIndex в магическом режиме не работает так же, как ractive.get('imgIndex')?

Ответ состоит из двух частей: во-первых, я не подумал добавить эту функцию, а во-вторых, оказалось, что она не работает! Точнее, очень хрупкий. Я изменил магический режим, чтобы метод доступа get() захватывал зависимость так же, как это делает ractive.get(), но self.imgIndex является только дескриптором доступа (в отличие от дескриптора данных), если Ractive уже столкнулся с ним. Так что это работало, когда у нас было <p>Image index: {{ imgIndex }} </p> вверху шаблона, но не когда оно внизу!

Обычно рецепт довольно прост: используйте ractive.get(), чтобы сделать зависимость от self.imgIndex явной внутри curImageSrc() и curImageCaption(). Но поскольку вы используете пользовательский объект модели представления, это не идеально, поскольку фактически означает жесткое программирование ключевых путей.

Решение - создание собственного адаптера

Вот что я бы порекомендовал - сделать адаптер, который работает с пользовательским объектом модели представления:

Ractive.adaptors.slides = {
  filter: function ( object ) {
    return object instanceof SlideshowViewModel;
  },
  wrap: function ( ractive, slides, keypath, prefix ) {
    var originalNext, originalPrev;

    // intercept next() and prev()
    originalNext = slides.next;
    slides.next = function () {
      originalNext.call( slides );
      ractive.update( keypath );
    };

    originalPrev = slides.prev;
    slides.prev = function () {
      originalPrev.call( slides );
      ractive.update( keypath );
    };

    return {
      get: function () {
        return {
          current: slides.curImage(),
          index: slides.imgIndex
        };
      },
      teardown: function () {
        slides.next = originalNext;
        slides.prev = originalPrev;
      }
    };
  }
};

var ractive = new Ractive({
  el: '#output',
  template: '#template',
  data: mySlideshow,
  adaptors: [ 'slides' ]
});

Это очень простой адаптер, и его, вероятно, можно было бы улучшить, но суть вы уловили — мы перехватываем вызовы к next() и prev() и сообщаем Ractive (через ractive.update()), что ему нужно выполнить грязную проверку. Обратите внимание, что мы представляем фасад (через метод get() оболочки), поэтому шаблон выглядит немного иначе — посмотри на эту ручку.

Надеюсь это поможет.

person Rich Harris    schedule 20.12.2013
comment
спасибо богат! последующие вопросы: 1. можете ли вы предложить какие-либо мысли / критические анализы моего решения обновления 3? он кажется немного более лаконичным, чем официальный адаптер ractive, поэтому мне любопытны его недостатки. 2. в вашей версии curImageCaption, которая использует this.get( 'imgIndex' ); вместо this.imgIndex, как ractive реализует свою магию метапрограммирования? как он узнает, что this.get( 'imgIndex') вызывается внутри функции усов {curImageCaption()}? он просто запускает его во время компиляции шаблона и проверяет, был ли как-то вызван get? - person Jonah; 20.12.2013
comment
Еще одно замечание, которое я хотел сделать: с решением, которое я предложил в обновлении 3, нам даже не нужно использовать магический режим. Если вы посмотрите, то увидите, что я даже заменил ViewModel на версию, в которой используется раскрывающийся шаблон модуля. Однако я считаю, что официальный метод адаптера также имеет это преимущество. - person Jonah; 20.12.2013
comment
Я думаю, что ваше перо обновления 3 прекрасно подходит — это в основном оптимизированная система пабов и подписок, которая является проверенным в бою шаблоном. Поскольку вы «владеете» объектом модели представления, вы можете добавлять эти перехватчики событий, так почему бы и нет — обычно я придерживаюсь мнения, что «если это работает и кажется правильным, значит, это правильно». - person Rich Harris; 21.12.2013
comment
this.get() работает, потому что функции, которые составляют часть таких выражений, как {{ curImageCaption() }}, оборачиваются — перед вызовом функция ractive.get() перезаписывается, так что любые вызовы к ней регистрируются. После завершения выполнения функции мы видим, какие вызовы были зарегистрированы, и проверяем их на соответствие существующим «мягким зависимостям» (т. е. зависимостям, которые не указаны явно в самом выражении), при необходимости добавляя наблюдателей к этим зависимостям. Это очень похоже на то, как Knockout обрабатывает отслеживание зависимостей внутри функций. - person Rich Harris; 21.12.2013

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

ОТРЕДАКТИРОВАНО: использовать текущее изображение в качестве контекстного блока вместо циклического перебора коллекции.

  <div class='slideshow'>
    {{#curImage}}
    <div class='main'>
      <a class='prev' on-tap='prev'><span>&laquo;</span></a>
      <div class='main-image' style='background-image: url({{ src }});'></div>
      <a class='next' on-tap='next'><span>&raquo;</span></a>
    </div>

    <div class='caption'>
      <p>{{ caption }}</p>
      <p>Image index: {{ imgIndex }} </p>
    </div>
  </div>

...

    function SlideshowViewModel(imageData) {
      ...
      self.curImage = imageData[self.imgIndex]
      ...
      self.setLegalIndex = function(newIndex) { 
        self.imgIndex = mod(newIndex,imageData.length); 
        self.curImage = imageData[self.imgIndex]
        } 
    }

Это использование вашей оригинальной ручки только с ключевыми модификациями. Вот новая ручка.

Я бы по-прежнему перемещал кнопки во внешнюю часть шаблона, чтобы отображение в середине можно сделать частичным:

<div class='main'>
  <a class='prev' on-tap='prev'><span>&laquo;</span></a>
  {{#current}}
    {{>partial}}
  {{/}}
  {{/current}}
  <a class='next' on-tap='next'><span>&raquo;</span></a>
</div>

и инкапсулировать в Ractive.extend, но если ViewModel работает на вас...

person martypdx    schedule 20.12.2013
comment
Привет, Марти, я ценю твой ответ, но мне совсем не нравится это решение. по сути, это побеждает то, что я считаю одним из основных преимуществ использования пользовательской модели представления: вы можете изолировать всю свою логику отображения в POJO, позволяя вашим шаблонам представления быть действительно пассивными (в основном, самообновляющиеся шаблоны слияния почты). в вашем примере логика отображения пробирается с помощью on-tap='move:-1', а также пробирается в обработчик событий ractive с помощью ractive.data.goto(ractive.data.current + i);. изоляция полностью потеряна. - person Jonah; 20.12.2013
comment
Я только что заметил, что шаблон также перебирает каждое изображение в модели с представлением и сравнивает его current, просто чтобы отобразить правильное изображение. младенец Иисус плачет :) - person Jonah; 20.12.2013
comment
Спасибо за ответ. Я отредактировал его, максимально приблизив к оригиналу. Ключевым моментом является использование curImage в качестве контекста. - person martypdx; 20.12.2013
comment
Я пытался избавиться от дублирования в предыдущих и следующих обработчиках событий. Я согласен, что «move:-1» слишком много, но я бы не стал создавать отдельный объект ViewModel, я бы просто использовал Ractive.extend. Я думаю, что Ractive мог бы искать методы в экземпляре Ractive и вызывать их, если они существуют, а не события (или в дополнение к ним). - person martypdx; 20.12.2013
comment
Использование Ractive.extend — это хорошо, но оно связывает вашу доменную модель с ractive framework, тогда как на самом деле ваша доменная модель не имеет ничего общего с ractive. Используя POJO ViewModel, вы можете легко переключиться на другой фреймворк. Это также упрощает модульное тестирование, поскольку вы просто тестируете POJO без каких-либо зависимостей. Так что есть веские причины делать то, что на первый взгляд кажется чересчур сложным. Что касается удаления дублирования в prev/next, просто создайте метод модели частного представления и позвольте prev/next делегировать ему, передав 1/-1. Хотя мне казалось, что этого делать не стоит. - person Jonah; 20.12.2013
comment
Хорошая мысль о подключении к фреймворку Ractive. Поигравшись с ним еще немного, также выяснилось, что объект Ractive.extend не работает в магическом режиме. Решает ли вашу проблему наличие контекстного блока с текущим изображением? Или это гипотетически, чтобы доказать случай, когда вам нужно вызывать функции в вашей ViewModel? - person martypdx; 20.12.2013
comment
Проблема в том, что теперь вы кодируете свою модель представления в соответствии с ractive. Например, если вы хотите использовать шаблон раскрывающегося модуля, а не читать свойства напрямую, вы не можете этого сделать. Кроме того, если ваша модель представления уже существует, вам придется изменить ее. Если вы пишете с нуля и не возражаете против того, чтобы ractive немного повлияло на ваш стиль программирования, это совершенно нормально. Вы по-прежнему будете пользоваться преимуществами отделения от фреймворка и более чистой тестируемости. Но решение не такое гибкое, как использование адаптеров, официальных или неофициальных, которые я предложил. - person Jonah; 20.12.2013