|
43 | 43 | BEHIND: 'BEHIND' // scroll down or right. |
44 | 44 |
|
45 | 45 | }; |
46 | | - var SIZE_TYPE = { |
| 46 | + var CALC_TYPE = { |
47 | 47 | INIT: 'INIT', |
48 | 48 | FIXED: 'FIXED', |
49 | 49 | DYNAMIC: 'DYNAMIC' |
50 | 50 | }; |
| 51 | + var LEADING_BUFFER = 1; |
51 | 52 |
|
52 | 53 | var Virtual = /*#__PURE__*/function () { |
53 | 54 | function Virtual(param, updateHook) { |
|
64 | 65 | this.updateHook = updateHook; // size data. |
65 | 66 |
|
66 | 67 | this.sizes = new Map(); |
67 | | - this.caches = new Map(); |
68 | 68 | this.firstRangeTotalSize = 0; |
69 | 69 | this.firstRangeAverageSize = 0; |
70 | | - this.lastCalculatedIndex = 0; |
71 | | - this.sizeType = SIZE_TYPE.INIT; |
72 | | - this.sizeTypeValue = 0; // scroll data. |
| 70 | + this.lastCalcIndex = 0; |
| 71 | + this.fixedSizeValue = 0; |
| 72 | + this.calcType = CALC_TYPE.INIT; // scroll data. |
73 | 73 |
|
74 | 74 | this.offset = 0; |
75 | 75 | this.direction = ''; // range data. |
|
79 | 79 | if (this.param && !this.param.disabled) { |
80 | 80 | this.checkRange(0, param.keeps - 1); |
81 | 81 | } // benchmark test data. |
| 82 | + // this.__bsearchCalls = 0 |
| 83 | + // this.__getIndexOffsetCalls = 0 |
82 | 84 |
|
83 | | - |
84 | | - this.__bsearchCalls = 0; |
85 | | - this.__getIndexOffsetCalls = 0; |
86 | | - this.__getIndexOffsetCacheHits = 0; |
87 | 85 | } |
88 | 86 | }, { |
89 | 87 | key: "destroy", |
|
100 | 98 | range.padFront = this.range.padFront; |
101 | 99 | range.padBehind = this.range.padBehind; |
102 | 100 | return range; |
| 101 | + } |
| 102 | + }, { |
| 103 | + key: "isLower", |
| 104 | + value: function isLower() { |
| 105 | + return this.direction === DIRECTION_TYPE.BEHIND; |
| 106 | + } |
| 107 | + }, { |
| 108 | + key: "isUpper", |
| 109 | + value: function isUpper() { |
| 110 | + return this.direction === DIRECTION_TYPE.FRONT; |
103 | 111 | } // return start index offset. |
104 | 112 |
|
105 | 113 | }, { |
|
122 | 130 | // if there is no size value different from this at next comming saving |
123 | 131 | // we think it's a fixed size list, otherwise is dynamic size list. |
124 | 132 |
|
125 | | - if (this.sizeType === SIZE_TYPE.INIT) { |
126 | | - this.sizeTypeValue = size; |
127 | | - this.sizeType = SIZE_TYPE.FIXED; |
128 | | - } else if (this.sizeType === SIZE_TYPE.FIXED && this.sizeTypeValue !== size) { |
129 | | - this.sizeType = SIZE_TYPE.DYNAMIC; // it's no use at all. |
| 133 | + if (this.calcType === CALC_TYPE.INIT) { |
| 134 | + this.fixedSizeValue = size; |
| 135 | + this.calcType = CALC_TYPE.FIXED; |
| 136 | + } else if (this.calcType === CALC_TYPE.FIXED && this.fixedSizeValue !== size) { |
| 137 | + this.calcType = CALC_TYPE.DYNAMIC; // it's no use at all. |
130 | 138 |
|
131 | | - delete this.sizeTypeValue; |
| 139 | + delete this.fixedSizeValue; |
132 | 140 | } // calculate the average size only in the first range. |
133 | 141 |
|
134 | 142 |
|
|
139 | 147 | // it's done using. |
140 | 148 | delete this.firstRangeTotalSize; |
141 | 149 | } |
142 | | - } // when dataSources length change, we need to force update |
143 | | - // just keep the same range and recalculate pad front and behind. |
| 150 | + } // in some special situation (e.g. length change) we need to update in a row |
| 151 | + // try goiong to render next range by a leading buffer according to current direction. |
144 | 152 |
|
145 | 153 | }, { |
146 | | - key: "handleDataSourcesLengthChange", |
147 | | - value: function handleDataSourcesLengthChange() { |
148 | | - this.updateRange(this.range.start, this.range.end); |
| 154 | + key: "handleDataSourcesChange", |
| 155 | + value: function handleDataSourcesChange() { |
| 156 | + var start = this.range.start; |
| 157 | + |
| 158 | + if (this.direction === DIRECTION_TYPE.FRONT) { |
| 159 | + start = start - LEADING_BUFFER; |
| 160 | + } else if (this.direction === DIRECTION_TYPE.BEHIND) { |
| 161 | + start = start + LEADING_BUFFER; |
| 162 | + } |
| 163 | + |
| 164 | + start = Math.max(start, 0); |
| 165 | + this.updateRange(start, this.getEndByStart(start)); |
149 | 166 | } // when slot size change, we also need force update. |
150 | 167 |
|
151 | 168 | }, { |
152 | 169 | key: "handleSlotSizeChange", |
153 | 170 | value: function handleSlotSizeChange() { |
154 | | - this.handleDataSourcesLengthChange(); |
| 171 | + this.handleDataSourcesChange(); |
155 | 172 | } // calculating range on scroll. |
156 | 173 |
|
157 | 174 | }, { |
|
171 | 188 | } |
172 | 189 | } // ----------- public method end. ----------- |
173 | 190 |
|
174 | | - }, { |
175 | | - key: "isFixedSize", |
176 | | - value: function isFixedSize() { |
177 | | - return this.sizeType === SIZE_TYPE.FIXED; |
178 | | - } |
179 | 191 | }, { |
180 | 192 | key: "handleFront", |
181 | 193 | value: function handleFront() { |
182 | 194 | var overs = this.getScrollOvers(); // should not change range if start doesn't exceed overs. |
183 | 195 |
|
184 | 196 | if (overs > this.range.start) { |
185 | 197 | return; |
186 | | - } // move up start by a buffer length. |
| 198 | + } // move up start by a buffer length, and make sure its safety. |
187 | 199 |
|
188 | 200 |
|
189 | 201 | var start = Math.max(overs - this.param.buffer, 0); |
|
212 | 224 | } // if this list is fixed size, that can be easily. |
213 | 225 |
|
214 | 226 |
|
215 | | - if (this.isFixedSize()) { |
216 | | - return Math.floor(offset / this.sizeTypeValue); |
| 227 | + if (this.isFixedType()) { |
| 228 | + return Math.floor(offset / this.fixedSizeValue); |
217 | 229 | } |
218 | 230 |
|
219 | 231 | var low = 0; |
|
222 | 234 | var high = this.param.uniqueIds.length; |
223 | 235 |
|
224 | 236 | while (low <= high) { |
| 237 | + // this.__bsearchCalls++ |
225 | 238 | middle = low + Math.floor((high - low) / 2); |
226 | 239 | middleOffset = this.getIndexOffset(middle); |
227 | | - this.__bsearchCalls++; |
228 | 240 |
|
229 | 241 | if (middleOffset === offset) { |
230 | 242 | return middle; |
|
236 | 248 | } |
237 | 249 |
|
238 | 250 | return low > 0 ? --low : 0; |
239 | | - } // return a scroll offset from given index. |
240 | | - // @todo can efficiency be improved more here? |
| 251 | + } // return a scroll offset from given index, can efficiency be improved more here? |
| 252 | + // although the call frequency is very high, its only a superposition of numbers. |
241 | 253 |
|
242 | 254 | }, { |
243 | 255 | key: "getIndexOffset", |
244 | 256 | value: function getIndexOffset(givenIndex) { |
245 | | - // we know this without calculate! |
| 257 | + // we know this. |
246 | 258 | if (!givenIndex) { |
247 | 259 | return 0; |
248 | | - } // get from cache if possible. |
249 | | - |
250 | | - |
251 | | - if (this.caches.has(givenIndex)) { |
252 | | - this.__getIndexOffsetCacheHits++; |
253 | | - return this.caches.get(givenIndex); |
254 | 260 | } |
255 | 261 |
|
256 | 262 | var offset = 0; |
257 | 263 | var indexSize = 0; |
258 | 264 |
|
259 | 265 | for (var index = 0; index <= givenIndex; index++) { |
260 | | - this.__getIndexOffsetCalls++; // cache last index offset if exist. |
261 | | - |
262 | | - if (index && indexSize) { |
263 | | - this.caches.set(index, offset); |
264 | | - } |
265 | | - |
| 266 | + // this.__getIndexOffsetCalls++ |
266 | 267 | indexSize = this.sizes.get(this.param.uniqueIds[index]); |
267 | 268 | offset = offset + (indexSize || this.getEstimateSize()); |
268 | 269 | } // remember last calculate index. |
269 | 270 |
|
270 | 271 |
|
271 | | - this.lastCalculatedIndex = Math.max(this.lastCalculatedIndex, givenIndex - 1); |
272 | | - this.lastCalculatedIndex = Math.min(this.lastCalculatedIndex, this.getLastIndex()); |
| 272 | + this.lastCalcIndex = Math.max(this.lastCalcIndex, givenIndex - 1); |
| 273 | + this.lastCalcIndex = Math.min(this.lastCalcIndex, this.getLastIndex()); |
273 | 274 | return offset; |
| 275 | + } |
| 276 | + }, { |
| 277 | + key: "isFixedType", |
| 278 | + value: function isFixedType() { |
| 279 | + return this.calcType === CALC_TYPE.FIXED; |
274 | 280 | } // return the real last index. |
275 | 281 |
|
276 | 282 | }, { |
|
323 | 329 | }, { |
324 | 330 | key: "getPadFront", |
325 | 331 | value: function getPadFront() { |
326 | | - if (this.isFixedSize()) { |
327 | | - return this.sizeTypeValue * this.range.start; |
| 332 | + if (this.isFixedType()) { |
| 333 | + return this.fixedSizeValue * this.range.start; |
328 | 334 | } else { |
329 | 335 | return this.getIndexOffset(this.range.start); |
330 | 336 | } |
|
337 | 343 | var end = this.range.end; |
338 | 344 | var lastIndex = this.getLastIndex(); |
339 | 345 |
|
340 | | - if (this.isFixedSize()) { |
341 | | - return (lastIndex - end) * this.sizeTypeValue; |
342 | | - } // if already calculate all, return the exactly padding. |
| 346 | + if (this.isFixedType()) { |
| 347 | + return (lastIndex - end) * this.fixedSizeValue; |
| 348 | + } // if calculated all already, return the exactly offset. |
343 | 349 |
|
344 | 350 |
|
345 | | - if (this.lastCalculatedIndex === lastIndex) { |
| 351 | + if (this.lastCalcIndex === lastIndex) { |
346 | 352 | return this.getIndexOffset(lastIndex) - this.getIndexOffset(end); |
347 | 353 | } else { |
348 | | - // if not, return a estimate padding. |
| 354 | + // if not, return a estimate offset. |
349 | 355 | return (lastIndex - end) * this.getEstimateSize(); |
350 | 356 | } |
351 | | - } // get estimate size for one item. |
| 357 | + } // get estimate size for one item, get from param.size at first range. |
352 | 358 |
|
353 | 359 | }, { |
354 | 360 | key: "getEstimateSize", |
|
400 | 406 | "default": 'vertical' // the other value is horizontal. |
401 | 407 |
|
402 | 408 | }, |
| 409 | + upperThreshold: { |
| 410 | + type: Number, |
| 411 | + "default": 0 |
| 412 | + }, |
| 413 | + lowerThreshold: { |
| 414 | + type: Number, |
| 415 | + "default": 0 |
| 416 | + }, |
403 | 417 | start: { |
404 | 418 | type: Number, |
405 | 419 | "default": 0 |
|
430 | 444 | }, |
431 | 445 | footerClass: { |
432 | 446 | type: String, |
433 | | - "default": 'div' |
| 447 | + "default": '' |
434 | 448 | }, |
435 | 449 | disabled: { |
436 | 450 | type: Boolean, |
|
511 | 525 | }, |
512 | 526 | // tell parent current size identify by unqiue key. |
513 | 527 | dispatchSizeChange: function dispatchSizeChange() { |
514 | | - this.$parent.$emit(this.event, this.uniqueKey, this.getCurrentSize()); |
| 528 | + this.$parent.$emit(this.event, this.uniqueKey, this.getCurrentSize(), this.hasInitial); |
515 | 529 | } |
516 | 530 | } |
517 | 531 | }; // wrapping for item. |
|
552 | 566 | // string value also use for aria role attribute. |
553 | 567 | FOOTER: 'footer' |
554 | 568 | }; |
555 | | - var VirtualList = Vue.component('virtual-list', { |
| 569 | + var NAME = 'virtual-list'; |
| 570 | + var VirtualList = Vue.component(NAME, { |
556 | 571 | props: VirtualProps, |
557 | 572 | data: function data() { |
558 | 573 | return { |
|
563 | 578 | dataSources: function dataSources(newValue, oldValue) { |
564 | 579 | if (newValue.length !== oldValue.length) { |
565 | 580 | this.virtual.updateParam('uniqueIds', this.getUniqueIdFromDataSources()); |
566 | | - this.virtual.handleDataSourcesLengthChange(); |
| 581 | + this.virtual.handleDataSourcesChange(); |
567 | 582 | } |
568 | 583 | } |
569 | 584 | }, |
|
581 | 596 | // recommend for a third of keeps. |
582 | 597 | uniqueIds: this.getUniqueIdFromDataSources() |
583 | 598 | }, this.onRangeChanged); // just for debug |
584 | | - |
585 | | - window.virtual = this.virtual; // also need sync initial range first. |
| 599 | + // window.virtual = this.virtual |
| 600 | + // also need sync initial range first. |
586 | 601 |
|
587 | 602 | this.range = this.virtual.getRange(); // listen item size changing. |
588 | 603 |
|
|
609 | 624 | this.virtual.saveSize(id, size); |
610 | 625 | }, |
611 | 626 | // event called when slot mounted or size changed. |
612 | | - onSlotResized: function onSlotResized(type, size) { |
| 627 | + onSlotResized: function onSlotResized(type, size, hasInit) { |
613 | 628 | if (type === SLOT_TYPE.HEADER) { |
614 | 629 | this.virtual.updateParam('slotHeaderSize', size); |
615 | 630 | } else if (type === SLOT_TYPE.FOOTER) { |
616 | 631 | this.virtual.updateParam('slotFooterSize', size); |
617 | 632 | } |
618 | 633 |
|
619 | | - this.virtual.handleSlotSizeChange(); |
| 634 | + if (hasInit) { |
| 635 | + this.virtual.handleSlotSizeChange(); |
| 636 | + } |
620 | 637 | }, |
621 | 638 | // here is the rerendering entry. |
622 | 639 | onRangeChanged: function onRangeChanged(range) { |
|
630 | 647 | } |
631 | 648 |
|
632 | 649 | var offset = root[this.directionKey]; |
633 | | - this.emitEvent(offset, evt); |
634 | 650 | this.virtual.handleScroll(offset); |
| 651 | + this.emitEvent(offset, evt); |
635 | 652 | }, |
636 | 653 | getUniqueIdFromDataSources: function getUniqueIdFromDataSources() { |
637 | 654 | var _this = this; |
|
653 | 670 | // ref element is definitely available here. |
654 | 671 | var root = this.$refs.root; |
655 | 672 | var range = this.virtual.getRange(); |
| 673 | + var isLower = this.virtual.isLower(); |
| 674 | + var isUpper = this.virtual.isUpper(); |
656 | 675 | var offsetShape = root[this.isHorizontal ? 'clientWidth' : 'clientHeight']; |
657 | | - var scrollShape = root[this.isHorizontal ? 'scrollWidth' : 'scrollHeight']; // only non-empty & offset === 0 calls totop. |
| 676 | + var scrollShape = root[this.isHorizontal ? 'scrollWidth' : 'scrollHeight']; |
658 | 677 |
|
659 | | - if (!!this.dataSources.length && !offset) { |
660 | | - this.$emit('totop', evt, range); |
661 | | - } else if (offset + offsetShape >= scrollShape) { |
662 | | - this.$emit('tobottom', evt, range); |
| 678 | + if (isUpper && !!this.dataSources.length && offset - this.upperThreshold <= 0) { |
| 679 | + this.$emit('toupper', evt, range); |
| 680 | + } else if (isLower && offset + offsetShape + this.lowerThreshold >= scrollShape) { |
| 681 | + this.$emit('tolower', evt, range); |
663 | 682 | } else { |
664 | | - this.$emit('onscroll', evt, range); |
| 683 | + this.$emit('scroll', evt, range); |
665 | 684 | } |
666 | 685 | }, |
667 | 686 | // get the real render slots based on range data. |
|
671 | 690 | var end = this.disabled ? this.dataSources.length - 1 : this.range.end; |
672 | 691 |
|
673 | 692 | for (var index = start; index <= end; index++) { |
674 | | - slots.push(h(Item, { |
675 | | - "class": this.itemClass, |
676 | | - props: { |
677 | | - tag: this.itemTag, |
678 | | - event: EVENT_TYPE.ITEM, |
679 | | - horizontal: this.isHorizontal, |
680 | | - uniqueKey: this.dataSources[index][this.dataKey], |
681 | | - source: this.dataSources[index], |
682 | | - component: this.dataComponent |
683 | | - } |
684 | | - })); |
| 693 | + var dataSource = this.dataSources[index]; |
| 694 | + |
| 695 | + if (dataSource) { |
| 696 | + slots.push(h(Item, { |
| 697 | + "class": this.itemClass, |
| 698 | + props: { |
| 699 | + tag: this.itemTag, |
| 700 | + event: EVENT_TYPE.ITEM, |
| 701 | + horizontal: this.isHorizontal, |
| 702 | + uniqueKey: dataSource[this.dataKey], |
| 703 | + source: dataSource, |
| 704 | + component: this.dataComponent |
| 705 | + } |
| 706 | + })); |
| 707 | + } else { |
| 708 | + console.warn("[".concat(NAME, "]: cannot get the index ").concat(index, " from data-sources.")); |
| 709 | + } |
685 | 710 | } |
686 | 711 |
|
687 | 712 | return slots; |
|
0 commit comments