使用Knockout和jQuery实现图片的懒加载

9
我有一个图片密集型网站,用knockout构建,包括jQuery。
这些都在foreach循环中:
<!-- ko foreach: {data: categories, as: 'categories'} -->
<!-- Category code -->
<!-- ko foreach: {data: visibleStations, as: 'stations'} -->
<!-- specific code -->
<img class="thumb lazy" data-bind="attr: { src: imageTmp,
                                           'data-src': imageThumb,
                                           alt: name,
                                           'data-original-title': name },
                                   css: {'now-playing-art': isPlaying}">
<!-- /ko -->
<!-- /ko -->

基本上,当我创建这些元素时,imageTmp是一个计算的可观察对象,返回临时url,而imageThumb则设置为来自CDN的真实url。

我还有一段代码,称之为Lazy Sweeper:

var lazyInterval = setInterval(function () {
    $('.lazy:in-viewport').each(function () {
        $(this).attr('src', $(this).data('src')).bind('load', function(){
            $(this).removeClass('lazy')
        });
    });
}, 1000);

那段代码会查找视窗中的图像(使用自定义选择器仅查找屏幕上的图像),然后将src设置为data-src。我们想要的行为是避免加载用户看不到的大量(实际上只有几百张)图片所带来的开销。但是我们看到的行为是,在第一次加载后,似乎在调用ko.applyBindings()之后,懒加载程序被破坏了,我们看到图片恢复为默认图片。然后再次运行懒加载程序,我们才看到它们展示出来。我们不清楚如何更好地以knockout方式实现这一点。你有什么想法吗?见解?建议?
我在Twitter上得到了关于另一个lazyloading库的答案。那并没有解决问题-问题是不理解DOM和ko表示形式需要如何交互来设置lazyloading。 我相信我需要更好地考虑创建一个knockout模型,该模型设置< code>imageTmp ,并根据是否在视口中响应lazyloading,然后在加载完imageThumb (真正的图像)后更新该模型。
3个回答

11

更新:现在有一个可工作的示例

我的方法是:

  • 让你的模型(车站)决定图像URL是什么 - 无论是临时的还是真实的,就像你已经做的一样
  • 有一个绑定,其工作是处理DOM - 设置该图像源并处理load事件
  • 将懒惰清扫器限制为仅提供信号“你现在可见”

视图模型

  1. add a showPlaceholder flag which contains our state:

    this.showPlaceholder = ko.observable(true);
    
  2. add a computed observable that always returns the currently correct image url, depending on that state:

    this.imageUrl = ko.computed(function() {
      return this.showPlaceholder() ? this.imageTemp() : this.imageThumb();
    }, this);
    
现在我们需要做的就是在应该加载图像时将showPlaceholder设置为false。稍后会详细说明。
绑定的作用是在计算的imageUrl更改时设置<img src>。如果src是真实的图像,则应在加载后删除lazy类。
  ko.bindingHandlers.lazyImage = {
    update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
      var $element     = $(element),
          // we unwrap our imageUrl to get a subscription to it,
          // so we're called when it changes.
          // Use ko.utils.unwrapObservable for older versions of Knockout
          imageSource  = ko.unwrap(valueAccessor());

      $element.attr('src', imageSource);

      // we don't want to remove the lazy class after the temp image
      // has loaded. Set temp-image-name.png to something that identifies
      // your real placeholder image
      if (imageSource.indexOf('temp-image-name.png') === -1) {
        $element.one('load', function() {
          $(this).removeClass('lazy');
        });
      }
    }
  };

懒惰的扫把

这只需要向我们的视图模型提示,现在应该从占位符切换到真实图像。

ko.dataFor(element)ko.contextFor(element) helper functions 让我们可以从外部访问绑定在 DOM 元素上的任何内容:

var lazyInterval = setInterval(function () {
    $('.lazy:in-viewport').each(function () {
      if (ko.dataFor(this)) {
        ko.dataFor(this).showPlaceholder(false);
      }
    });
}, 1000);

尽管此答案的基本前提来自我的下面的答案(本可以发表评论提供如何改进答案),但为了帮助未来用户记住这一点-为每个图像(OP说有1000个)创建两个新的可观察对象,每次滚动都会调用3倍的订阅通知,而不是使用单个绑定。 - PW Kad
1
@PWKad 我非常尊重地不同意。我们的方法除了创建自定义绑定之外,几乎没有共同点。我专门创建了我的方法来分离视图模型、绑定和外部库之间的职责。至于性能方面,可观察对象仅在每个模型更改一次,而不是同时更改数千个,只有那些自上次滚动事件以来变得可见的对象才会更改。 - janfoeh
这种方法在开发中的重图像网站上表现很好。非常感谢!赏金花得其所! - artlung
1
@artlung 感谢您的反馈!很高兴听到它对您有帮助。 - janfoeh

3
我不熟悉Knockout.js,因此无法引导您走更多“Knockout”的方式。但是,我有一个小技巧可以使检查每个图像的成本降低:

首先:您可以针对代码进行优化。

var lazyInterval = setInterval(function () {
    $('.lazy:in-viewport').each(function () {
        $(this)
            .attr('src', $(this).data('src'))
            .removeClass('lazy'); 
        // you want to remove it from the loadable images once you start loading it
        // so it wont be checked again.
        // if it won't be loaded the first time, 
        // it never will since the SRC won't change anymore.
    });
}, 1000);

另外,如果您在视口中检查图像但视口没有更改,则只是一遍又一遍地重新检查它们,没有任何好的理由... 您可以添加“脏标志”来检查视口是否实际更改。

注:viewport指的是浏览器窗口中可见的区域。
var reLazyLoad = true;
var lazyInterval = setInterval(function () {
    if (! reLazyLoad) return;
    ...current code...
    reLazyLoad = false;
}, 1000);
$(document).bind('scroll',function(){ reLazyLoad = true; });

当然,您希望在修改DOM时每次都重新检查它。
这并不能解决您的数据绑定问题,但它有助于提高性能 :-)
(您还可以将lazySweeper作为节流函数,并在每次更改(视口或DOM)时调用它。这样代码看起来更美观...)
最后:您不能使用数据绑定添加lazy-class吗?这样,它只会在绑定完成后被lazySweeper拾取...(我在输入时想到了这个方法。我真的不知道knockout js如何使用数据绑定,所以这是一个冒险)

2

使用自定义绑定处理程序

我还没有创建一个fiddle来测试这个,但如果你觉得这是正确的方向,但我的伪代码有问题,只要让我知道,我就可以在fiddle中验证。

这是我个人会做的 -

使用自定义绑定处理程序检查元素上是否有'class'为'lazy'。每当同一元素上的绑定具有“viewPortChanged”名称(allBindingsAccessor.get()应该找到该绑定并将本地变量设置为它)时,它应该触发 -

ko.bindingHandlers.showPicture = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        // The actual link to your image or w/e
        var actualSource = valueAccessor();
        var viewPortChanged = allBindingsAccessor.get('viewPortChanged');
        viewPortChanged.subscribe(function () {
            if ($(element).hasClass('lazy')) {
                $(element).attr("src", actualSource());
            }
        });
    }
};

在您的视图模型中,创建一个标志来触发自定义绑定处理程序 -

function viewModel() {
    var self = this;
    self.viewPortChanged = ko.observable(false);

    // Register this to fire on resize of window
    $(window).resize(function() {
        // Do your view change class logic
        $('.lazy:in-viewport').each(function () {
            $(this).attr('src', $(this).data('src')).bind('load', function(){
                $(this).removeClass('lazy')
            });
        });
        // Have the observable flag change to recalc
        // the custom binding handler
        self.viewPortChanged(!self.viewPortChanged());
    });
}
ko.applyBindings(new viewModel());

最后,在您的元素上注册自定义绑定处理程序 -
<img class="thumb lazy" data-bind="showPicture: thisImageSource, viewPortChanged: viewPortChanged">

基本上,每当 viewPortChanged 可观察对象触发时,应该对每张图片进行懒惰检查。一个问题是,viewPortChanged 显然只是在相反地设置它自己,所以你可能想把它变成一个计算属性执行其他操作,但要小心不要重复标记所有的可观察对象。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接