19 Sep 2017

知乎问题-回答视窗图片延迟加载

注: 本文涉及代码均源自知乎, 通过 Chrome Devtool 观察代码而来.

今天在浏览知乎时, 突发奇想想看一下知乎的前端大大的代码, 但是代码都是webpackbundle, 也经过了压缩混淆, 因此只能生涩的阅读一下, 从中发现一些设计实现的方式.

目前, 网站前端采用了React的技术栈, 本文主要介绍一下在知乎问题回答页面图片延迟加载的一些原理, 如有疏漏, 希望大家不吝指出.

当我们进入到一个回答页面时, 不可见部分的图片是不会进行下载与显示的, 在页面上, 则是一个空的div:

<span>
  <div data-reactroot="" class="VagueImage origin_image zh-lightbox-thumb" data-src="https://pic1.zhimg.com/v2-fa2c910478a2d00438916ab5bebdfad4_b.png" style="width: 654px; height: 334.301px;">
  </div>
</span>

想必大家也已经注意到了data-reactroot, 说明这个节点是react组件的根节点, 所以延迟加载的图片是以Component的形式存在的, 下面我们来看图片 Componentrender方法:

{
  key: "render",
  value: function() {
    var e = this
      , t = (0,
    _.omit)(this.props, "thumbnail")
      , n = t.className
      , r = t.url
      , a = t.alt
      , i = t.threshold
      , l = o(t, ["className", "url", "alt", "threshold"])
      , u = this.state
      , s = u.loaded
      , c = u.vagued
      , d = u.computeWidth
      , p = u.computeHeight;
    return c ? v.default.createElement(P.default, {
        rootMargin: i + "px",
        onObserve: this.handleObserve
    }, v.default.createElement("div", {
        className: (0,
        b.default)("VagueImage", n),
        ref: function(t) {
            e.wrapperNode = t
        },
        style: f({}, d && {
            width: d + "px"
        }, p && {
            height: p + "px"
        }),
        "data-src": r
    }, s && v.default.createElement("img", {
        className: "VagueImage-innerLarge",
        src: r,
        alt: a
    }))) : v.default.createElement("img", f({
        className: n,
        width: d,
        src: r,
        alt: a
    }, l))
  }
}

render会根据是否显示模糊图(vagued), 图片是否已加载(loaded)作为条件来进行不同渲染, 两者都是state中的数据, vagued初始化默认为true, loaded默认为false:

//控件构造函数
r.state = {
  vagued: !0,
  loaded: !1,
  computeWidth: r.props.width,
  computeHeight: r.props.height
}

由此也验证了之前所见到的div的由来, 值得注意的是, render 方法并非直接创建了一个div, 而是通过创建了一个观察者 Component(P.default混淆代码)嵌套了一下, 下面我们来看一下这个观察者 Component的代码:

 function t() {
    var e, n, a, i;
    r(this, t);
    for (var l = arguments.length, u = Array(l), s = 0; s < l; s++)
      u[s] = arguments[s];
    return n = a = o(this, (e = t.__proto__ || Object.getPrototypeOf(t)).call.apply(e, [this].concat(u))),
    a.handleObserve = function(e) {
      var t = a.props.onObserve;
      t && t(e)
    }
    ,
    i = n,
    o(a, i)
}
return a(t, e),
u(t, [{
  key: "componentDidMount",
  value: function() {
      this.target = (0,
      d.findDOMNode)(this),
      this.observer = this.createObserver(),
      this.observer.observe(this.target)
  }
}, {
  key: "componentWillUnmount",
  value: function() {
      this.observer.unobserve(this.target)
  }
}, {
  key: "createObserver",
  value: function() {
    var e = this.props
      , t = e.root
      , n = e.rootMargin
      , r = e.threshold
      , o = {
        root: t,
        rootMargin: n,
        threshold: r
    };
    return new window.IntersectionObserver(this.handleObserve,o)
  }
}, {
  key: "render",
  value: function() {
      return this.props.children
  }
}]),
t

通过阅读可以发现, 该观察者 Component起到了一个观察器作用, 通过令观察者 Component的dom节点订阅window.IntersectionObserver, 进而回调图片 Component传入的onObserve方法, 来实现图片的视窗滚动加载.

关于window.IntersectionObserver的知识, 可以参考: Intersection Observer API, 该API仅部分浏览器版本支持, 对于不支持浏览器, 可通过引入polyfill支持, 参考: IntersectionObserver polyfill

观察者 Component初始化完毕后, 当视窗滚动至图片 Component时, 会触发onObserve回调方法(对应图片 ComponenthandleObserve):

handleObserve = function(e) {
  e[0].intersectionRatio <= 0 && !r.hasLoaded || (r.hasLoaded = !0,
  r.loadThumbnail(),
  r.loadImage())
}

如果图片未曾加载过, 则会进行一个两段式的加载, 实现一个逐步渲染效果:

  1. loadThumbnail: 下载小尺寸图片(60px), 并将其绘制会正常尺寸, 产生模糊效果
  2. loadImage: 下载正常尺寸图片, 将stateloaded设值为true, vagued为false, 重新render绘制出正常图片

function t() {
  r.handleObserve = function(e) {
    e[0].intersectionRatio <= 0 && !r.hasLoaded || (r.hasLoaded = !0,
    r.loadThumbnail(),
    r.loadImage())
  },
  {
    key: "loadThumbnail",
    value: function() {
      this.thumbnail = u(this.props.thumbnail, this.handleThumbnailLoaded, {
        crossOrigin: "anonymous"
      })
    }
  },
  r.loadImage = function() {
    r.image = u(r.props.url, r.handleImageLoaded)
  },
  r.handleThumbnailLoaded = function() {
    var e = r.state
      , t = e.computeWidth;
    if (!e.computeHeight) {
      var n = r.thumbnail
        , o = n.naturalWidth
        , a = n.naturalHeight
        , i = t / o
        , l = a * i;
      r.setState({
        computeHeight: l
      })
    }
    r.draw()
  },
  r.handleImageLoaded = function() {
    r.canvasEl && r.canvasEl.classList.add("is-active"),
    r.setState({
      loaded: !0
    }),
    r.timeout = setTimeout(function() {
      return r.setState({
        vagued: !1
      })
    }, S)
  },
  r.draw = function() {
    var e = (0,
    T.default)(navigator.userAgent);
    if (!e.IE && !e.Edge)
      try {
        var t = r.wrapperNode
          , n = t.offsetWidth
          , o = t.offsetHeight
          , a = document.createElement("canvas");
        a.classList.add("VagueImage-canvas"),
        a.width = n,
        a.height = o;
        var i = a.getContext("2d");
        i.drawImage(r.thumbnail, 0, 0, n, o);
        var l = i.getImageData(0, 0, n, o);
        A.postMessage({
          url: r.props.url,
          width: n,
          height: o,
          imageData: l,
          radius: 42
        }),
        A.addEventListener("message", r.handleAddCanvas),
        r.canvasEl = a
      } catch (e) {
        r.setState({
          vagued: !1
        })
      }
  }
}


Tags:
0 comments