点击上方蓝字关注我们

背景:系统中有一个添加品牌的搜索框,当搜索类目不做限制的时候,全部的品牌列表会有1W多个,这时候在框架的加持下,操作速度感人。可以在https://codesandbox.io/s/pure-vue-kqber中体验一下,甚至不用打开控制台看console输出,就可以感受到载入长列表和重置之间切换时,页面停止响应的时间。

问题产生原因


DOM节点数量过多,浏览器渲染吃力

(图片引用自https://zhuanlan.zhihu.com/p/26022258)

其实不光是初次渲染时间长,如果有大量节点出现,那么在滚动的时候,也能明显感受到不流畅的滚动现象

可选方案


懒加载

通过懒加载的方式,在出现长列表的时候,第一次并不完全渲染所有的DOM节点,即可解决一部分场景下的问题。

优点:实现简单

缺点:

  1. 想要定位到某一个位置的数据会非常困难

  2. 如果一直加载到底,那么最终还是会出现大量的DOM节点,导致滚动不流畅

虚拟渲染

懒加载无法满足真正的长列表展示,那么如果真正要解决此类问题该怎么办?还有一种思路就是:列表局部渲染,又被称为虚拟列表.

当前比较知名的一些第三方库有vue-virtual-scroller、react-tiny-virtual-list、react-virtualized。它们都可以利用局部加载解决列表过长的问题的,vue-virtual-scroller、react-tiny-virtual-list一类的方案只支持虚拟列表,而react-virtualized这种大而全的库则是支持表格、集合、列表等多种情况下的局部加载方案。

单纯列表虚拟渲染

我们先看下vue-virtual-scroller、react-tiny-virtual-list这种纯虚拟列表的解决方案。它们的实现原理是利用视差和错觉制作一份出一份“虚拟”列表,一个虚拟列表由三部分组成:

  1. 视窗口

  2. 虚拟数据列表(数据展示)

  3. 滚动占位区块(底部滚动区)

虚拟列表侧面图示意:

正面图:

滚动一段距离后:

最终要实现的效果:由滚动占位区块产生滚动条,随着滚动条的移动,在可视窗口展示虚拟数据列表

react-virtualized的二维虚拟渲染

react-virtualized的实现方案和我们上面探讨的不太一样,因为表格是二维的,而列表是一维的(可以认为列表是一种特殊的表格),react-virtualized就是在二维的基础上构建的一套虚拟数据渲染工具。

示意图如下:

蓝色的部分被称为Cell,上面白色线分隔的区块叫做Section。

基本原理:在列表的上方打上一层方格(Section),下面的每个元素(Cell)都能落到某个方格上(Section)。滚动的时候,随着Cell的动态增加,Section也会被动态的创建,将每一个Cell都注册到对应的Section下。根据当前滚动到的Section,可以得到当前Section下包含的Cell,从此将Cell渲染出来。

/*  0 1 2 3 4 5 ┏━━━┯━━━┯━━━┓0┃0 0┊1 3┊6 6┃1┃0 0┊2 3┊6 6┃ ┠┈┈┈┼┈┈┈┼┈┈┈┨2┃4 4┊4 3┊7 8┃3┃4 4┊4 5┊9 9┃ ┗━━━┷━━━┷━━━┛Sections to Cells map: 0.0 [0] 1.0 [1, 2, 3] 2.0 [6] 0.1 [4] 1.1 [3, 4, 5] 2.1 [7, 8, 9]*/

实现方案


由于我们的目的是处理前端超长列表,而react-virtualized的实现方案是基于二维表格的,其List组件也是继承自Grid组件,如果要做列表方案,必须先实现二维的Grid方案。只处理长列表的情况下,实现一个单纯的虚拟列表渲染方案比二维的Grid方案要更合适一些。

基本结构

首先我们按照虚拟列表示意图来规划出若干个元素。.virtual-scroller乃整个滚动列表组件,在最外层监测其滚动事件。在内部我们需放置一个.phantom来撑开容器,使滚动条出现,并且该元素的高度 = 数据总数 * 列表项高度。接着我们在.phantom的上一层,再画出一个ul列表,它被用来动态加载数据,而它的位置和数据将由计算得出。

https://codesandbox.io/s/list--scrollbar-basic-bbxlq

<template>  <div id="app">    <input type="text" v-model.number="dataLength">条    <div class="virtual-scroller" @scroll="onScroll" :style="{height: 600 + 'px'}">      <div class="phantom" :style="{height: this.dataLength * itemHeight + 'px'}">        <ul :style="{'margin-top': `${scrollTop}px`}">          <li v-for="item in visibleList" :key="item.brandId" :style="{height: `${itemHeight}px`, 'line-height': `${itemHeight}px`}">            <div>              <div>{{item.name}}div>            div>          li>        ul>      div>    div>  div>template><script>export default {  name: "App",  data() {    return {      itemHeight: 60,      visibleCount: 10,      dataLength: 100,      startIndex: 0,      endIndex: 10,      scrollTop: 0    };  },  computed: {    dataList() {      const newDataList = [...Array(this.dataLength || 0).keys()].map((v, i) => ({        brandId: i + 1,        name: `第${i + 1}项`,        height: this.itemHeight      }));      return newDataList;    },    visibleList() {      return this.dataList.slice(this.startIndex, this.endIndex);    }  },  watch: {    dataList() {      console.time('rerender');      setTimeout(() => {        console.timeEnd('rerender');      }, 0)    }  },  methods: {    onScroll(e) {          }  }};script><style lang="stylus" scoped>#app {  font-family: 'Avenir', Helvetica, Arial, sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  text-align: center;  color: #2c3e50;}.virtual-scroller {  border: solid 1px #eee;  margin-top: 10px;  height 600px  overflow auto}.phantom {  overflow hidden}ul {  background: #ccc;  list-style: none;  padding: 0;  margin: 0;  li {    outline: solid 1px #fff;  }}style>

Make it scroll

上一例中,onScroll函数并没有填写,也就是说虚拟列表中的数据及位置并不会随着我们滚动而更新。这一步,补全onScroll函数。

https://codesandbox.io/s/list--scrollbar-easy-i7ok7

<template>  <div id="app">    <input type="text" v-model.number="dataLength">条    <div class="virtual-scroller" @scroll="onScroll" :style="{height: 600 + 'px'}">      <div class="phantom" :style="{height: this.dataLength * itemHeight + 'px'}">        <ul :style="{'margin-top': `${scrollTop}px`}">          <li v-for="item in visibleList" :key="item.brandId" :style="{height: `${itemHeight}px`, 'line-height': `${itemHeight}px`}">            <div>              <div>{{item.name}}div>            div>          li>        ul>      div>    div>  div>template><script>export default {  name: "App",  data() {    return {      itemHeight: 60,      visibleCount: 10,      dataLength: 100,      startIndex: 0,      endIndex: 10,      scrollTop: 0    };  },  computed: {    dataList() {      const newDataList = [...Array(this.dataLength || 0).keys()].map((v, i) => ({        brandId: i + 1,        name: `第${i + 1}项`,        height: this.itemHeight      }));      return newDataList;    },    visibleList() {      return this.dataList.slice(this.startIndex, this.endIndex);    }  },  watch: {    dataList() {      console.time('rerender');      setTimeout(() => {        console.timeEnd('rerender');      }, 0)    }  },  methods: {    onScroll(e) {      const scrollTop = e.target.scrollTop;      this.scrollTop = scrollTop;      console.log('scrollTop', scrollTop);      this.startIndex = Math.floor(scrollTop / this.itemHeight);      this.endIndex = this.startIndex + 10;    }  }};script><style lang="stylus" scoped>#app {  font-family: 'Avenir', Helvetica, Arial, sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  text-align: center;  color: #2c3e50;}.virtual-scroller {  border: solid 1px #eee;  margin-top: 10px;  height 600px  overflow auto}.phantom {  overflow hidden}ul {  background: #ccc;  list-style: none;  padding: 0;  margin: 0;  li {    outline: solid 1px #fff;  }}style>

解决滚动不连贯的问题

上一例中我们滚动时,会发现一定要滚动一段距离后,虚拟列表中的内容才会突然更新一下,而不是循序渐进的过程。

这是因为startIndex由scrollTop/itemHeight计算出来,只能是item高度的倍数,假设scrollTop值在1倍和2倍之间的时候,虚拟列表内的startIndex并不会更新,也不会产生滚动现象

那么如何解决呢?其实我们利用ul元素自身的滚动来“欺骗眼睛”,原理如下图所示:

只需将我们的onScroll函数调整一下。ul的margin-top由计算得出,而不是直接使用e.target.scrollTop。

onScroll(e) {  const scrollTop = e.target.scrollTop;  this.startIndex = Math.floor(scrollTop / this.itemHeight);  this.endIndex = this.startIndex + 10;  this.scrollTop = this.startIndex * this.itemHeight;}

减少reflow

由于我们每滚动一次,就需要改变一次margin-top,可能会频发引发reflow,那么我们可以考虑降低margin-top改变的频率

onScroll(e) {  const scrollTop = e.target.scrollTop;  const startIndex = Math.floor(scrollTop / this.itemHeight);  let endIndex = startIndex + 10;  if (endIndex > this.dataList.length) {    endIndex = this.dataList.length;  }  // 当前滚动高度  const currentScrollTop = startIndex * this.itemHeight;  // 如果往下滚了可视区域的一部分,或者往上滚任意距离  if (currentScrollTop - this.scrollTop > this.itemHeight * (this.visibleCount - 1) || currentScrollTop - this.scrollTop < 0) {    this.scrollTop = currentScrollTop;    this.startIndex = startIndex;    this.endIndex = endIndex;  }}

列表项高度不固定,但可在渲染前获得高度的

上面处理的基本是写死高度的情况,如果是由数据中获取高度的,需要如下改写。

https://codesandbox.io/s/list--scrollbar-diff-height-7yb8m

<template>  <div id="app">    <input type="text" v-model.number="dataLength">条{{this.scrollBarHeight}}    <div class="virtual-scroller" @scroll="onScroll" :style="{height: 600 + 'px'}">      <div class="phantom" :style="{height: this.scrollBarHeight + 'px'}">        <ul :style="{'margin-top': `${scrollTop}px`}">          <li v-for="item in visibleList" :key="item.brandId" :style="{height: `${item.height}px`, 'line-height': `${item.height}px`}">            <div>              <div>{{item.name}}div>            div>          li>        ul>      div>    div>  div>template><script>export default {  name: "App",  data() {    return {      visibleCount: 10,      dataLength: 2000,      startIndex: 0,      endIndex: 10,      scrollTop: 0,      bufferItemCount: 4,      dataList: []    };  },  computed: {    visibleList() {      return this.dataList.slice(this.startIndex, this.endIndex + this.bufferItemCount);    },    scrollBarHeight() {      return this.dataList.reduce((pre, current)=> {        console.log(pre, current)        return pre + current.height;      }, 0);    }  },  watch: {    dataList() {      console.time('rerender');      setTimeout(() => {        console.timeEnd('rerender');      }, 0)    }  },  mounted() {    this.dataList = this.getDataList();  },  methods: {    getDataList() {      const newDataList = [...Array(this.dataLength || 0).keys()].map((v, i) => ({        brandId: i + 1,        name: `第${i + 1}项`,        height: Math.floor(Math.max(Math.random() * 10, 5)) * 10      }));      return newDataList;    },    getScrollTop(startIndex) {      return this.dataList.slice(0, startIndex).reduce((pre, current) => {        return pre + current.height;      }, 0)    },    getStartIndex(scrollTop) {      let index = 0;      let heightAccumulate = 0;      for (let i = 0; i < this.dataList.length; i++) {        if (heightAccumulate > scrollTop) {          index = i - 1;          return index;        }        if (heightAccumulate === scrollTop) {          index = i;          return i        }        heightAccumulate += this.dataList[i].height;      }      return index;    },    onScroll(e) {      const scrollTop = e.target.scrollTop;      this.startIndex = this.getStartIndex(scrollTop);      this.endIndex = this.startIndex + 10;      this.scrollTop = this.getScrollTop(this.startIndex);    }  }};script><style lang="stylus" scoped>#app {  font-family: 'Avenir', Helvetica, Arial, sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  text-align: center;  color: #2c3e50;}.virtual-scroller {  border: solid 1px #eee;  margin-top: 10px;  height 600px  overflow auto}.phantom {  overflow hidden}ul {  background: #ccc;  list-style: none;  padding: 0;  margin: 0;  li {    outline: solid 1px #fff;  }}style>

缓存每个元素的scrollTop

上一个例子中,我们每次getScrollTop都需要重新计算一次,比较浪费性能。可以在一开始的时候加上缓存,这样每次调用时直接从map中取,耗时较小。

https://codesandbox.io/s/list--scrollbar-diff-height-opt1-n2gvs

generatePositionCache() {  const allHeight = this.dataList.reduce((pre, current, i) => {    const heightSum = pre + current.height;    this.positionCache[i] = pre;    return heightSum;  }, 0)  this.scrollBarHeight = allHeight}

二分查找减少startIndex的查找时间

另外,还可以利用二分查找来降低getStartIndex的时间

getStartIndex(scrollTop) {  // 在itemTopCache中找到一个左侧小于scrollTop但右侧大于scrollTop的位置  // 复杂度O(n)  // for (let i = 0; i < this.itemTopCache.length; i++) {  //   if (this.itemTopCache[i] > scrollTop) {  //     return i - 1;  //   }  // }  // 复杂度O(logn)  let arr = this.itemTopCache;  let index = -1;  let left = 0,      right = arr.length - 1,      mid = Math.floor((left + right) / 2);  let circleTimes = 0;  while (right - left > 1) {    // console.log('index: ', left, right);    // console.log('height: ', arr[left], arr[right]);    circleTimes++;    // console.log('circleTimes:', circleTimes)    // 目标数在左侧    if (scrollTop < arr[mid]) {      right = mid;      mid = Math.floor((left + right) / 2);    } else if (scrollTop > arr[mid]) {      // 目标数在右侧      left = mid;      mid = Math.floor((left + right) / 2);    } else {      index = mid;      return index;    }  }  index = left;  return index;}

解决CSS索引问题

正常的列表结构是从第0个元素开始的,我们在CSS中通过选择器2n可以选中偶数行的列表。但虚拟列表不同,我们每次计算出来的startIndex都不同,startIndex为奇数时,2n便表现异常,所以我们需要保证startIndex为一个偶数。解决方法也很简单,如果发现是奇数,则取上一位,确保startIndex一定是偶数。

https://codesandbox.io/s/list--scrollbar-diff-height-opt3-9pb9x

ul {  background: #ccc;  list-style: none;  padding: 0;  margin: 0;  li {    outline: solid 1px #fff;    &:nth-child(2n) {      background: #fff;    }  }}...// onScroll中加入// 如果是奇数开始,就取其前一位偶数if (startIndex % 2 !== 0) {  this.startIndex = startIndex - 1;} else {  this.startIndex = startIndex;}

渲染后才可确定高度的

有种情况是每个列表项中包含的文字数量不同,导致渲染后撑开的高度不一样。那么我们就可以在组件mounted后更新一次列表项的高度。

https://codesandbox.io/s/list--scrollbar-diff-height-opt4-sholq

Item.vue

<template>  <li :key="item.brandId" :style="{height: `${item.height}px`, 'line-height': `${item.height}px`}" ref="node">    <div>      <div>{{item.name}}div>    div>  li>template><script>export default {  props: {    item: {      default() {        return {}      },      type: Object    },    index: Number  },  data() {    return {          }  },  mounted() {    this.$emit('update-height', {height: this.$refs.node.getBoundingClientRect().height, index: this.index})  }}script>

Item组件加载时会更新高度,但是整个列表初始化时是没有高度的怎么办?我们需要引入一个估算值:estimatedItemHeight,它代表每个Item的预估高度,每当Item有更新时,则替换掉预估值,同时更新列表的整体高度。

App.vue

  
"app"> "text" v-model.number="dataLength">条 Height:{{scrollBarHeight}}

class="virtual-scroller" @scroll="onScroll" :style="{height: 600 + 'px'}">

class="phantom" :style="{height: scrollBarHeight + 'px'}"> "{'transform': `translate3d(0,${scrollTop}px,0)`}"> for="item in visibleList" :item="item" :index="item.index" :key="item.brandId" @update-height="updateItemHeight"/>

import Item from './components/Item.vue';export default { name: "App", components: { Item }, data() { return { estimatedItemHeight: 30, visibleCount: 10, dataLength: 200, startIndex: 0, endIndex: 10, scrollTop: 0, scrollBarHeight: 0, bufferItemCount: 4, dataList: [], itemHeightCache: [], itemTopCache: [] }; }, computed: { visibleList() { return this.dataList.slice(this.startIndex, this.endIndex + this.bufferItemCount); } }, watch: { dataList() { console.time('rerender'); setTimeout(() => { console.timeEnd('rerender'); }, 0) } }, created() { this.dataList = this.getDataList(); this.generateEstimatedItemData(); }, mounted() { }, methods: { generateEstimatedItemData() { const estimatedTotalHeight = this.dataList.reduce((pre, current, index)=> { this.itemHeightCache[index] = this.estimatedItemHeight; const currentHeight = this.estimatedItemHeight; this.itemTopCache[index] = index === 0 ? 0 : this.itemTopCache[index - 1] + this.estimatedItemHeight; return pre + currentHeight }, 0); this.scrollBarHeight = estimatedTotalHeight; }, updateItemHeight({index, height}) { this.itemHeightCache[index] = height; this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => { return pre + current; }, 0) let newItemTopCache = [0]; for (let i = 1, l = this.itemHeightCache.length; i < l; i++) { newItemTopCache[i] = this.itemTopCache[i - 1] + this.itemHeightCache[i - 1] }; this.itemTopCache = newItemTopCache; }, getDataList() { const newDataList = [...Array(this.dataLength || 0).keys()].map((v, i) => ({ index: i, brandId: i + 1, name: `第${i + 1}项`, height: Math.floor(Math.max(Math.random() * 10, 5)) * 10 // height: 50 })); return newDataList; }, getStartIndex(scrollTop) { // 在heightAccumulateCache中找到一个左侧小于scrollTop但右侧大于scrollTop的位置 // 复杂度O(n) // for (let i = 0; i < this.itemTopCache.length; i++) { // if (this.itemTopCache[i] > scrollTop) { // return i - 1; // } // } // 复杂度O(logn) let arr = this.itemTopCache; let index = -1; let left = 0, right = arr.length - 1, mid = Math.floor((left + right) / 2); let circleTimes = 0; while (right - left > 1) { circleTimes++; // 目标数在左侧 if (scrollTop < arr[mid]) { right = mid; mid = Math.floor((left + right) / 2); } else if (scrollTop > arr[mid]) { // 目标数在右侧 left = mid; mid = Math.floor((left + right) / 2); } else { index = mid; return index; } } index = left; return index; }, onScroll(e) { const scrollTop = e.target.scrollTop; console.log('scrollTop', scrollTop); let startIndex = this.getStartIndex(scrollTop); // 如果是奇数开始,就取其前一位偶数 if (startIndex % 2 !== 0) { this.startIndex = startIndex - 1; } else { this.startIndex = startIndex; } this.endIndex = this.startIndex + this.visibleCount; this.scrollTop = this.itemTopCache[this.startIndex] || 0; } }}; scoped>#app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50;}
.virtual-scroller { border: solid 1px #eee; margin-top: 10px; height 600px overflow auto}
.phantom { overflow hidden}
ul { background: #ccc; list-style: none; padding: 0; margin: 0; li { outline: solid 1px #fff; &:nth-child(2n) { background: #fff; } }}

假如列表项中包含了img标签,并会被img自动撑开,那么我们可以利用img的onload事件来通知列表更新高度。react-virtualized中也有img配合CellMeasure组件使用的例子。那么如果遇到更复杂的高度变化场景该怎么办?

ResizeObserver

ResizeObserver 接口可以监听到 Element 的内容区域或 SVGElement的边界框改变,可以处理复杂的高度变化场景。

但ResizeObserver 兼容性较为一般: https://caniuse.com/#feat=resizeobserver

虽然兼容性不太好,但在某些后台系统中,还是可以尝试使用的。

ResizeObserve使用例子

https://codesandbox.io/s/list--scrollbar-diff-height-opt5-89ckw

https://codesandbox.io/s/list--scrollbar-diff-height-opt6-nhcm0

主要调整点就是在list item中增加observe和unobserve方法。

Item.vue

<template>  <li :key="item.brandId" :style="{height: `${item.height}px`, 'line-height': `${item.height}px`}" ref="node">    <div>      <div>{{item.name}}div>    div>  li>template><script>export default {  props: {    item: {      default() {        return {}      },      type: Object    },    index: Number  },  data() {    return {}  },  mounted() {    this.observe();  },  methods: {    observe() {      this.resizeObserver = new ResizeObserver((entries) => {        const entry = entries[0];        console.log(this.index, entry.contentRect.height)        this.$emit('update-height', {height: entry.contentRect.height, index: this.index})      });      this.resizeObserver.observe(this.$refs.node);    },    unobserve() {      this.resizeObserver.unobserve(this.$refs.node);    }  },  beforeDestroy() {    this.unobserve();  }}script>

使用resize dectect库监测高度

对于高度变化场景且兼容性要求较高的,我们可以使用它的polyfill:ResizeObserver Polyfill,支持到IE8以上。另外应注意到它的一些限制:

  • Notifications are delivered ~20ms after actual changes happen.

  • Changes caused by dynamic pseudo-classes, e.g. :hover and :focus, are not tracked. As a workaround you could add a short transition which would trigger the transitionend event when an element receives one of the former classes (example).

  • Delayed transitions will receive only one notification with the latest dimensions of an element.

如果在没有原生ResizeObserver的情况下想实现:hover及:focus后的size更新观察,那么就要使用element-resize-detector、javascript-detect-element-resize(react-virtualized使用)这一类的第三方库了,当然它们也有一些限制,可以在observation-strategy中详细查阅到。

总结


解决了上述的一系列问题,我们才算实现了一个较为基础的虚拟列表。一些兼容性问题的修复和性能的优化,需要根据实际情况来看。在生产环境中,建议直接使用成熟的第三方库,在兼容性和性能方面有保证。如果时间充裕,可以造个轮子理解下思路,这样在使用第三方组件时也会更加得心应手。

参考文章:

https://github.com/dwqs/blog/issues/70

https://yuque.antfin-inc.com/abraham.cj/aay1e0/gtmmim

https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver

https://zhuanlan.zhihu.com/p/26022258

https://zhuanlan.zhihu.com/p/34585166

https://ant.design/components/list-cn/

https://github.com/que-etc/resize-observer-polyfill/blob/master/README.md

写在最后

方凳雅集是由阿里巴巴B系6大BU(1688,ICBU,零售通,AE,企业金融,考拉)共同维护的公众号奥,我们会定期发送优质好文,欢迎扫码关注

求关注

求转发

vue商品列表滚动效果_如何处理前端超长列表相关推荐

  1. JS实现仿新浪微博大厅和腾讯微博首页滚动效果_前端开发

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/ ...

  2. vue广告栏上下滚动效果

    vue广告栏上下滚动效果 html部分 <div class="roll"><img src="xxx.jpg" alt /><u ...

  3. better-scroll实现菜单和列表滚动效果联动。

    上一篇中,我们使用better-scroll实现了列表滚动效果,但是并没有与菜单栏关联起来,这里我们就实现左右联动功能. 解决思路:右侧滚动的时候,可以通过获取右侧列表栏实时的Y轴坐标,然后判断Y轴坐 ...

  4. vue实现数字滚动效果

    vue实现数字滚动效果 npm 下载 vue-count-to https://npmmirror.com/package/vue-count-to

  5. vue做数字滚动效果

    vue实现数字滚动效果 近期在做项目的时候,产品要求实现数字滚动效果如下: 用jquery实现 html: <div class="develop"> <!--滚 ...

  6. 置顶带滚动效果_前端面试:如何实现轮播图效果?

    本文将实现如上所示的轮播图.源代码 (https://github.com/z1ming/AKJS/tree/master/%E8%BD%AE%E6%92%AD%E5%9B%BE%E6%95%88%E ...

  7. vue实现卡片滚动效果

    vue实现卡片滚动左右切换效果 HTML: //最外层盒子 <div class="box_1">//内层盒子<div class="box_2&quo ...

  8. python3 列表取交集_常用序列数据类型列表

    python中有列表.元组.集合.字典这四种可以存放多个数据元素的集合,他们在总体功能上都起着存放数据的作用,却都有着各自的特点.本片文章中我们会对列表的用法做详细说明. 演示环境: python3. ...

  9. python 随机获取列表的元素_练习 34 - 获取列表元素 - Learn Python 3 The Hard Way

    练习 34 获取列表元素 列表(list)真的非常有用,前提是你要能获取到它们里面的内容.你已经能够按顺序遍历列表中的元素,但是如果你要取其中的第5个元素,你该怎么操操做?你需要知道如何获取一个列表里 ...

最新文章

  1. redux-thunk使用教程
  2. Office Live for Small Business--开启您创业的大门
  3. 【每日一算法】最长公共前缀
  4. 2020人工神经网络第一次作业-解答第一部分
  5. java 学习 --------接口概念
  6. python和对象复习_面向对象阶段复习
  7. 软件工程导论第六周作业:关于servlet,jquery,ExtJs,Spket
  8. 在DOS环境下编译及运行java程序教程
  9. 大型互联网架构演变历程-《淘宝技术这10年》
  10. SQLAlchemy schema.Table
  11. C#订阅与发布标准实现
  12. 路由器的修改权限密码、还原出厂设置、备份配置文件和升级操作系统实际操作...
  13. HDU4548 美素数【水题】
  14. 解决办法:Eclipse卡死在“revert resources”,进度一直为0
  15. Package inputenc Error: Invalid UTF-8 byte “A1;Improper alphabetic constant. <to be read again>
  16. uniapp遮罩_uni-app实现弹窗遮罩
  17. catflag Crypto KeyBoard
  18. 计算机与地球科学,地球科学与遥感
  19. word统计纯汉字字数
  20. 用WPF做一个简易浏览器

热门文章

  1. UINavigationController 返回到各级目录
  2. Symantec(VeriSign)SSL证书
  3. Java开发中业务层入参校验详细解析
  4. spring+kafka消费者的2种配置方式
  5. 内核compiler.h的学习
  6. Java利用Qrcode生成二维码
  7. MySQL 里设置或修改系统变量的几种方法,这个写的非常清晰
  8. python 目录遍历 目录文件列表 介绍
  9. 嵌入式http服务器boa 简介
  10. linux 软件安装 NOKEY问题