背景
思路
- 最容易想到的就是做一个底部检测,当用户滚动到底部时再请求下一部分内容,并将结果拼接到上一次结果之后。
- 但很多情况下返回内容不只有字符,还有媒体文件,这个时候浏览器的下载通道很容易被占满。这个时候 我们实际需要的其实是用户看得到的内容去做加载就可以,用户看不到的可以先用墓碑(占位符)。这么做可以大大减少流量浪费~
- 理清思路后大致实现不难,不过滚动是个高频发事件,这在一些极端 case 下容易引起卡顿,需要进行优化,这也是本文重点。
优化滚动
- 常用两种优化方式
Throttle
: 允许我们限制激活响应的数量。通过限制每秒回调的数量的方式来达到优化目的;
// const fn = throttle(e => console.log(e, "123"), 300)
// const scroll = listen(document.body, "mousewheel", fn)
function throttle(...props) {
const [fn, threshhold = 250, scope] = props
let last
let deferTimer
return event => {
const context = scope || this
const now = +new Date()
if (last && now < last + threshhold) {
clearTimeout(deferTimer)
deferTimer = setTimeout(() => {
// 停止滚动延迟后触发
last = now
fn.apply(context, [event, ...props])
}, threshhold)
} else {
last = now
fn.apply(context, [event, ...props])
}
}
}
Debounce
: 事件发生时,不会立即激活回调。而是等待一定的时间并检查相同的事件是否再次触发。如果是,我们重置定时器,并再次等待。如果在等待期间没有发生相同的事件,我们就立即激活回调。 => 使用于滚动结束后执行操作
// 基本防抖
function debounce(...props) {
const [action, wait = 200] = props
let last
return function(event) {
const ctx = this
clearTimeout(last)
last = setTimeout(function() {
action.apply(ctx, [event, ...props])
}, wait)
}
}
// lodash 实现
import { isObject, isFn } from "./is"
/**
* 截流函数
*
* @param {function} fn
* @param {number} [wait=250]
* @param {object} options
* @returns
*/
function throttle(func, wait = 250, options) {
let leading = true
let trailing = true
if (isObject(options)) {
leading = "leading" in options ? !!options.leading : leading
trailing = "trailing" in options ? !!options.trailing : trailing
}
return debounce(func, wait, {
leading,
trailing,
maxWait: wait
})
}
/**
* 防抖函数
*
* @param {function} func
* @param {*} wait
* @param {object} options
* - leading:Boolean 开始时调用
* - trailing:Boolean 结束时调用
* - maxWait:Number 最大等待时间
* @returns
*/
function debounce(func, wait, options) {
let lastArgs, lastThis, maxWait, result, timerId, lastCallTime
let lastInvokeTime = 0
let leading = false
let maxing = false
let trailing = true
const isBrowser = typeof window == "object" && window !== null
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF =
!wait &&
wait !== 0 &&
isBrowser &&
typeof window.requestAnimationFrame === "function"
if (!isFn(func)) {
throw new TypeError("Expected a function")
}
wait = +wait || 0
if (isObject(options)) {
leading = !!options.leading
maxing = "maxWait" in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = "trailing" in options ? !!options.trailing : trailing
}
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
function startTimer(pendingFunc, wait) {
if (useRAF) {
window.cancelAnimationFrame(timerId)
return window.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
function cancelTimer(id) {
if (useRAF) {
return window.cancelAnimationFrame(id)
}
clearTimeout(id)
}
function leadingEdge(time) {
lastInvokeTime = time
timerId = startTimer(timerExpired, wait)
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
// 第一次调用 || 上一次已经停止 || 系统时间倒退 || 已经达到max限制
return (
lastCallTime === undefined ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxing && timeSinceLastInvoke >= maxWait)
)
}
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
// 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器
return trailingEdge(time)
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
function pending() {
return timerId !== undefined
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
export { throttle, debounce }
判断到达底部
- scrollHeight <=> scrollTop + offsetHeight
对于预加载可以适当加个预加载高度 这样提前加载会更加流畅
dom 回收
由于每一个节点都会增加一些额外的内存、布局、样式和绘制。进行 DOM 回收可以使 dom 保持在一个比较低的数量上,进而加快上面提到的这些处理过程。在 dom 移除页面一定高度后对节点进行 move()
操作。为了保证滚动条的正确比例和防止高度塌陷,需要显示的声明高度。