1. Введение
Платформа BI является очень важным продуктом уровня платформы для команды мидл-офиса Alibaba Data.Чтобы обеспечить хороший опыт редактирования и просмотра отчетов, оптимизация производительности имеет важное значение.
В настоящее время инструменты BI обычно представлены в виде отчетов. Вы должны знать, что форма отчета — это не просто компонент диаграммы. Условия фильтрации и отношения связи, связанные с этими компонентами, сложны. Любое изменение условий фильтрации вызовет связанные с ним элементы для повторной выборки и повторного рендеринга компонентов. , а объем данных отчета очень велик, и компонент таблицы обычно загружает миллионы данных. Чтобы поддерживать нормальное отображение такого большого объем данных, рендеринг по запросу является обязательным.
Упомянутый здесь рендеринг по запросу не относится к бесконечной прокрутке ListView, потому что режим макета отчета включает в себя три набора макета потока, макет плитки и свободный макет, Каждый стиль макета очень отличается, и его невозможно использовать фиксированная формула для расчета видимости компонента Мы решили инициализировать полную визуализацию компонентов, чтобы предотвратить повторную визуализацию компонентов ниже сгиба. Поскольку данные не были получены при начальных условиях, полная визуализация не вызовет проблем с производительностью, что является предпосылкой этой схемы.
Итак, сегодня я представлю техническое решение, как использовать DOM, чтобы определить, виден ли компонент на холсте. Оно будет разбито шаг за шагом с точки зрения проектирования архитектуры и абстракции кода. Я не только надеюсь, что вы легко понять, как реализовано это техническое решение, но также надеяться, что вы сможете освоить этот один из приемов, научитесь делать выводы из других.
2 Интенсивное чтение
Мы берем React Frame в качестве примера, чтобы сделать способ рендеринга, способ рендеринга выглядит следующим образом:
Получить компонентactive
статус -> блокировка неactive
Рендеринг компонента.
Здесь я решил начать с результата, сначала рассмотреть, как заблокировать отрисовку компонента, а затем шаг за шагом вывести, как написать функцию, чтобы определить, виден ли компонент.
Блокировка повторного рендеринга компонента
нам нужноRenderWhenActive
компоненты, поддерживающиеactive
параметр, когдаactive
При значении true этот слой прозрачен, приactive
Когда false блокирует весь рендеринг.
Чтобы быть более конкретным, эффект выглядит следующим образом:
- Когда inActive любые изменения реквизита не приведут к отображению компонента.
- При переключении с inActive на active свойства, ранее примененные к компоненту, вступят в силу немедленно.
- Если реквизиты не изменились с момента переключения на активный, это также не должно вызывать повторный рендеринг.
- Переключение с активного на неактивное не должно запускать рендеринг и немедленно блокировать последующие повторные рендеринги.
В настоящее время функциональный компонент не может этого сделать, нам все еще нужно использовать классовый компонент.shouldComponentUpdate
Это сделано потому, что компонент класса будет хранить последние реквизиты при блокировке рендеринга, а компонент функции вообще не имеет внутреннего состояния и в настоящее время не соответствует заданию.
Мы можем написатьRenderWhenActive
Компоненты легко реализуют эту функциональность:
class RenderWhenActive extends React.Component {
public shouldComponentUpdate(nextProps) {
return nextProps.active;
}
public render() {
return this.props.children
}
}
Активное состояние компонента сбора данных
Прежде чем дальше думать, не опускайтесь до "как определить, отображается ли компонент", можно предположить, что "такая функция уже есть", как ее вызывать.
Очевидно, нам нужен собственный хук:useActive
Определяем активен ли компонент и получаем егоactive
Возвращаемое значение передаетсяRenderWhenActive
Компоненты:
const ComponentLoader = ({ children }) => {
const active = useActive();
return <RenderWhenActive active={active}>{children}</RenderWhenActive>;
};
Таким образом, движок рендеринга используетComponentLoader
Любой визуализируемый компонент имеет возможность рендеринга по требованию.
Реализовать useActive
Теперь, когда компоненты и процесс стороны хука полностью связаны, мы можем сосредоточиться на том, как добитьсяuseActive
Этот крючок.
Используя Hooks API, вы можете использовать его после рендеринга компонента.useEffect
Определите, является ли компонент активным, и используйтеuseState
Сохраните это состояние:
export function useActive(domId: string) {
// 所有元素默认 unActive
const [active, setActive] = React.useState(false);
React.useEffect(() => {
const visibleObserve = new VisibleObserve(domId, "rootId", setActive);
visibleObserve.observe();
return () => visibleObserve.unobserve();
}, [domId]);
return active;
}
При инициализации активное состояние всех компонентов ложно, но это состояние находится вshouldComponentUpdate
Он не блокирует первый рендеринг, поэтому инициализация узла dom компонента все равно будет рендериться.
существуетuseEffect
Стадия зарегистрированаVisibleObserve
Этот пользовательский класс используется для мониторинга узла компонента dom в его родительском узле.rootId
Виден ли он внутри и выбрасывается ли он третьим обратным вызовом при изменении состояния, здесь будетsetActive
В качестве третьего параметра активное состояние текущего компонента может быть изменено во времени.
VisibleObserve
Эта функция имеетobserve
а такжеunobserve
Два API начинают прослушивание и отменяют прослушивание, используяuseEffect
Функция обратного вызова выполняется при его уничтожении, а также завершается механизм мониторинга и уничтожения.
Следующий шаг — как достичь наибольшего ядраVisibleObserve
Функция для контроля за видимостью компонента.
Подготовка к мониторингу видимости компонента
в реализацииVisibleObserve
Прежде чем думать об этом, есть несколько способов добиться этого? Там может быть много странных сценариев, появляющихся в вашем уме. Да, есть много решений для судейства, видно ли компонент в контейнере. Даже если оптимальное решение можно найти функционально, идеальное решение не может быть найдено с точки зрения совместимости. Следовательно, это решение с несколькими возможностями реализации. Лучшая стратегия - использовать разные решения в разных версиях браузеров.
Один из способов справиться с этой ситуацией — создать абстрактный класс и позволить всем фактическим методам наследовать и реализовать абстрактный класс, чтобы у нас было несколько наборов «разных реализаций одного и того же API», которые можно переключать в любое время. в любое время в различных сценариях.
использоватьabstract
Создать абстрактный классAVisibleObserve
, реализовать конструктор и объявить две общедоступные важные функцииobserve
а такжеunobserve
:
/**
* 监听元素是否可见的抽象类
*/
abstract class AVisibleObserve {
/**
* 监听元素的 DOM ID
*/
protected targetDomId: string;
/**
* 可见范围根节点 DOM ID
*/
protected rootDomId: string;
/**
* Active 变化回调
*/
protected onActiveChange: (active?: boolean) => void;
constructor(targetDomId: string, rootDomId: string, onActiveChange: (active?: boolean) => void) {
this.targetDomId = targetDomId;
this.rootDomId = rootDomId;
this.onActiveChange = onActiveChange;
}
/**
* 开始监听
*/
abstract observe(): void;
/**
* 取消监听
*/
abstract unobserve(): void;
}
Таким образом, мы можем реализовать несколько решений. После небольшого мышления мы можем найти, что нам нужно только два набора решений, один должен использоватьsetInterval
Глупый способ реализовать обнаружение опроса, один из них - использовать высокоуровневый API браузера.IntersectionObserver
Модный способ реализации, поскольку у последнего есть требования совместимости, первый реализуется как решение «снизу вверх».
Таким образом, мы можем определить два набора соответствующих методов:
class IntersectionVisibleObserve extends AVisibleObserve {
constructor(/**/) {
super(targetDomId, rootDomId, onActiveChange);
}
observe() {
// balabala..
}
unobserve() {
// balabala..
}
}
class SetIntervalVisibleObserve extends AVisibleObserve {
constructor(/**/) {
super(targetDomId, rootDomId, onActiveChange);
}
observe() {
// balabala..
}
unobserve() {
// balabala..
}
}
Наконец, сделайте общий класс в качестве записи вызова:
/**
* 监听元素是否可见总类
*/
export class VisibleObserve extends AVisibleObserve {
/**
* 实际 VisibleObserve 类
*/
private actualVisibleObserve: AVisibleObserve = null;
constructor(targetDomId: string, rootDomId: string, onActiveChange: (active?: boolean) => void) {
super(targetDomId, rootDomId, onActiveChange);
// 根据浏览器 API 兼容程度选用不同 Observe 方案
if ('IntersectionObserver' in window) {
// 最新 IntersectionObserve 方案
this.actualVisibleObserve = new IntersectionVisibleObserve(targetDomId, rootDomId, onActiveChange);
} else {
// 兼容的 SetInterval 方案
this.actualVisibleObserve = new SetIntervalVisibleObserve(targetDomId, rootDomId, onActiveChange);
}
}
observe() {
this.actualVisibleObserve.observe();
}
unobserve() {
this.actualVisibleObserve.unobserve();
}
}
В конструкторе оценивается, поддерживает ли текущий браузерIntersectionObserver
API, однако, экземпляры независимо от того, какая программа создана для наследованияAVisibleObserve
, поэтому мы можем использовать унифицированныйactualVisibleObserve
Переменные-члены сохраняются.
observe
а такжеunobserve
Все этапы могут игнорировать реализацию конкретного класса и вызывать напрямуюthis.actualVisibleObserve.observe()
а такжеthis.actualVisibleObserve.unobserve()
эти два API.
Идея, воплощенная здесь, заключается в том, что родительский класс заботится об API уровня интерфейса, а подкласс заботится о том, как реализовать API на основе этого интерфейса.
Далее мы смотрим на низкую версию (совместимую) и высокую версию (родной) соответственно, как.
Виден ли прослушивающий компонент — совместимые версии
В режиме совместимой версии необходимо определить дополнительную переменную-член.interval
Сохраняет ссылку SetInterval вunobserve
когдаclearInterval
.
Я абстрагировал видимую функцию его сужденияjudgeActive
В функции основное мышление заключается в том, чтобы определить, существуют ли два прямоугольника (компоненты, подлежащие оценке), включают отношения, если они включают установление, их можно увидеть, если не включены, включены.
Ниже приведена полная функция реализации:
class SetIntervalVisibleObserve extends AVisibleObserve {
/**
* Interval 引用
*/
private interval: number;
/**
* 检查是否可见的时间间隔
*/
private checkInterval = 1000;
constructor(targetDomId: string, rootDomId: string, onActiveChange: (active?: boolean) => void) {
super(targetDomId, rootDomId, onActiveChange);
}
/**
* 判断元素是否可见
*/
private judgeActive() {
// 获取 root 组件 rect
const rootComponentDom = document.getElementById(this.rootDomId);
if (!rootComponentDom) {
return;
}
// root 组件 rect
const rootComponentRect = rootComponentDom.getBoundingClientRect();
// 获取当前组件 rect
const componentDom = document.getElementById(this.targetDomId);
if (!componentDom) {
return;
}
// 当前组件 rect
const componentRect = componentDom.getBoundingClientRect();
// 判断当前组件是否在 root 组件可视范围内
// 长度之和
const sumOfWidth =
Math.abs(rootComponentRect.left - rootComponentRect.right) + Math.abs(componentRect.left - componentRect.right);
// 宽度之和
const sumOfHeight =
Math.abs(rootComponentRect.bottom - rootComponentRect.top) + Math.abs(componentRect.bottom - componentRect.top);
// 长度之和 + 两倍间距(交叉则间距为负)
const sumOfWidthWithGap = Math.abs(
rootComponentRect.left + rootComponentRect.right - componentRect.left - componentRect.right,
);
// 宽度之和 + 两倍间距(交叉则间距为负)
const sumOfHeightWithGap = Math.abs(
rootComponentRect.bottom + rootComponentRect.top - componentRect.bottom - componentRect.top,
);
if (sumOfWidthWithGap <= sumOfWidth && sumOfHeightWithGap <= sumOfHeight) {
// 在内部
this.onActiveChange(true);
} else {
// 在外部
this.onActiveChange(false);
}
}
observe() {
// 监听时就判断一次元素是否可见
this.judgeActive();
this.interval = setInterval(this.judgeActive, this.checkInterval);
}
unobserve() {
clearInterval(this.interval);
}
}
Судя по контейнеруrootDomId
с компонентамиtargetDomId
, мы можем получить соответствующий экземпляр DOM и вызватьgetBoundingClientRect
Получите положение, ширину и высоту соответствующего прямоугольника.
Идея алгоритма следующая:
Контейнер предоставляется для корня компонента, component.
- Рассчитайте длину корня и компонента
sumOfWidth
сумма шириныsumOfHeight
. - Вычислить сумму длин корня и компонента + двойной интервал
sumOfWidthWithGap
сумма ширины + двойной интервалsumOfHeightWithGap
. -
sumOfWidthWithGap - sumOfWidth
Разница в поперечном расстоянии GAP,sumOfHeightWithGap - sumOfHeight
Разница — это расстояние по горизонтали, и оба значения отрицательные, чтобы указать внутреннюю часть.
Ключевым моментом здесь является то, что с горизонтальной точки зрения следующую формулу можно понимать как сумму ширин + удвоенный интервал ширины:
// 长度之和 + 两倍间距(交叉则间距为负)
const sumOfWidthWithGap = Math.abs(
rootComponentRect.left +
rootComponentRect.right -
componentRect.left -
componentRect.right
);
а такжеsumOfWidth
представляет собой сумму ширин, а разница между ними равна удвоенному значению интервала. Положительное число означает, что горизонтального пересечения нет. Когда и горизонтальное, и вертикальное пересечения отрицательные, это означает, что пересечение есть или содержится внутри.
Виден ли прослушивающий компонент — нативная версия
если браузер это поддерживаетIntersectionObserver
С этим API намного проще работать, вот полный код:
class IntersectionVisibleObserve extends AVisibleObserve {
/**
* IntersectionObserver 实例
*/
private intersectionObserver: IntersectionObserver;
constructor(targetDomId: string, rootDomId: string, onActiveChange: (active?: boolean) => void) {
super(targetDomId, rootDomId, onActiveChange);
this.intersectionObserver = new IntersectionObserver(
changes => {
if (changes[0].intersectionRatio > 0) {
onActiveChange(true);
} else {
onActiveChange(false);
// 因为虚拟 dom 更新导致实际 dom 更新,也会在此触发,判断 dom 丢失则重新监听
if (!document.body.contains(changes[0].target)) {
this.intersectionObserver.unobserve(changes[0].target);
this.intersectionObserver.observe(document.getElementById(this.targetDomId));
}
}
},
{
root: document.getElementById(rootDomId),
},
);
}
observe() {
if (document.getElementById(this.targetDomId)) {
this.intersectionObserver.observe(document.getElementById(this.targetDomId));
}
}
unobserve() {
this.intersectionObserver.disconnect();
}
}
пройти черезintersectionRatio > 0
Вы можете определить, появляется ли элемент в родительском контейнере, еслиintersectionRatio === 1
Это означает, что компонент появляется в контейнере полностью, и наше требование здесь состоит в том, чтобы любая часть была активной, когда она появляется.
Следует отметить, что это суждение отличается от SetInterval, потому что виртуальный DOM React может обновлять экземпляр DOM, что приводит кIntersectionObserver.observe
После того, как отслеживаемый элемент DOM будет уничтожен, последующий мониторинг будет недействителен, поэтому необходимо добавить следующий код, когда элемент скрыт:
// 因为虚拟 dom 更新导致实际 dom 更新,也会在此触发,判断 dom 丢失则重新监听
if (!document.body.contains(changes[0].target)) {
this.intersectionObserver.unobserve(changes[0].target);
this.intersectionObserver.observe(document.getElementById(this.targetDomId));
}
- Когда элемент оценивается как находящийся за пределами видимой области, это также включает уничтожение элемента.
- следовательно
body.contains
Элемент определения уничтожается, и если он уничтожается, новый экземпляр DOM прослушивается повторно.
3 Резюме
Подводя итог, можно сказать, что применение логики рендеринга по требованию находится не только в движке рендеринга, более навязчиво добавить эту логику в код, написанный непосредственно в сцене ProCode.
Возможно, рендеринг по требованию в видимой области можно сделать внутри фреймворка фронтенд-разработки, хотя это и не стандартная функция фреймворка, но это не совсем бизнес-функция.
На этот раз оставьте вопрос для размышления: если в вашем рукописном коде React есть необходимость отрисовывать функцию, как это лучше?
Адрес обсуждения:Интенсивное чтение «Визуализация по требованию с помощью React», выпуск № 254 dt-fe/еженедельно
Если вы хотите принять участие в обсуждении, пожалуйста,кликните сюда, с новыми темами каждую неделю, выходящими по выходным или понедельникам. Интерфейс интенсивного чтения — поможет вам отфильтровать надежный контент.
Сфокусируйся наАккаунт WeChat для интенсивного чтения в интерфейсе
Заявление об авторских правах: Бесплатная перепечатка - некоммерческая - не производная - сохранить авторство (Лицензия Creative Commons 3.0)