魔术模式忽略对自定义对象的更新

时间:2013-12-15 23:33:52

标签: ractivejs

请参阅this pen了解该问题的演示(基于教程中的幻灯片演示)。单击“下一个”和“上一个”箭头时,您会注意到imgIndex小胡子正确更新,但<p>{{ curImageCaption() }}</p>等表达式小写字母无法识别它们的值何时发生变化。

也就是说,对象是变异的,如果表达式被重新评估,胡须值会改变,但ractive似乎没有意识到这一点。除非编写适配器,否则有什么方法可以让它工作吗?我误解魔法模式是如何工作的吗?有趣的是,即使我在事件处理程序中明确调用ractive.update(),ractive仍然没有响应。

更新新信息

经过更多的摆弄,我想出了this hack让它发挥作用。黑客攻击是改变,例如<p>{{ curImageCaption() }}</p><p>{{ curImageCaption(imgIndex) }}</p> - 在胡子表达中添加一个简单的原语,rative理解如何正确观察。

我想我现在看到了正在发生的事情,但是必须明确地向包含更改原语的mustache表达式添加参数会破坏拥有单独域对象的大部分目的 - 也就是说,现在您正在使用ractive对域对象进行编码记住,使用更改原语是一种基本的发布/订阅机制,用于通知ractive的变化。

必须在我的自定义对象上创建一个真正的发布/订阅机制,然后明确订阅,这样就可以了。问题是,正如我在OP中所指出的那样,即使通过ractive.update()通知ractive有关更改,它仍然不知道它应该重新计算胡须,除非我使用假参数hack。因此,目前尚不清楚应该注册什么回调以使一切正常。

我不太了解ractive的内部工作,但我怀疑需要的是能够直接使用_deps内容,并手动触发表达式的重新计算。如果这听起来是对的,那么如何实现它的一个例子将不胜感激。

更新2 - 一个不错的解决方案

这是proof of concept for a not-too-hacky workaround

我们的想法是使用ECMA5属性来装饰您的自定义域对象,提供委托给您想要使用但在ractive模板中不起作用的现有方法的属性。属性,otoh,工作得很好。

因此,我们只需编写<p>{{ curImageCaption() }}</p>而不是<p>{{ imageCaption }}</p>,然后我们就像这样装饰我们的自定义域对象:

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

这个装饰,在我的演示中有点冗长,可以通过创建一个辅助方法轻松缩小,该方法接受一个对象将您的新的友好属性名称映射到对象上现有方法的名称,并处理上述样板给你。

注意:此方法的一个缺点是您必须在事件处理程序中手动调用ractive.update()。我想知道是否有办法解决这个问题。如果没有,这会导致多大的性能影响呢?它是否会破坏ractive手术更新的全部目的?

更新3 - 更好的解决方案?

This pen采用另一种方法,通过通用调度程序对象(实现notify()的对象)将我们的自定义域模型与ractive链接起来。我认为这是迄今为止我最喜欢的方法....

它类似于官方ractive adaptors,但我们使用DI将我们的非官方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; }

2 个答案:

答案 0 :(得分:3)

在提出可能的解决方案之前,我会尝试解释一下发生了什么:

以魔术模式包装对象

在魔术模式下,当Ractive遇到对象的解包数据描述符时,通过将访问者描述符替换为来包装 - get()/set()函数。 (More info on MDN,对于那些感兴趣的人。)所以当你self.imgIndex = 1时,你实际上是在触发set()函数,它知道如何通知所有依赖者 imgIndex属性。

这里的关键词是'遇到'。如果我们imgIndex,Ractive知道它需要包裹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知道每次imagesimgIndex更改时都需要重新运行该函数。< / p>

您实际提出的问题是一个合理的问题:为什么在魔术模式中检索self.imgIndex的值与执行ractive.get('imgIndex')的工作方式相同?

答案分为两部分:首先,我没想过要添加这个功能,其次,事实证明它不起作用!或者说,它非常脆弱。我改变了魔术模式,以便get()访问器以ractive.get()的方式捕获依赖关系 - 但self.imgIndex只是一个访问者描述符(而不是数据描述符)如果Ractive已经遇到过它。所以当我们在模板的顶部有<p>Image index: {{ imgIndex }} </p>时它才有效,但是当它在底部时却没有!

通常情况下处方相当简单:使用ractive.get()self.imgIndexcurImageSrc()内明确依赖curImageCaption()。但是因为你使用的是自定义的viewmodel对象,所以这并不理想,因为它实际上意味着硬编码关键路径。

解决方案 - 创建自定义适配器

这是我推荐的 - 制作适用于自定义viewmodel对象的适配器:

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知道(通过{{1它需要做一些脏检查。请注意,我们正在展示一个外观(通过包装器的ractive.update()方法),因此模板看起来略有不同 - see this pen

希望这有帮助。

答案 1 :(得分:0)

也许这是一个学术练习,我是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]
        } 
    }

这只是使用原始笔,只需修改键。这是新的pen

我仍然会将按钮移动到模板的外部,以便中间的显示可以成为部分:

<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适合你......