likes
comments
collection
share

长列表渲染优化

作者站长头像
站长
· 阅读数 16

前言

长列表其实可以分为两种,一种是元素不长但元素个数很,一种是元素个数不多,但元素都很

针对第一种,我们采用传统的虚拟列表去优化渲染,详情可看这篇文章:三种虚拟列表原理与实现

而第二种就是本文的主要内容了。

有“短”又长的列表

表现

这种列表之所以说短,是因为元素个数很少,而长,是因为元素高度很大,导致整个列表非常的长。我们来看一下这种列表的结构就明白了。

const List = () => (
  <div className="list">
    {new Array(10).fill(true).map((listItem, index) => (
      <div className="list-item" key={index}>
        {new Array(100).fill(true).map((item, i) => (
          <div className="child" key={`${index}-${i}`}>{`${index}-${i}`}</div>
        ))}
      </div>
    ))}
  </div>
);

长列表渲染优化

list中一共就只有10个元素,但每个元素都包含100个元素,且自身无滚动条,难以用传统的虚拟列表去解决。

毫无疑问,此时页面中已经有上千个dom了,如果每个元素再复杂点,页面难免会出现卡顿的现象。

分析

或许有人会说,这里一样可以用传统的虚拟列表去解决。但我们仔细想想,如果是传统的虚拟列表,可视区放一个,上下缓冲区放一个,那常态下也是有着至少300个dom,并不见得是好方案,并且,如果说我的这个List组件并不是如此纯净的列表呢?如果他是一个页面呢?

长列表渲染优化

这种情况下我们计算元素的top值是否就变得困难了。

到此,我们可以沿用虚拟列表的思想,想想看有没一种办法可以使我们知道那个ListItem出现在了可视区,只渲染它就好了,这样常态下只有100个元素,交接处最多才200。

解决方案

要想知道哪个listItem在可视区内,我们需要了解一个api:IntersectionObserver

IntersectionObserver

IntersectionObserver是一个观察者,我们可以指定让他观察某个元素,可以监听到观察元素是否与指定父盒子发生了交接。

看个demo:

<body>
  <div class="box">
    <div class="child"></div>
    <div class="child"></div>
    <div class="child"></div>
  </div>
</body>
<script>
const child = document.querySelector('.child');
    const box = document.querySelector('.box');

    // 第一个参数callback:当元素可见比例超过指定阈值后,会调用一个回调函数
    // 第二个参数options:其中有一项配置就是指定监听的父元素
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        console.log(entry);
      })
    }, { root: box });
    observer.observe(child);
</script>

以上demo的主要效果是,当第一个child进入或离开box的可视区内,都会触发callback。

长列表渲染优化

我们主要关注触发callback是打印的entry,这个entry中有一个很关键的属性isIntersecting,它的意思是,如果child在box的可视区内就为true,不在就为false。

以上只是简单介绍IntersectionObserver,若想了解更多建议去MDN学习:IntersectionObserver

主要思路

通过上面的IntersectionObserver,我们可以轻松获取到当前出现在List可视区内的LIstItem,之后我们只需要将不在可视区内的ListItem用一个等高的空盒子替换掉元素内容,撑开滚动条,然后只渲染可视区内的ListItem就好了。

实现

import React from 'react';

const ListItem = ({ index }) => {
  const [visible, setVisible] = React.useState(true);
  const ListItemRef = React.useRef();

  React.useEffect(() => {
    const box = document.querySelector('#list');
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        setVisible(entry.isIntersecting);
      })
    }, { root: box });
    observer.observe(ListItemRef.current);
    // 组件被卸载一定要记得取消观察,页面中观察者太多也是很耗性能的
    return () => {
      observer.unobserve(ListItemRef.current);
    };
  }, []);

  return (
    <div className="list-item" ref={ListItemRef}>
      <div style={{ height: 20 * 100, width: '100%', display: visible ? 'none' : 'block' }} />
      <div style={{ display: visible ? 'block' : 'none' }}>
        {new Array(100).fill(true).map((item, i) => (
          <div
            className="child"
            key={`${index}-${i}`}
            style={{ background: i % 2 ? 'lightblue' : 'lightcoral' }}
          >
            {`${index}-${i}`}
          </div>
        ))}
      </div>
    </div>
  );
};

const List = () => (
  <div id="list" >
    <header>页面的头部,这里包含各种标题,登录信息等</header>
    <section>页面的简介部分等等</section>
    {
      new Array(10).fill(true).map((listItem, index) => (
        <ListItem key={index} index={index} />
      ))
    }
  </div >
);

export default List;

长列表渲染优化

可以看到只有第一个在可视区内,所以就只有第一个的包含100个元素的盒子是block状态,其他都是none。

进阶版

相信有不少读者意识到了一点,上面例子中的元素高度都被固定,且用于撑开高度的空白盒子也是写死了2000的高度。但凡任何一个盒子可能发生高度变化,上面的代码就会出现不合理的地方了。接下来我们完善下它。

ResizeObserver

在这个版本我们需要认识这个ResizeObserverapi,一样是观察者家族的成员,他能指定观察的元素,当元素大小变化时就触发callback。

import React from 'react';
const Demo = () => {
  const [items, setItems] = React.useState([0, 1]);
  const boxRef = React.useRef();

  React.useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        console.log(entry);
      })
    })
    observer.observe(boxRef.current);
    return () => {
      observer.unobserve(boxRef.current);
    };
  }, []);

  return (
    <div style={{ borderBottom: '1px solid lightgreen' }} ref={boxRef}>
      <button onClick={() => setItems((pre) => [...pre, pre.length])}>增加元素</button>
      {items.map(item => (
        <div key={item}>{item}</div>
      ))}
    </div>
  );
};

export default Demo;

长列表渲染优化

以上demo可以看出,当所观察的盒子发生大小变化时,就会触发callback,我们可以在entry.contentRect中拿到height。

以上只是简单介绍ResizeObserver,若想了解更多建议去MDN学习:ResizeObserver

主要思路

我们只要观察listItem高度变化,当高度出现变化时,我们就更新空盒子的高度。

实现

import React, { useState } from 'react';

const Item = ({ index, str }) => {
  const [content, setContent] = useState([str]);

  return (
    <div
      className="child"
      key={index}
      style={{ background: index % 2 ? 'lightblue' : 'lightcoral' }}
    >
      {content.map((item, i) => (<div key={i}>{item}</div>))}
      <button onClick={() => setContent((pre) => [...pre, pre[0]])}>增加元素</button>
    </div>
  );
};

const ListItem = ({ index }) => {
  const [height, setHeight] = React.useState(0);
  const [visible, setVisible] = React.useState(true);
  const ListItemRef = React.useRef();
  const containerRef = React.useRef();

  React.useEffect(() => {
    const box = document.querySelector('#list');
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        setVisible(entry.isIntersecting);
      })
    }, { root: box });
    observer.observe(ListItemRef.current);
    return () => {
      observer.unobserve(ListItemRef.current);
    };
  }, []);

  React.useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.contentRect.height) {
          setHeight(entry.contentRect.height);
        }
      })
    })
    observer.observe(containerRef.current);
    return () => {
      observer.unobserve(containerRef.current);
    };
  }, []);

  return (
    <div className="list-item" ref={ListItemRef}>
      <div style={{ height, width: '100%', display: visible ? 'none' : 'block' }} />
      <div style={{ display: visible ? 'block' : 'none' }} ref={containerRef}>
        {new Array(100).fill(true).map((item, i) => (
          <Item key={`${index}-${i}`} str={`${index}-${i}`} index={i} />
        ))}
      </div>
    </div>
  );
};

const List = () => (
  <div id="list" >
    <header>页面的头部,这里包含各种标题,登录信息等</header>
    <section>页面的简介部分等等</section>
    {
      new Array(10).fill(true).map((listItem, index) => (
        <ListItem key={index} index={index} />
      ))
    }
  </div >
);

export default List;

长列表渲染优化

结尾

以上就是本文的所有内容了,看完本文和上一篇虚拟列表的文章,相信无论是何种长列表各位读者都能轻松拿捏。

上一篇::三种虚拟列表原理与实现

转载自:https://juejin.cn/post/7237856777588310073
评论
请登录