Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | 7x 7x 7x | <template> <component :is="tagName" :class="$style.root" @scroll="onScroll" v-on="$listeners"> <div ref="virtual" :class="$style.virtual" :style="{ paddingTop: virtualTop + 'px', paddingBottom: virtualBottom + 'px' }"> <f-render :vnode="virtualSlot"></f-render> </div> </component> </template> <script> import throttle from 'lodash/throttle'; export default { name: 'f-virtual-list', props: { list: Array, itemHeight: Number, virtual: { type: Boolean, default: true }, virtualCount: { type: Number, default: 60 }, throttle: { type: Number, default: 60 }, tagName: { type: String, default: 'div' }, listKey: { type: String, default: 'list' }, }, data() { return { virtualIndex: 0, virtualTop: 0, virtualBottom: 0 }; }, computed: { virtualList() { const list = this[this.listKey]; if (!this.virtual) return list; else return ( list && list.slice( this.virtualIndex, this.virtualIndex + this.virtualCount, ) ); }, virtualSlot() { // 给该 computed 添加一个依赖 list // eslint-disable-next-line no-unused-vars const list = this[this.listKey]; if (!this.virtual) return this.$slots.default; else return ( this.$slots.default && this.$slots.default.slice( this.virtualIndex, this.virtualIndex + this.virtualCount, ) ); }, }, created() { this.throttledVirtualScroll = throttle( this.handleVirtualScroll, this.throttle, { leading: true, trailing: true }, ); }, methods: { /** * 监听列表容器的滚动事件 * 一般用于重写 * @override * @param {*} e - 滚动事件对象 */ onScroll(e) { if (!this.virtual) return; this.throttledVirtualScroll(e); this.$emit('scroll', e, this); }, handleVirtualScroll(e) { if (!this.virtual) return; const listEl = e.target; const virtualEl = this.$refs.virtual; const list = this[this.listKey]; if (!virtualEl || !list) return; // 缓存当前可见 DOM 节点的高度 if (this.itemHeight === undefined) { const children = Array.from(virtualEl.children); children.forEach((childEl, index) => { const item = list[this.virtualIndex + index]; if ( item && item.height === undefined && item._cacheHeight === undefined ) item._cacheHeight = item.height || childEl.offsetHeight; }); } const getHeight = (item) => { if (this.itemHeight !== undefined) return this.itemHeight; else if (item.height !== undefined) return item.height; else if (item._cacheHeight !== undefined) return item._cacheHeight; else return 0; }; const scrollTop = listEl.scrollTop; let accHeight = 0; let virtualIndex = this.virtualIndex; let currentIndex = 0; for (currentIndex = 0; currentIndex < list.length; currentIndex++) { const item = list[currentIndex]; accHeight += getHeight(item); if (accHeight > scrollTop) break; } virtualIndex = Math.max( 0, currentIndex - Math.floor(this.virtualCount / 2), ); // eslint-disable-next-line yoda // 该方法容易出现白屏。有截流了问题不大。 if ( this.virtualCount / 3 <= currentIndex - this.virtualIndex && currentIndex - this.virtualIndex < (this.virtualCount * 2) / 3 ) return; let virtualTop = 0; let virtualBottom = 0; for (let i = 0; i < list.length; i++) { const item = list[i]; if (i < virtualIndex) { virtualTop += getHeight(item); } else if (i >= virtualIndex + this.virtualCount) { virtualBottom += getHeight(item); } } this.virtualIndex = virtualIndex; this.virtualTop = virtualTop; this.virtualBottom = virtualBottom; // Vue 应该是对渲染做了优化,为了减少在高频滚动时出现白屏的问题,需要强制更新 this.$nextTick(() => { this.$forceUpdate(); this.$emit( 'virtual-scroll', { virtualIndex, virtualCount: this.virtualCount, virtualTop, virtualBottom, }, this, ); }); }, }, }; </script> <style module> .root { overflow: auto; } </style> |