[“吊打面试官“系列之] 一晚带你玩转图片懒加载及其底层原理

一晚带你玩转图片懒加载及其底层原理

课程大纲

  • 从浏览器底层渲染机制分析懒加载的意义
  • 最初基于JS盒模型实现的懒加载方案
  • 基于getBoundingClientRect的进阶方案
  • 手撕Lodash源码中的debounce(函数防抖)
  • 手撕Lodash源码中的throttle(函数节流)
  • 终极方案:IntersectionObserver
  • 未来设想:img.loading=lazy

基于JS盒模型的花瓣网瀑布流懒加载

一. 实现思路

比如页面就是一个容器,里面有三列,分别从服务器拿到很多数据,比如从服务器拿到50条数据。50条数据按照一定规则插入到三列当中,首先我会把50条数据里的前3条拿到,第一次插的时候直接往进插就可以了,每个card的图片大小不一样,每个card的高度也就不一样,宽度固定,但高度不一样。现在已经把前3个数据插进去了,3个插完之后,我从50个数据里再拿下一组三个,我首先会看一下这三列当中现在的高度的排列顺序,然后按三列现有的高度按它的内容由高到低进行排序,并且也会把我拿到的3条数据进行由低到高进行排序。把当前拿到的3条数据中最小的最低的插入上一条数据最高的那一列里;把第二小的插入到第二列里,把当前拿到的最高的插入到最小的列。这样就保证三列布局每3个往进插每3个往进插,最后三列的高度相差也不是特别大,这就是瀑布流无规则排列。宽度固定,调整图片高度排列顺序。
在这里插入图片描述

(二)实现步骤:

1.实现瀑布流效果

代码思路详细解剖
(1)最早期的模块化思想[没有用vue和react]
(2)写业务逻辑,我们会return个对象来,我们会写一个方法,叫init(),init()是我们当前模块的唯一入口。
(3)一会我们再在页面里要干什么都会调init()方法,在init()里控制先干什么后干什么。
(4)未来我们想实现功能,只需要用命名空间或用模块的名字调它的init()方法。
(5)这就是我们早期的基于闭包、基于惰性函数惰性思想的JS高阶编程技巧实现业务开发的模块化思想。
接下来
(6)第一步从服务器获取数据才能干我们接下来的事了,用async await请求utils的ajax方法请求本地里有一个data.json
(7)有数据后接下来该做数据绑定了,写个方法叫bindHTML(),把data传进去实现数据绑定
(8)数据绑定思路:一共有三列,接下来就把从服务器拿到的50条数据每3个为一组分别插入到3列当中,这么一步步处理就好了
但是在处理之前,从服务器拿到的数据data有一个特点,每一个数据里都包含图片的高和宽,宽和高是按照图片本身来的
实现瀑布流就要有宽高,没有宽高就要服务器处理,一般服务器返回的图片都会有宽和高。服务器返回的数据里图片的宽度是300,
但是我们要把数据插入这个列里,每一列是240,每一列左右还有5px padding,真实的是230.把300的图片放到230的区域里呈现
宽度就要缩小,从300缩到230,那高度也要同比例缩小一些才不会导致图片的变形。
(9)根据服务器返回的图片宽高,动态计算出图片放到230容器中,高度应该怎么缩放。因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度,这样才能用一个容器先占位。
(10)元素集合是类数组集合不是数组,未来想进行排序操作得要转换成数组。用Array.from把类数组集合转换成数组。
(11)data是50条数据50条数据要每3个去拿。
(12)js盒子模型的13个属性,clientHeight、clientWidth、clientLeft、clientTop、offsetHeight、offsetWidth、offsetLeft、offsetTop、offsetParent、scrollHeight、scrollWidth、scrollLeft、scrollTop

index.html
<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
	<title>珠峰在线Web高级课</title>
	<!-- IMPORT CSS -->
	<link rel="stylesheet" href="css/reset.min.css">
	<link rel="stylesheet" href="css/index.css">
</head>

<body>
	<div class="container clearfix">
		<div class="column">
			<!-- <div class="card">
				<a href="#">
					<div class="lazyImageBox">
						<img src="" alt="" data-image="images/1.jpg">
					</div>
					<p>泰勒·斯威夫特(Taylor Swift),19891213日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证</p>
				</a>
			</div> -->
		</div>
		<div class="column"></div>
		<div class="column"></div>
	</div>

	<!-- IMPORT JS -->
	<script src="js/utils.js"></script>
	<script src="js/index.js"></script>
</body>

</html>
/*index.js*/
let  imageModule=(function(){
    //元素集合是类数组集合不是数组,未来想进行排序操作得要转换成数组。用Array.from把类数组集合转换成数组
     let columns= Array.from(document.querySelectorAll('.column'));


     //数据绑定
     function bindHTML(data){
       //根据服务器返回的图片宽高,动态计算出图片放到230容器中,高度应该怎么缩放
       //因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度
       //这样才能用一个容器先占位
       data=data.map(item=>{
           let {
               width,
               height
           } = item;
           item.height=height/(width/230);
           item.width=230;
           return  item;
       });


       //每3个为一组获取数据
       for(let i = 0;i<data.length;i+=3){
           let group =data.slice(i,i+3);
          
          //实现每一列的降序
          columns.sort((a,b)=>{
              return b.offsetHeight - a.offsetHeight;
          });
          //把一组数据的进行升序
          group.sort((a,b)=>{
              return a.height - b.height;

          });
          

          //分别把最小数据插入到最大的列中
          group.forEach((item,index)=>{
              let{
                  width,
                  height,
                  title,
                  pic
              } = item;
              let card= document.createElement('div');
              card.className = "card";
              card.innerHTML =
              `<a href="#">
                   <div class="lazyImageBox" style="height:${height}px">
                      <img src="" alt="" data-image="${pic}">
                   </div>
                  <p>${title}</p>
               </a>`;
             columns[index].appendChild(card);
          });
       }
    

     }



     return {
        async init(){
            let data = await utils.ajax('./data.json');
           // console.log(data);获取到50条数据了
            bindHTML(data);
         }
     }

})();
imageModule.init();

瀑布流效果
在这里插入图片描述

2.图片显示

let imageModule = (function () {
    //元素集合是类数组集合不是数组,未来想进行排序操作得要转换成数组。用Array.from把类数组集合转换成数组
    let columns = Array.from(document.querySelectorAll('.column'));


    //数据绑定
    function bindHTML(data) {
        //根据服务器返回的图片宽高,动态计算出图片放到230容器中,高度应该怎么缩放
        //因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度
        //这样才能用一个容器先占位
        data = data.map(item => {
            let {
                width,
                height
            } = item;
            item.height = height / (width / 230);
            item.width = 230;
            return item;
        });


        //每3个为一组获取数据
        for (let i = 0; i < data.length; i += 3) {
            let group = data.slice(i, i + 3);

            //实现每一列的降序
            columns.sort((a, b) => {
                return b.offsetHeight - a.offsetHeight;
            });
            //把一组数据的进行升序
            group.sort((a, b) => {
                return a.height - b.height;

            });


            //分别把最小数据插入到最大的列中
            group.forEach((item, index) => {
                let {
                    width,
                    height,
                    title,
                    pic
                } = item;
                let card = document.createElement('div');
                card.className = "card";
                card.innerHTML = `<a href="#">
                                    <div class="lazyImageBox" style="height:${height}px">
                                        <img src="" alt="" data-image="${pic}">
                                    </div>
                                    <p>${title}</p>
                                </a>`;
                columns[index].appendChild(card);
            });
        }


    }

    //实现图片的延迟加载
    let lazyImageBoxs;
    function lazyFunc() {
        !lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;

        lazyImageBoxs.forEach(lazyImageBox => {
            //已经处理过则不再处理
            let isLoad = lazyImageBox.getAttribute('isLoad');
            if (isLoad) return;
            lazyImg(lazyImageBox);
        });
    }

    function lazyImg(lazyImageBox) {
        let img = lazyImageBox.querySelector('img'),
            trueImg = img.getAttribute('data-image');
        img.src = trueImg;
        img.onload = function () {
            //   图片加载成功
            utils.css(img, 'opacity', 1);
        };
        img.removeAttribute('data-image');
        //记录当前图片都处理过了
        lazyImageBox.setAttribute('isLoad', 'true');
    }

    return {
        async init() {
            let data = await utils.ajax('./data.json');
            // console.log(data);获取到50条数据了
            bindHTML(data);
            setTimeout(lazyFunc, 500);//window.onload也可以
        }
    }

})();
imageModule.init();

图片显示
在这里插入图片描述

3.图片的延迟加载的详细原因、思路及实现

浏览器渲染页面

  • 1.构建DOM树
  • 2.构建CSSOM树
  • 3.生成RENDER TREE
  • 4.布局
  • 5.分层
  • 6.珊格化
  • 7.绘制
  • 构建DOM树中如果遇到img
  • 老版本:阻碍DOM渲染
  • 新版本:不会阻碍 每一个图片请求都会占用一个HTTP(浏览器同时发送的HTTP 6个)
  • 拿回来资源后会和RENDER TREE一起渲染
  • 开始加载图片,一定会让页面第一次渲染速度变慢(白屏)
  • 图片延迟加载:第一次不请求也不渲染图片,等页面加载完,其他资源都渲染好了,再去请求加载图片.
    懒加载的思路
    在这里插入图片描述
css
.card a .lazyImageBox {
	/* height: xxx;  如果是需要进行图片延迟加载,在图片不显示的时候,我们要让盒子的高度等于图片的高度,这样才能把盒子撑开(服务器返回给我们的数据中,一定要包含图片的高度和宽度) */
	/* background: url("../images/default.gif") no-repeat center center #F4F4F4; */
	overflow: hidden;
}
html
	<div class="lazyImageBox" style="height:${height}px">
						<img src="" alt="" data-image="${pic}">
					</div>

分析条件
临界点:如图当盒子刚刚完全显示在浏览器当前窗口中时,盒子顶部距离body的偏移量加上盒子本身的高度恰等于滚动条卷去的高度+浏览器的高度。
在这里插入图片描述
那么如果盒子底部距离页面顶部的长度小于滚动条卷去的高度+浏览器的高度,那么说明图片完全显示在页面视口中,就需要做延迟加载。

//实现图片的延迟加载
    let lazyImageBoxs;
  + let winH = document.documentElement.clientHeight;
    function lazyFunc() {
        !lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;

        lazyImageBoxs.forEach(lazyImageBox => {
            //已经处理过则不再处理
            let isLoad = lazyImageBox.getAttribute('isLoad');
            if (isLoad) return;

           + //加载条件:盒子底边距离BODY距离(盒子顶部距离body的偏移量加上盒子本身的高度)<浏览器距离BODY的高度(滚动条卷去的高度+浏览器的高度)
           +   let B=utils.offset(lazyImageBox).top+lazyImageBox.offsetHeight,
           +     A=winH+document.documentElement.scrollTop;
           + if(B<=A){
           +    lazyImg(lazyImageBox);
            }
         
        });
    }

在这里插入图片描述
那么如何实现随着滚动页面而实现的延迟加载?

 return {
        async init() {
            let data = await utils.ajax('./data.json');
            // console.log(data);获取到50条数据了
            bindHTML(data);
            setTimeout(lazyFunc, 500);//window.onload也可以
          +  window.onscroll = lazyFunc;
        }
    }

})();

在这里插入图片描述

基于getBoundingClientRect的进阶方案

(一)在浏览器中打开1.html。在控制台输入此方法,DOMRect包含了当前的盒子及盒子的样式,最下面的x和y一般不用,因为它兼容性特别差,width和height在ie678下是不兼容的。bottom、left、right、top都是兼容浏览器的。真实项目中已经完全用这种方案代替JS盒模型了,因为盒子模型太麻烦了,要计算很多值
在这里插入图片描述
在这里插入图片描述

let imageModule = (function () {
    //元素集合是类数组集合不是数组,未来想进行排序操作得要转换成数组。用Array.from把类数组集合转换成数组
    let columns = Array.from(document.querySelectorAll('.column'));


    //数据绑定
    function bindHTML(data) {
        //根据服务器返回的图片宽高,动态计算出图片放到230容器中,高度应该怎么缩放
        //因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度
        //这样才能用一个容器先占位
        data = data.map(item => {
            let {
                width,
                height
            } = item;
            item.height = height / (width / 230);
            item.width = 230;
            return item;
        });


        //每3个为一组获取数据
        for (let i = 0; i < data.length; i += 3) {
            let group = data.slice(i, i + 3);

            //实现每一列的降序
            columns.sort((a, b) => {
                return b.offsetHeight - a.offsetHeight;
            });
            //把一组数据的进行升序
            group.sort((a, b) => {
                return a.height - b.height;

            });


            //分别把最小数据插入到最大的列中
            group.forEach((item, index) => {
                let {
                    width,
                    height,
                    title,
                    pic
                } = item;
                let card = document.createElement('div');
                card.className = "card";
                card.innerHTML = `<a href="#">
                                    <div class="lazyImageBox" style="height:${height}px">
                                        <img src="" alt="" data-image="${pic}">
                                    </div>
                                    <p>${title}</p>
                                </a>`;
                columns[index].appendChild(card);
            });
        }


    }

    //实现图片的延迟加载
    let lazyImageBoxs;
    let winH = document.documentElement.clientHeight;
    function lazyFunc() {
        !lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;

        lazyImageBoxs.forEach(lazyImageBox => {
            //已经处理过则不再处理
            let isLoad = lazyImageBox.getAttribute('isLoad');
            if (isLoad) return;//加载条件:盒子底边距离BODY距离(盒子顶部距离body的偏移量加上盒子本身的高度)<浏览器距离BODY的高度(滚动条卷去的高度+浏览器的高度)
           _   // let B=utils.offset(lazyImageBox).top+lazyImageBox.offsetHeight,//     A=winH+document.documentElement.scrollTop;// if(B<=A){//     lazyImg(lazyImageBox);// }
           +  let {bottom}=lazyImageBox.getBoundingClientRect();
           +    if(bottom<=winH){
           +    lazyImg(lazyImageBox);
            }
         
        });
    }

    function lazyImg(lazyImageBox) {
        let img = lazyImageBox.querySelector('img'),
            trueImg = img.getAttribute('data-image');
        img.src = trueImg;
        img.onload = function () {
            //   图片加载成功
            utils.css(img, 'opacity', 1);
        };
        img.removeAttribute('data-image');
        //记录当前图片都处理过了
        lazyImageBox.setAttribute('isLoad', 'true');
    }

    return {
        async init() {
            let data = await utils.ajax('./data.json');
            // console.log(data);获取到50条数据了
            bindHTML(data);
            setTimeout(lazyFunc, 500);//window.onload也可以
            window.onscroll = lazyFunc;
        }
    }

})();
imageModule.init();

在这里插入图片描述
这么做了之后,我们当前的延迟就达到我们的效果了吗?No,还没有达到呢?我们说了在index.js,我们刚开始进来要做延迟加载,滚动的时候也要执行lazyFunc做延迟加载。
在这里插入图片描述
做个小测验,在lazyFunc(),打印OK。我们发现在浏览器向下滚动时中有很多个OK打印,说明lazyFunc被频繁触发了好多次,虽然最终没有达到条件和定义延迟加载,但这些东西被触发很多次,说明性能就会有所差距。所以在这个基础上要进行优化。
在这里插入图片描述
在这里插入图片描述
onscroll触发频率太高了,滚动一下可能要被触发很多次,导致很多没必要的计算和处理,消耗性能=>我们需要降低onscrll的时候的触发频率(节流)。

 return {
        async init() {
            let data = await utils.ajax('./data.json');
            // console.log(data);获取到50条数据了
            bindHTML(data);
            setTimeout(lazyFunc, 500);//window.onload也可以
            //onscroll触发频率太高了,滚动一下可能要被触发很多次,导致很多没必要的计算和处理,消耗性能=>我们需要降低onscrll
            //的时候的触发频率(节流)
           + window.onscroll = utils.throttle(lazyFunc,500);
        }
    }

在这里插入图片描述
整个频率降低了很多很多,达到了性能优化的过程。

终极方案:IntersectionObserverIntersectionObserver

上一步用的是getBoundingClientRect+节流进行了性能优化,看起来很好,这种方案现在不需要做什么防抖节流,即使节流也会触发很多没必要的操作,真实想做的操作就是只要它一出来就让它加载。不出来就不管它了,不是在onscroll随时校验,是真正达到这个条件再去做这个事情。那一定比我们的节流还要做的更好。我们节流也只是把之间的频率降低了而已,降低了也会有很多没必要的操作。IntersectionObserverIntersectionObserver能把上面讲的东西全部优化了,新出来的,这种方案的兼容性不是特别好,低版本浏览器是不兼容的。polyfill处理不了它,移动端不考虑低版本浏览器,一般都是这种方案。但是这个性能超好,不需要节流处理。
**IntersectionObserverIntersectionObserver**的简介。

1.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>珠峰在线Web高级课</title>
    <link rel="stylesheet" href="css/reset.min.css">
    <style>
        .box {
            width: 300px;
            margin: 1300px auto;
        }

        .box img {
            width: 100%;
        }
    </style>
</head>

<body>
    <div class="box" id="box">
        <img src="images/1.jpg" alt="">
    </div>

    <script>
        let observer = new IntersectionObserver(changes => {
            // changes包含所有监听对象的信息
            // target当前监听的对象
            // isIntersecting 是否出现在视口中
            // boundingClientRect 
            // ...
            console.log(changes);
        });
        observer.observe(box);
    </script>
</body>

</html>

在这里插入图片描述

由截图可知,这个方法刚开始会触发一次,当滚动到图片出现在视窗口中会再触发一次。完全离开的时候再触发一次。
离开时移除监听

    <script>
        let observer = new IntersectionObserver(changes => {
            // changes包含所有监听对象的信息
            // target当前监听的对象
            // isIntersecting 是否出现在视口中
            // boundingClientRect 
            // ...
            console.log(changes);
           + let item = changes[0];
           + if (item.isIntersecting) {
                // 进入到视口
                // ...
            +    observer.unobserve(item.target);
            + }
        });
        observer.observe(box);
    </script>

index2.js

let imageModule = (function () {
    let columns = Array.from(document.querySelectorAll('.column'));

    // 数据绑定
    function bindHTML(data) {
        // 根据服务器返回的图片的宽高,动态计算出图片放在230容器中,高度应该怎么缩放
        // 因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度,这样才能又一个容器先占位
        data = data.map(item => {
            let {
                width,
                height
            } = item;
            item.height = height / (width / 230);
            item.width = 230;
            return item;
        });

        // 每三个为一组获取数据
        for (let i = 0; i < data.length; i += 3) {
            let group = data.slice(i, i + 3);

            // 实现每一列的降序
            columns.sort((a, b) => {
                return b.offsetHeight - a.offsetHeight;
            });

            // 把一组的数据进行升序
            group.sort((a, b) => {
                return a.height - b.height;
            });

            // 分别把最小数据插入到最大的列中
            group.forEach((item, index) => {
                let {
                    height,
                    title,
                    pic
                } = item;
                let card = document.createElement('div');
                card.className = "card";
                card.innerHTML = `<a href="#">
                    <div class="lazyImageBox" style="height:${height}px">
                        <img src="" alt="" data-image="${pic}">
                    </div>
                    <p>${title}</p>
                </a>`;
                columns[index].appendChild(card);
            });
        }
    }

    // 实现图片的延迟加载
    // IntersectionObserver 监听DOM对象,当DOM元素出现和离开视口的时候触发回调函数
 +   let lazyImageBoxs,
 +      observer = new IntersectionObserver(changes => {
 +          changes.forEach(item => {
 +              console.log(changes)//刚开始有50个
 +              let {
 +                  isIntersecting,
 +                  target
 +              } = item;
 +              if (isIntersecting) {//出现在视口中
 +                  lazyImg(target);
 +                 observer.unobserve(target);//处理过的移除监听
 +                }
 +         });
 +     });

    function lazyFunc() {
        !lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;

        lazyImageBoxs.forEach(lazyImageBox => {
            observer.observe(lazyImageBox);
        });
    }

    function lazyImg(lazyImageBox) {
        let img = lazyImageBox.querySelector('img'),
            trueImg = img.getAttribute('data-image');
        img.src = trueImg;
        img.onload = function () {
            // 图片加载成功
            utils.css(img, 'opacity', 1);
        };
        img.removeAttribute('data-image');
    }

    return {
        async init() {
            let data = await utils.ajax('./data.json');
            bindHTML(data);
            setTimeout(lazyFunc, 500);
     _
     _   
       }
    }
})();
imageModule.init();

在这里插入图片描述

加载的效果几乎看不到下面没加载图片的空白区域,只有快速滚动才能看到效果。
这个方案还可以实现哪些功能?
加载到底部加载更多数据,在移动端如果不需要考虑太多低版本操作系统,做延迟加载时基本都用这种方案。思路如下:
在这里插入图片描述

未来设想:img.loading=lazy

未来的设想,啥也不用管,只要设置lazy,浏览器就会帮我们做延迟加载。 这种方案目前只兼容 Chrome 76,并且窗口高度 网速 滚动 窗口大小改变。
img.loading=lazy ,下一步要做的事情:我们自己在不兼容的情况下,写一个插件,兼容它(其实就是自己去实现一套处理方法).
index.html

	<script src="js/utils.js"></script>
	<script src="js/index3.js"></script>

index3.js

let imageModule = (function () {
    let columns = Array.from(document.querySelectorAll('.column'));

    // 数据绑定
    function bindHTML(data) {
        // 根据服务器返回的图片的宽高,动态计算出图片放在230容器中,高度应该怎么缩放
        // 因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度,这样才能又一个容器先占位
        data = data.map(item => {
            let {
                width,
                height
            } = item;
            item.height = height / (width / 230);
            item.width = 230;
            return item;
        });

        // 每三个为一组获取数据
        for (let i = 0; i < data.length; i += 3) {
            let group = data.slice(i, i + 3);

            // 实现每一列的降序
            columns.sort((a, b) => {
                return b.offsetHeight - a.offsetHeight;
            });

            // 把一组的数据进行升序
            group.sort((a, b) => {
                return a.height - b.height;
            });

            // 分别把最小数据插入到最大的列中
            group.forEach((item, index) => {
                let {
                    height,
                    title,
                    pic
                } = item;
                let card = document.createElement('div');
                card.className = "card";
                // Chrome 76
                // 窗口高度 网速 滚动 窗口大小改变 ...
                card.innerHTML = `<a href="#">
                    <div class="lazyImageBox" style="height:${height}px">
                        <img src="${pic}" alt="" loading="lazy">
                    </div>
                    <p>${title}</p>
                </a>`;
                columns[index].appendChild(card);
            });
        }
    }

    return {
        async init() {
            let data = await utils.ajax('./data.json');
            bindHTML(data);
        }
    }
})();
imageModule.init();

/* // 下一步要做的事情:我们自己在不兼容的情况下,写一个插件,兼容它(其实就是自己去实现一套处理方法)
if ('loading' in (new Image)) {
    console.log('ok');
} */
// typeof IntersectionObserver==="undefined"
// ...

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值