注: 本文涉及代码均源自知乎, 通过 Chrome Devtool
观察代码而来.
今天在浏览知乎时, 突发奇想想看一下知乎的前端大大的代码, 但是代码都是webpack
的bundle
, 也经过了压缩混淆, 因此只能生涩的阅读一下, 从中发现一些设计实现的方式.
目前, 网站前端采用了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
的形式存在的, 下面我们来看图片 Component
的render
方法:
{
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
回调方法(对应图片 Component
内handleObserve
):
handleObserve = function(e) {
e[0].intersectionRatio <= 0 && !r.hasLoaded || (r.hasLoaded = !0,
r.loadThumbnail(),
r.loadImage())
}
如果图片未曾加载过, 则会进行一个两段式的加载, 实现一个逐步渲染效果:
loadThumbnail
: 下载小尺寸图片(60px), 并将其绘制会正常尺寸, 产生模糊效果loadImage
: 下载正常尺寸图片, 将state
中loaded
设值为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
})
}
}
}