vue2.0实现瀑布流布局+虚拟列表,附带源码及效果预览视频

效果预览

vue2.0瀑布流+虚拟列表效果预览

目录

效果预览

一、什么是瀑布流布局

二、什么是虚拟列表

三、需求前提

四、源码


一、什么是瀑布流布局

瀑布流布局是现代浏览器常见布局之一,是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。

瀑布流布局通常用于电商、视频、图片网站等,例如抖音、花瓣、小红书等。其优点本文不做介绍。

二、什么是虚拟列表

虚拟列表是一种优化长列表性能的手段。

它能够做到节省内存、提升页面流畅性、提升用户体验等,通俗来讲就是只在你能看见的地方渲染元素,你看不见的地方部分渲染或者不渲染。

三、需求前提

实现瀑布流布局+虚拟列表,首先我的这个案例有一个大前提,即拿到的图片数据均为已知宽高!

1. 纯css还是js?

纯css实现瀑布流有点麻烦,不管是用column-count也好grid也好flex也好反正都行,但是它无法实现虚拟列表啊,所以毙掉这种方式,另外每一个元素都有自己的位置,那就需要绝对定位,因此肯定是选择js+绝对定位的方式。

还有,用原生js实现不会受到框架的限制,方便进行拓展。

2. translate还是left top?

translate不会引起重排而left top会,还能开启硬件加速,性能肯定强于left top,translate赢麻了。

3.什么时候才渲染真实dom?

当不存在虚拟列表时,dom元素排列如下图所示:

上刻线与下刻线, 是决定元素是否被渲染的重要参照,根据元素与参照的位置,我们得出以下结论:

  • 情况①元素处于上刻线之上,见图索引0 1 2 3 4 6
  • 情况②元素与上刻线交叉,见图索引7 5
  • 情况③元素处于上刻线与下刻线之间,见图索引8 9 10 11 12 13
  • 情况④元素与下刻线交叉,见图索引14 15 16
  • 情况⑤元素处于下刻线之下,见图索引17 18 19

使用虚拟列表后,上述所列情况中的①⑤不会进行渲染,其余情况均为渲染。

4. 需要对虚拟列表设置startIdx和endIdx?

需要,生成位置表(下文均有介绍)后,比起直接循环整个位置表,当位置表中记录了1000条甚至更多条记录时,startIdx和endIdx的存在明确了循环区间,极大的缩短循环次数,减少页面留白时间,提升性能。

5. 上下滚动时,如何进行添加、删除dom?

不管怎么滚动,都要做两件事情。

第一件事:

根据不同的滚动方向,添加dom元素

  • 滚动方向向下时,从endIdx + 1 处开始循环位置表到位置表尾,不断的添加dom,直到找到一个元素的位置属于情况⑤时停止添加。
  • 滚动方向向上时,从startIdx - 1 处倒序循环到索引0,不断的添加dom,直到找到一个元素的位置属于情况①时停止添加。

第二件事:

循环位置表,从startIdx至endIdx(如图6-1中的5与16),将对应索引元素的位置与上述第6点中提到的情况①⑤进行比较, 此时会出现:

  • 属于情况①⑤,检查已渲染表中是否 含有 该项,有即删除。
  • 不属于情况①⑤,检查已渲染表中是否 没有 该项,无则添加。

循环结束后更新startIdx、endIdx、已渲染表,下次发生滚动事件时继续重复这套逻辑。

解释下为何不直接循环已渲染表进行元素的删除?如果是这样,那就会出现一个bug:如图8-2所示,虚线框为上次视口位置,实线框为当前视口位置,如果按照直接循环已渲染表的方式,那么就只有删除dom这一种情况,于是图中索引6的dom应被删除,结束后更新startIdx、endIdx、已渲染表,那么如果此刻向上滚动,回到上次视口位置,startIdx将从5开始0结束进行寻找,索引为6的dom明明满足,却没有执行添加dom,造成了该位置缺失dom,从而形成了bug。

流程图:

四、源码

页面vue文件代码如下:

<template>
    <div class="container">
      <div class="water-fall-container">
        <div class="box">
            <div class="loading">加载中...</div>
        </div>
      </div>
      <div class="to-top" @click="onclick">↑</div>
    </div>

</template>
<script>
import WHList from './data'
import './debounce'
import'./throttle'
export default {
    name:'WaterfallVirtual',
    data(){
        return{
          waterfallContainerDom:'',
          containerDom:'',
          loadingDom:'',
          canvas:'',
          getTextLineHeightCtx:'',
          list:[],
          page:1,
          pageSize:50,
          hasNextPage:true,
          gap:16,
          columnWidth:0,
          containerTop:0,
          domDataList:[],
          positionList:[],
          renderMap:{},
          startIdx:0,
          endIdx:0,
          screenOffset:'',// 偏移量
          isLoadNextPage:false,// 是否加载下一页数据
          testList:[
            '《蜡笔小新》是一部于1992年出品的日本家庭搞笑动画片,该片主要由本乡满、原惠一、武藤裕治导演,日本朝日电视台于1992年4月13日播映了第一集。至今仍在播出。',
            '看过蜡笔小新的人,都知道,他有一个很逗的老爸——野原广志。 这位胡须浓密、面条脸的野原广志先生是一名普通的上班族,在车上享受着和周围女子相互挤攘的感觉(偶尔旁边是大叔也很囧)。',
            '脚臭的广志,小气的美伢,淘气的小新……',
          ],
          imgList:[
            "https://img2.baidu.com/it/u=3600821550,221281285&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500",
            "https://img0.baidu.com/it/u=2506471502,1373494428&fm=253&fmt=auto&app=120&f=JPEG?w=530&h=500",
            "https://img2.baidu.com/it/u=824566914,3863846826&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=500"
          ],
          resizeCallback:null,
          lastOffsetWidth:'',
          lastScrollNumY:0, // 上次滚动距离Y
          lastScrollNumX :0,// 上次滚动距离X
          scrollDirection:1,// 上次滚动方向 向下 为 1,向上为 -1
        }
    },
    methods:{
      getList(){// 获取数据
        return new Promise(resolve => {
          const start = (this.page - 1) * this.pageSize
          const nextList = WHList.slice(start, start + this.pageSize)
          this.hasNextPage = !!nextList.length
          this.list = this.page === 1 ? nextList : this.list.concat(nextList)
          setTimeout(() => {
            resolve(nextList)
          }, this.page === 1 ? 0 : 2000) // 模拟发送请求
        })
      },
      computeDomData(list, startRenderIdx = 0){// 计算数据形成 排序表
        const tempDomDataList = []
        for (let i = 0; i < list.length; i++) {
          const param = {
            idx: startRenderIdx + i,
            img:this.imgList[Math.trunc(Math.random() * 3)],
            columnIdx: 0,
            width: this.columnWidth,
            height: list[i].h * this.columnWidth / list[i].w,
            left: 0,
            top: 0,
            text: this.testList[Math.trunc(Math.random() * 3)],
            lineHeight: 74,// 根据css设置的值计算得到
          }
          // 排序,第一项必定是长度最短的一列
          this.positionList.sort((a, b) => a.columnHeight - b.columnHeight)
          param.columnIdx = this.positionList[0].columnIdx
          param.left = (param.columnIdx - 1) * (this.gap + this.columnWidth)
          
          param.top = this.positionList[0].columnHeight    

          const canvas = document.createElement('canvas')
          this.getTextLineHeightCtx = canvas.getContext('2d')
          this.getTextLineHeightCtx.font = '16px sans-serif'
          // css 样式表设置了 纵坐标的12px内边距,要加上
          param.lineHeight = this.getTextLineHeightCtx.measureText(param.text).width + 24 > this.columnWidth ? 98 : 78

          param.height += param.lineHeight
          this.positionList[0].columnHeight += param.height + this.gap
          tempDomDataList.push(param)
        }
        this.domDataList = this.domDataList.concat(tempDomDataList)

        // 设置容器高度
        this.positionList.sort((a, b) => a.columnHeight - b.columnHeight)
        this.containerDom.style.height = this.positionList[this.positionList.length - 1].columnHeight + 32 + 'px'

      },
      renderDomByDomDataList(startRenderIdx = 0){// 根据元素列表进行渲染
        if (!this.domDataList.length) return
        const tempRenderMap = {}
        let topIdx = startRenderIdx
        let bottomIdx = startRenderIdx

        // 处于这两条线之间的元素将被渲染进容器
        for (let i = startRenderIdx; i < this.domDataList.length; i++) {
          const { idx } = this.domDataList[i]
          const { overTopLine, underBottomLine } = this.checkIsRender(this.domDataList[i])
          const dom = this.containerDom.querySelector(`#item_${idx}`)
          if (overTopLine || underBottomLine) {
            dom?.remove()
            continue
          }
          topIdx = topIdx < idx ? topIdx : idx
          bottomIdx = bottomIdx < idx ? idx : bottomIdx

          if (dom) {
            tempRenderMap[idx] = this.createDom(dom, this.domDataList[i])
          } else {
            tempRenderMap[idx] = this.createDom(document.createElement('div'), this.domDataList[i])
            this.containerDom.append(tempRenderMap[idx])
          }
        }
        const keys = Object.keys(Object.assign(this.renderMap, tempRenderMap))
        this.startIdx = +keys[0]
        this.endIdx = +keys[keys.length - 1]
      },
      checkIsRender(params){// 计算元素是否符合渲染条件
        const { top, height } = params
        const y = top + height + this.containerTop
        // 1个视口的数据再快速滚动滚动条时大概率会有加载项,不妨扩大到上下各0.5个视口,共2个视口内的数据,这样就比较丝滑了,这里也是自由发挥
        const topLine = this.waterfallContainerDom.scrollTop - this.screenOffset
        const bottomLine = this.waterfallContainerDom.scrollTop + this.waterfallContainerDom.offsetHeight + this.screenOffset
        // 是否在上线之上
        const overTopLine = topLine > y
        // 是否在下线之下
        const underBottomLine = top > bottomLine
        return{
          overTopLine,
          underBottomLine,
        }
      },
      createDom(dom, param){// 创建瀑布流每一项  dom元素
        dom.classList.add('waterfall-item')
        dom.style.width = param.width + 'px'
        dom.style.height = param.height + 'px'
        dom.style.transform = `translate(${param.left}px, ${param.top}px)`
        dom.id = `item_${param.idx}`
        // <div class="main">${param.idx}</div>
        // <div class="main">${param.idx}</div>
        dom.innerHTML = `
              <image class="main" src="${param.img}" alt=""/>
              <div class="footer" style="height: ${param.lineHeight}px">
                <div class="text">${param.idx}--${param.text}</div>
                <div class="info">@脆脆土豆条 -《蜡笔小新》</div>
              </div>`
        return dom
      },
      getColumnNum(boxWidth){// 根据容器宽度获取显示列数(自由发挥)
        if (boxWidth >= 1600) return 5
        else if (boxWidth >= 1200) return 4
        else if (boxWidth >= 768 && boxWidth < 1200) return 3
        else return 2
      },
      computeColumnWidth(){// 计算瀑布流每一列列宽
        // 首先计算应呈现的列数
        const columnNum = this.getColumnNum(window.innerWidth)
        const allGapLength = this.gap * (columnNum - 1)
        this.columnWidth = (this.containerDom.offsetWidth - allGapLength) / columnNum
      },
      initPositionList(){// 重置瀑布流每一列数据
        this.positionList = []
        // 首先计算应呈现的列数
        for (let i = 0; i < this.getColumnNum(window.innerWidth); i++) {
          this.positionList.push({
            columnIdx: i + 1,
            columnHeight: 0
          })
        }
      },
      updateDomPosition(direction = 1){// 当滚动条滚动时,更新容器内的 每一项 元素是 插入 还是 删除
        const tempRenderMap = {}
        console.log(this,'updateDomPosition',this.endIdx)
        for (let i = this.startIdx; i <= this.endIdx; i++) {// 检查已渲染列表中的元素,不符合条件删除元素,反之插入元素

          if(!this.domDataList[i])  return
          const { overTopLine, underBottomLine } = this.checkIsRender(this.domDataList[i])
          if (overTopLine || underBottomLine) {
            this.renderMap[i]?.remove()
          } else if (this.renderMap[i]) {
            tempRenderMap[i] = this.renderMap[i]
          } else {
            tempRenderMap[i] = this.createDom(document.createElement('div'), this.domDataList[i])
            this.containerDom.append(tempRenderMap[i])
          }
        }
        // 向上
        if (direction < 0) {

          for (let i = this.startIdx - 1; i >= 0; i--) {

            const { overTopLine } = this.checkIsRender(this.domDataList[i])
            if (overTopLine) break
            tempRenderMap[i] = this.createDom(document.createElement('div'), this.domDataList[i])
            this.containerDom.append(tempRenderMap[i])
          }
        } else { // 向下

          for(let i = this.endIdx + 1; i < this.domDataList.length; i++) {


            const { underBottomLine } = this.checkIsRender(this.domDataList[i])
            // 只要找到Bottom在下线之下的立即停止
            if (underBottomLine) break
            tempRenderMap[i] = this.createDom(document.createElement('div'), this.domDataList[i])
            this.containerDom.append(tempRenderMap[i])
          }
        }
        this.renderMap = tempRenderMap
        const keys = Object.keys(this.renderMap)
        this.startIdx = +keys[0]
        this.endIdx = +keys[keys.length - 1]
      },
      resizeFn(){
        this.computeColumnWidth()
        // 如果宽度发生变化时,若列宽是一致的不用处理
        if (this.lastOffsetWidth !== window.innerWidth && this.columnWidth === this.domDataList[0]?.width) return
        this.lastOffsetWidth = window.innerWidth
        this.initPositionList()
        this.domDataList = []
        this.renderMap = {}
        this.computeDomData(this.list, 0)
        this.renderDomByDomDataList(0)
      },
      resize:window.debounce(function(){// 窗口变化事件
        console.log('resize')
          if (this.isLoadNextPage) {// 加载数据时发生了视口变化,保存回调
            this.resizeCallback = this.resizeFn()
            return
          }
          this.resizeFn()
        }, 150),
      handleScroll:window.throttle(async function(){// 窗口滚动事件
        this.waterfallContainerDom.scrollTop >= window.innerHeight ? this.gotoTopDom.classList.add('active') : this.gotoTopDom.classList.remove('active')
        this.scrollDirection = this.waterfallContainerDom.scrollTop - this.lastScrollNumY >= 0 ? 1 : -1
        this.lastScrollNumY = this.waterfallContainerDom.scrollTop

        this.updateDomPosition(this.scrollDirection)
        
        if (this.isLoadNextPage || !this.hasNextPage) return false
        if (this.waterfallContainerDom.scrollTop + this.waterfallContainerDom.offsetHeight >= this.waterfallContainerDom.scrollHeight * 0.85) {
          this.isLoadNextPage = true
          this.loadingDom.classList.add('active')
          this.page += 1
          const list = await this.getList()
          this.isLoadNextPage = false
          this.loadingDom.classList.remove('active')
          // 加载数据期间发生了视口变化时,执行一次回调
          if (this.resizeCallback) {
            this.resizeCallback()
            this.resizeCallback = null
          } else {
            // 节点信息排列完毕后进行渲染
            const startIdx = (this.page - 1) * this.pageSize
            this.computeDomData(list, startIdx)
            this.renderDomByDomDataList(startIdx)
          }
        }
      }, 150),
      onclick(){// 渠道顶部
        this.waterfallContainerDom.scrollTo({
          left: 0,
          top: 0,
          behavior: "smooth"
        })  
      },
      async getData(){
        this.computeDomData(await this.getList(), 0)
        this.renderDomByDomDataList(0)// 节点信息排列完毕后进行渲染
      }
    },
    mounted() {
      this.waterfallContainerDom = document.querySelector('.water-fall-container')
      this.screenOffset =this.waterfallContainerDom.offsetHeight / 2
      this.containerDom = document.querySelector('.box')
      this.loadingDom = document.querySelector('.loading')
      this.gotoTopDom = document.querySelector('.to-top')
      this.lastOffsetWidth = window.innerWidth
      this.waterfallContainerDom.addEventListener('scroll', ()=>{// 添加滚动事件监听器
        console.log('滚动事件触发');
        this.handleScroll()
      });
      window.addEventListener('resize', ()=>{// 添加滚动事件监听器
        console.log('视窗大小变化');
        this.resize()
      });
      this.computeColumnWidth()
      this.initPositionList()
      this.getData()
    },
    created(){
        this.$bus.emit('title', '虚拟列表+瀑布流');
    },
}
</script>
<style lang="less">
#app{
  width: 100%;
  height: 100vh;
  display: flex;
}
html{
  overflow: hidden;
}
.container{
  height: 100%;
  flex-grow: 1;
  flex-shrink: 0;
  padding-top: 0px;
}
.to-top{
  position: fixed;
  right: 40px;
  bottom: 40px;
  cursor: pointer;
  transform: scale(0);
  transition: transform .15s;
  width: 60px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background-color: #f8f8f8;
  color: tomato;
  font-size: 32px;
}
.to-top.active{
  transform: scale(1);
}

.loading{
  height: 32px;
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  opacity: 0;
  transition: all .15s;
}

.loading.active{
  opacity: 1;
}

.header{
  height: 80px;
  background-color: #aaa;
}

.water-fall-container::-webkit-scrollbar{
  width: 8px;
  background-color: #eee;
}
.water-fall-container::-webkit-scrollbar-thumb{
  background-color: #bbb;
  border-radius: 4px;
}

.water-fall-container::-webkit-scrollbar-thumb:hover{
  background-color: #aaa;
}

.water-fall-container{
  padding: 20px;
  height: calc(100% - 130px);
  overflow-y: scroll;
  overflow-x: hidden;
}

.box{
  position: relative;
  width: 100%;
}

.waterfall-item{
  position: absolute;
  transition: all .12s;
  font-family: sans-serif;
  display: flex;
  flex-direction: column;
}

.main{
  flex-grow: 1;
  flex-shrink: 0;
  background-color: pink;
  border-top-left-radius: 8px;
  border-top-right-radius: 8px;
  object-fit: contain;
}
.footer{
  box-sizing: border-box;
  padding: 12px;
  background-color: darksalmon;
  border-bottom-left-radius: 8px;
  border-bottom-right-radius: 8px;
}
.info{
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.text{
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  font-size: 16px;
  line-height: 24px;
  margin-bottom: 10px;
  letter-spacing: 0;
}
</style>

debounce文件内容如下 

var FUNC_ERROR_TEXT = 'Expected a function';

var NAN = 0 / 0;

var symbolTag = '[object Symbol]';

var reTrim = /^\s+|\s+$/g;

var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;

var reIsBinary = /^0b[01]+$/i;

var reIsOctal = /^0o[0-7]+$/i;

var freeParseInt = parseInt;

var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;

var freeSelf = typeof self == 'object' && self && self.Object === Object && self;

var root = freeGlobal || freeSelf || Function('return this')();

var objectProto = Object.prototype;


var objectToString = objectProto.toString;

var nativeMax = Math.max,
  nativeMin = Math.min;

var now = function() {
  return root.Date.now();
};

function debounce(func, wait, options) {
  var lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime,
    lastInvokeTime = 0,
    leading = false,
    maxing = false,
    trailing = true;

  if (typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  wait = toNumber(wait) || 0;
  if (isObject(options)) {
    leading = !!options.leading;
    maxing = 'maxWait' in options;
    maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }

  function invokeFunc(time) {
    var args = lastArgs,
      thisArg = lastThis;

    lastArgs = lastThis = undefined;
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
  }

  function leadingEdge(time) {
    // Reset any `maxWait` timer.
    lastInvokeTime = time;
    // Start the timer for the trailing edge.
    timerId = setTimeout(timerExpired, wait);
    // Invoke the leading edge.
    return leading ? invokeFunc(time) : result;
  }

  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime,
      result = wait - timeSinceLastCall;

    return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
  }

  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime;

    // Either this is the first call, activity has stopped and we're at the
    // trailing edge, the system time has gone backwards and we're treating
    // it as the trailing edge, or we've hit the `maxWait` limit.
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
  }

  function timerExpired() {
    var time = now();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    // Restart the timer.
    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  function trailingEdge(time) {
    timerId = undefined;

    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = lastThis = undefined;
    return result;
  }

  function cancel() {
    if (timerId !== undefined) {
      clearTimeout(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }

  function flush() {
    return timerId === undefined ? result : trailingEdge(now());
  }

  function debounced() {
    var time = now(),
      isInvoking = shouldInvoke(time);

    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        // Handle invocations in a tight loop.
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }
  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}

function isObject(value) {
  var type = typeof value;
  return !!value && (type == 'object' || type == 'function');
}

function isObjectLike(value) {
  return !!value && typeof value == 'object';
}

function isSymbol(value) {
  return typeof value == 'symbol' ||
    (isObjectLike(value) && objectToString.call(value) == symbolTag);
}

function toNumber(value) {
  if (typeof value == 'number') {
    return value;
  }
  if (isSymbol(value)) {
    return NAN;
  }
  if (isObject(value)) {
    var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
    value = isObject(other) ? (other + '') : other;
  }
  if (typeof value != 'string') {
    return value === 0 ? value : +value;
  }
  value = value.replace(reTrim, '');
  var isBinary = reIsBinary.test(value);
  return (isBinary || reIsOctal.test(value))
    ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
    : (reIsBadHex.test(value) ? NAN : +value);
}

window.debounce = debounce;

thorttle文件代码如下


var FUNC_ERROR_TEXT = 'Expected a function';

function throttle(func, wait, options) {
  var leading = true,
    trailing = true;

  if (typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  
  if (isObject(options)) {
    leading = 'leading' in options ? !!options.leading : leading;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }
  
  return window.debounce(func, wait, {
    'leading': leading,
    'maxWait': wait,
    'trailing': trailing
  });
}

function isObject(value) {
  return typeof value === 'object' && value!== null;
}

window.throttle = throttle;

data文件代码如下(这里我的是随机生成的,宽高可自行修改)

const WHList1 = [
    {
      "w": 600,
      "h": 600
    },
    {
      "w": 600,
      "h": 1067
    },
    {
      "w": 600,
      "h": 600
    },
    {
      "w": 600,
      "h": 1067
    },
    {
      "w": 600,
      "h": 800
    },
    {
      "w": 600,
      "h": 1067
    },
    {
      "w": 600,
      "h": 800
    },
    {
      "w": 600,
      "h": 600
    },
    {
      "w": 600,
      "h": 700
    },
    {
      "w": 600,
      "h": 600
    },
    {
      "w": 600,
      "h": 1067
    },
    {
      "w": 600,
      "h": 700
    },
    {
      "w": 600,
      "h": 700
    },
],
  let WHList = WHList1.concat(WHList2)
  export default WHList

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值