Вчера утром я получил интервью по телефону, болтал и говорил об оптимизации производительности, а затем интервьюер спросил о длинном списке. На самом деле то, что я делал раньше, это просто простая пейджинговая обработка, но это точно не то, о чем спрашивал интервьюер.Он озабочен виртуальными списками.Я примерно видел исходный код этого эффекта раньше.Хотя сам я его не реализовывал , у меня есть кое-что свое. Я подумал, так что блаблабла..., может из-за моей ограниченной способности выражаться, и я не знаю, понимает ли интервьюер, что я имею в виду, поэтому я просто реализую это и записываю
Когда я испытал этот эффект, я специально открыл панель производительности, чтобы проанализировать его, и остался не очень доволен. Найдите в интернете реализацию, восстановите сцену на тот момент и посмотрите на картинку:
Первая половина постоянно прокручивает колесико, а вторая половина быстро перетаскивает полосу прокрутки.. Для такого рода функций, связанных с прокруткой, я так одержим этим ... Производительность FPS очевидна, и есть красный alarm Теперь, глядя на этот график ЦП, у вас есть желание его сгладить? ? ?
Оптимизация длинного списка сама по себе является поведением оптимизации (ерунда), но сама оптимизация не может быть проигнорирована при оптимизации функции.После некоторой возни прошлой ночью я, наконец, добился следующих результатов:
Точно так же первая половина прокручивается колесиком, а вторая половина быстро перетаскивается полосой прокрутки. Тем не менее, после внедрения все еще остается дефект, и он будет оптимизирован в будущем, когда возникнет такой спрос.
- Поддерживает только списки фиксированной высоты (и, чтобы быть последовательным)
- При очень быстрой прокрутке будет мерцание, особенно на мобильных устройствах.Хорошего решения пока не придумал.Это связано с механизмом событий прокрутки.
Исходный код основан на реализации vue, вот единый словарь
-
Itemпредставляет каждого потомка длинного списка
идеи
В первую очередь необходимо четко написать назначение этой функции, или конечный эффект.
- Улучшить производительность страниц с длинным списком
- С точки зрения опыта, пользователи не могут понять, что вы использовали длинный список
- Сделать эту функцию составной (пока не рассматривается)
Исходя из вышеперечисленных 2-х пунктов, нам есть чем заняться
- Основное направление повышения производительности — уменьшение количества узлов рендеринга на страницах длинного списка.До оптимизации используется полный рендеринг.После оптимизации лучше всего рендерить только те узлы, которые видны пользователям, или чем меньше, тем лучше.
- Оптимизированная страница имеет ту же полосу прокрутки, что и обычная страница с длинным списком.
- Оптимизированная прокрутка должна быть очень близка к естественной прокрутке.
- подтягивающая загрузка
Пока я могу думать только об этих пунктах, ниже я буду реализовывать их один за другим.
раздвижное окно
Почему это раздвижное окно? Локально мы сохраняем очень длинный список данных, но добавлять их все в представление не обязательно, пользователю нужны и видны только те данные, которые отображаются в текущем окне просмотра, в этом случае мы можем использовать контейнер Store данные, которые должен видеть текущий пользователь, а затем отображать данные в этом контейнере для пользователя. Этот контейнер можно рассматривать как небольшое окно. Когда пользователь отправляет запрос на просмотр дополнительных данных, маленькое окно перемещается, а затем обновленный вид.
Итак, насколько велик размах этого окна?
- Если это просто высота вьюпорта, то при перемещении окна вниз нужно убрать элемент в верхней части окна, потому что пользователю не нужно его видеть, а затем протолкнуть следующие данные в самый низ окна окно, то окно перемещается быстро. , частота обновления будет очень быстрой
- Если вы немного увеличите окно, вы можете уменьшить указанную выше частоту обновления, что эквивалентно троттлингу, в зависимости от размера окна.
Теперь увеличим окно, принцип прост для понимания с картинкой
Конкретный подход заключается в том, что если на странице отображается 10 фрагментов данных, то я фактически отрисовываю 20 фрагментов данных и делю эти 20 фрагментов данных на 2 части, когда видимая область перемещается к краю контейнера.
- Если верхний край видимой области касается верхнего края контейнера, заполните вторую половину элемента первой половиной элемента, затем перенесите 10 фрагментов данных вперед в исходных данных, чтобы заполнить первую половину, а затем переместите позицию контейнера вверх на 10 элементов в высоту
- Вопреки вышесказанному
DOM-структура контейнера выглядит так
<div ref="fragment" class="fragment" :style="{ transform: `translate3d(0, ${translateY}px, 0)` }">
<template v-for="item in currentViewList">
<div :key="item.key">
<!-- item content -->
</div>
</template>
</div>
// 原始数据
const sourceList = [/* ... */]
// 状态1
const currentViewList = [...sourceList.slice(20, 30), ...sourceList.slice(30, 40)]
// 状态1 向下
currentViewList = [...sourceList.slice(30, 40), ...sourceList.slice(40, 50)]
// 状态1 向上
currentViewList = [...sourceList.slice(10, 20), ...sourceList.slice(20, 30)]
Translate используется здесь, потому что он может уменьшить ненужную верстку.В этой реализации перемещение контейнера является очень частой операцией, поэтому необходимо учитывать потребление верстки
событие прокрутки
Что касается поведения прокрутки, необходимо прояснить несколько моментов.Во-первых, посмотрите на картинку (что браузер должен сделать для отображения каждого кадра), а друзья, которым нужно знать больше, могут проверить соответствующую информацию.
- Прокрутка не должна быть непрерывной, например, быстрое перетаскивание полосы прокрутки.
- Событие прокрутки выполняется перед отрисовкой каждого кадра со своим собственным эффектом дросселирования и «синхронизируется» с каждым кадром. Нужно только убедиться, что логика обратного вызова проста и достаточно быстра, и постараться не запускать операцию перекомпоновки для убедитесь, что это не повлияет на оригинал Некоторые эффекты плавной прокрутки
полоса прокрутки
Требование к поведению прокрутки определяет использование нативной прокрутки, которая на самом деле очень проста.Так как нам также нужно реализовать функцию загрузки подтягивания, нам обязательно нужно поставить загрузку внизу.В этом случае мы можем установить значение paddingTop для загрузки и размер Item Высота списка умножается на длину списка, так что полоса прокрутки является реальной полосой прокрутки
<div ref="fragment" class="fragment" :style="{ transform: `translate3d(0, ${translateY}px, 0)` }">
<!---->
</div>
<div class="footer" :style="{ paddingTop: `${loadingTop}px` }">
<div class="footer-loading">Loading......</div>
</div>
Вам нужен ключ?
Тогда для Item в контейнере по характеристикам алгоритма vdom diff:
- В случае с настройками ключей, половине из них нужно только изменить свои позиции при обновлении, а другая половина будет удалена, а затем будет добавлена половина DOM.Если я быстро перетащу полосу прокрутки вручную, все DOM могут быть удалены и созданы заново.
- Если ключ не установлен, то 20 элементов не будут удалены.В этом случае, если быстро перетащить полосу прокрутки, нет необходимости пересоздавать DOM, но каждый элемент будет каждый раз повторно использоваться на месте.Недостаток в том, что узлы, которые могли быть только перемещены, также повторно используются на месте
Вероятно, догадка неубедительна. После того, как я ее написал, я сравнил два случая и протестировал их много раз. Я обнаружил, что разница между ними не очень велика (может быть, из-за моего компьютера). После нескольких тестов ситуация выглядит как когда ключ не используется чуть лучше
не использовать ключ
использовать ключ
На самом деле, я не сталкивался с таким спросом в последние несколько лет, здесь я предпочитаю не использовать ключевой рендеринг.
оценка критической точки
Здесь есть много способов, которые можно передать в событии прокруткиgetBoundingClientRectВычисляется после получения положения контейнера относительно области просмотра. У некоторых друзей здесь могут быть вопросы,getBoundingClientRectРазве метод не вызывает перекомпоновку? Вы часто вызываете этот метод для событий прокрутки, разве это не плохо для производительности? Давайте рассмотрим 2 небольших примера:
// 例1
setInterval(() => {
console.log(document.body.offsetHeight)
}, 100)
// 例2
let height = 1000
setInterval(() => {
document.body.style.height = `${height++}px`
console.log(document.body.offsetHeight)
}, 100)
Очевидно, что пример 1 здесь не вызовет перекомпоновки, а пример 2. Причина в том, что вы обновляете свойства, связанные с макетом, в текущем фрейме, и в то же время делаете запрос после установки, что заставляет браузер выполнять макет чтобы получить правильный макет. Значение возвращается вам. Поэтому, что касается свойств, о которых мы обычно говорим, которые приводят к макету, то будет использоваться не макет, а то, как вы его используете.
Тогда логика критической точки, вероятно, такова:
const innerHeight = window.innerHeight
const { top, bottom } = fragment.getBoundingClientRect()
if (bottom <= innerHeight) {
// 到达最后一个Item,向下
}
if (top >= 0) {
// 到达第一个Item,向上
}
Обратите внимание, что при прокрутке страницы логика вверх или вниз срабатывает здесь нечасто. В качестве примера возьмем нисходящую логику, когда срабатывает нисходящая логика, контейнерtranslateYОбновление значения (эквивалентно перемещению вниз на 10 высоты элемента) переводит вниз и одновременно обновляет элемент.После рендеринга следующего кадра нижний край контейнера возвращается к нижней части видимой области, а затем он срабатывать снова после прокрутки вниз на определенное расстояние Это на самом деле похоже на ленивую загрузку, только синхронно.
направление прокрутки
Только при прокрутке вниз надо выполнять логику для вниз, и то же самое для прокрутки вверх. Чтобы обрабатывать логику разных направлений, необходимо вычислить текущее направление прокрутки, что можно сделать, напрямую сохранив последнее значение.
let oldTop = 0
const scrollCallback = () => {
const scrollTop = getScrollTop(scroller)
if (scrollTop > oldTop) {
// 向下
} else {
// 向上
}
oldTop = scrollTop
}
выполнить
В сочетании с предыдущим кодом давайте сначала привяжем событие прокрутки
const innerHeight = window.innerHeight
// 滚动容器
const scroller = window
// Item容器
const fragment = this.$refs.fragment
let oldTop = 0
const scrollCallback = () => {
const scrollTop = getScrollTop(scroller)
const { top, bottom } = fragment.getBoundingClientRect()
if (scrollTop > oldTop) {
// 向下
if (bottom <= innerHeight) {
// 到达最后一个Item
this.down(scrollTop, bottom) // 待实现
}
} else {
// 向上
if (top >= 0) {
// 到达第一个Item
this.up(scrollTop, top) // 待实现
}
}
oldTop = scrollTop
}
scroller.addEventListener('scroll', scrollCallback)
ленивая загрузка
При работе с полосой прокрутки мы добавили тег загрузки, здесь нам нужно только оценить, появляется ли элемент загрузки в видимой области в событии прокрутки, и запустить логику загрузки, как только он появится. Здесь следует рассмотреть граничную ситуацию. После запуска логики загрузки исходные данные необходимо обновить при получении данных ответа. Если я останусь внизу в это время, новые данные должны быть автоматически отображены; если I Если вы прокрутите вверх до того, как получите данные, вам не нужно обновлять новые данные в представлении после получения ответа.
const loadCallback = () => {
if (this.finished) {
// 没有数据了
return
}
const { y } = loadGuard.getBoundingClientRect()
if (y <= innerHeight) {
if (this.loading) {
// 不能重复加载
return
}
this.loading = true
// 执行异步请求
}
}
прокрутить вниз
Во-первых, необходимо выполнить некоторую связанную обработку границ, напримерcurrentViewListОбъема данных недостаточно для прокрутки вниз и т. д. Главное отметить: прокрутка не обязательно непрерывная
down (scrollTop, y) {
const { size, currentViewList } = this
const currentLength = currentViewList.length
if (currentLength < size) {
// 数据不足以滚动
return
}
const { sourceList } = this
if (currentLength === size) {
// 单独处理第二页
this.currentViewList.push(...sourceList.slice(size, size * 2))
return
}
const length = sourceList.length
const lastKey = currentViewList[currentLength - 1].key
// 已经是当前最后一页了,但可能正在加载新的数据
if (lastKey >= length - 1) {
return
}
let startPoint
const { pageHeight } = this
if (y < 0) {
// 直接拖动滚动条,导致容器底部边缘直接出现在可视区上方,这种情况通过列表高度算出当前位置
const page = (scrollTop - scrollTop % pageHeight) / pageHeight + (scrollTop % pageHeight === 0 ? 0 : 1) - 1
startPoint = Math.min(page * size, length - size * 2)
} else {
// 连续的向下滚动
startPoint = currentViewList[size].key
}
this.currentViewList = sourceList.slice(startPoint, startPoint + size * 2)
}
прокрутить вверх
Обработка прокрутки вверх аналогична прокрутке вниз, и сюда непосредственно вставляется код.
up (scrollTop, y) {
const { size, currentViewList } = this
const currentLength = currentViewList.length
if (currentLength < size) {
return
}
const firstKey = currentViewList[0].key
if (firstKey === 0) {
return
}
let startPoint
const { sourceList, innerHeight, pageHeight } = this
if (y > innerHeight) {
const page = (scrollTop - scrollTop % pageHeight) / pageHeight + (scrollTop % pageHeight === 0 ? 0 : 1) - 1
startPoint = Math.max(page * size, 0)
} else {
startPoint = currentViewList[0].key - size
}
this.currentViewList = sourceList.slice(startPoint, startPoint + size * 2)
},
На данный момент эти функции почти реализованы.Если вы хорошенько обдумаете, если вы не используете какую-либо библиотеку или фреймворк для непосредственного управления DOM, вы сможете добиться лучшей производительности, потому что вы можете перемещать и повторно использовать DOM. более непосредственно, и в то же время меньше Слой vnode добавляется, чтобы уменьшить потребление внутреннего слоя, но он теряет лучшую ремонтопригодность.Если эту функцию можно разработать как отдельный плагин, ее можно рассмотреть. Если данные на локальном сервере, похоже, что это можно выброситьsourceList, в этом случае память страницы уменьшится, и в результате время белого экрана немного увеличится. Пишет относительно быстро, немного коряво, возможны ошибки, если есть ошибки, пожалуйста, оставьте сообщение.