react 图片加载

225 阅读3分钟

图片懒加载的原理

懒加载就突出一个字,只有当我们访问到图片资源时再去加载这张图片。这个时候就有小伙伴要问了,那什么时候叫访问到图片资源时?就是当图片出现在视口时,也就是浏览器可视区域内,我们再去加载这张图片,给<img />标签的src属性赋值为真实的图片地址。

图片懒加载实现思路

要想实现图片懒加载,我们需要先学会一个api:

IntersectionObserver

IntersectionObserver 接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗 (viewport) 交叉状态的方法。祖先元素与视窗 (viewport) 被称为**根 (root)。

这是MDN上对这这个api的描述。

以往我们想实现一个图片懒加载,只能使用getBoundingClientRect()获取图片节点的位置信息,添加滚动监听轮询节点的位置,这样就会导致每一次滚动事件触发时,都会执行大量的循环,这势必会对性能造成一定的影响。

现在,我们使用IntersectionObserver同时对多个图片进行观察,简单高效,整体效果如下:

具体实现

具体实现的话分两个部分

第一部分

第一是在所有懒加载图片公共父容器中将实例化一个观察器

js
复制代码
const observer = new IntersectionObserver();

实例化时传入一个回调函数,第二个参数使用默认值就行。

js
复制代码
const observer = new IntersectionObserver((entries) => {
  entries.forEach(item => {
    if(item.intersectionRatio > 0) {
      lazy(item.target);
    }
  });
});

其中的entries是一个触发回调事件的数组

一个IntersectionObserverEntry对象的数组,每个被触发的阈值,都或多或少与指定阈值有偏差。

intersectionRatio>0,也就是dom出现在浏览器视口时,调用lazy函数加载真实图片,使用useRef将该实例缓存下来

js
复制代码
const observer = useRef();
useEffect(() => {
  observer.current = new IntersectionObserver((entries) => {
    entries.forEach(item => {
      if(item.intersectionRatio > 0) {
        lazy(item.target);
      }
    });
  });
  return () => {};
}, []);

接下来就是lazy函数了,lazy函数接收一个dom,当dom内的img标签不存在src或者src有更新时,将最新图片地址赋值为真实地址:

js
复制代码
const lazy = (dom) => {
  const imgDom = dom.children[0];
  if(!imgDom.src || imgDom.src !== imgDom.dataset.src) {
    imgDom.src = imgDom.dataset.src;
  }
  imgDom.onload = () => {
    dom.style.backgroundColor = '';
    dom.style.aspectRatio = '';
  }
};

这里的children[0]为懒加载图片组件中的img标签,当图片加载完成时,取消懒加载组件的填充样式。app.jsx中完整代码如下:

js
复制代码
import React, { useEffect, useState, useRef } from 'react';
import './app.css';
import LazyImg from '../components/LazyImg';

const App = () => {
  const observer = useRef();
  const [imgs, setImgs] = useState([]);
  useEffect(() => {
    fetchData(1);
    observer.current = new IntersectionObserver((entries) => {
      entries.forEach(item => {
        if(item.intersectionRatio > 0) {
          lazy(item.target);
        }
      });
    });
    const lazy = (dom) => {
      const imgDom = dom.children[0];
      if(!imgDom.src || imgDom.src !== imgDom.dataset.src) {
        imgDom.src = imgDom.dataset.src;
      }
      imgDom.onload = () => {
        dom.style.backgroundColor = '';
        dom.style.aspectRatio = '';
      }
    };
    return () => {
      // 取消所有图片懒加载组件的观察
      observer.current.disconnect()
    };
  }, []);
  const fetchData = (page) => {
    // 请求图片数据
    fetch('/src/pages/data.json')
    .then(data => data.json())
    .then(res => {
      setTimeout(() => {
        setImgs(imgs => {
          return page === 1 ? res.data.oneImgs : imgs.concat(res.data.twoImgs);
        });
      }, 500);
    });
  };
  return (
    <ul className='app'>
      <li><button onClick={() => {fetchData(2)}}>加载</button></li>
      {
        !!imgs.length && imgs.map((img, index) => {
          return <li key={index}>
            <LazyImg imgObj={img} index={index} observer={observer.current}/>
          </li>
        })
      }
    </ul>
  )
}

export default App;

图片数据示例, 其中basicColor为懒加载组件填充色,width,height主要是获取宽高比

js
复制代码
{
    "url": "https://dengxiang-image.oss-cn-shanghai.aliyuncs.com/images/01.jpeg",
    "width": 353,
    "height": 441,
    "basicColor": "#a44a00"
},

第二部分

接着我们就要实现一个懒加载的图片组件,该组件包含展示的图片,和加载完图片前的填充样式。

我们需要将图片数据和观察器传入组件中,像这样:

js
复制代码
<LazyImg imgObj={img} index={index} observer={observer.current}/>

使用useRef获取图片组件,并使用传入的观察器进行观察

js
复制代码
const ref = useRef(null);
useEffect(() => {
  if(ref.current && ref.current.children[0]) {
    observer.observe(ref.current);
  }
  return () => {};
}, [ref]);

给图片组件添加填充样式,使用data-src保存真实的图片地址

js
复制代码
return (
  <div ref={ref} className='img-box' style={{
    aspectRatio: imgObj.width / imgObj.height,
    backgroundColor: `${imgObj.basicColor}`
  }}>
    <img className='img' data-src={imgObj.url} />
    <span className='index'>{index + 1}</span>
  </div>
)

图片组件完整代码如下:

js
复制代码
import React, { useRef, useEffect } from 'react';
import './index.css';

export default function LazyImg({imgObj, index, observer}) {
  const ref = useRef(null);

  useEffect(() => {
    if(ref.current && ref.current.children[0]) {
      observer.observe(ref.current);
    }
    return () => {};
  }, [ref]);

  return (
    <div ref={ref} className='img-box' style={{
      aspectRatio: imgObj.width / imgObj.height,
      backgroundColor: `${imgObj.basicColor}`
    }}>
      <img className='img' data-src={imgObj.url} />
      <span className='index'>{index + 1}</span>
    </div>
  )
}