@fesjs/fes-design
Version:
fes-design for PC
314 lines (283 loc) • 9.44 kB
JavaScript
import _defineProperty from '@babel/runtime/helpers/esm/defineProperty';
import { reactive } from 'vue';
var DIRECTION_TYPE = /*#__PURE__*/function (DIRECTION_TYPE) {
DIRECTION_TYPE["FRONT"] = "FRONT";
// scroll up or left
DIRECTION_TYPE["BEHIND"] = "BEHIND"; // scroll down or right
return DIRECTION_TYPE;
}(DIRECTION_TYPE || {});
var CALC_TYPE = /*#__PURE__*/function (CALC_TYPE) {
CALC_TYPE["INIT"] = "INIT";
CALC_TYPE["FIXED"] = "FIXED";
CALC_TYPE["DYNAMIC"] = "DYNAMIC";
return CALC_TYPE;
}(CALC_TYPE || {});
const LEADING_BUFFER = 0;
class Virtual {
constructor(param, callUpdate) {
_defineProperty(this, "_scrollRAF", null);
// 缓存滚动位置计算结果
_defineProperty(this, "_scrollPositionCache", new Map());
this.init(param, callUpdate);
}
init() {
let param = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
let callUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
this.param = param;
this.callUpdate = callUpdate;
// size data
this.sizes = reactive(new Map());
this.firstRangeTotalSize = 0;
this.firstRangeAverageSize = 0;
this.fixedSizeValue = 0;
this.calcType = CALC_TYPE.INIT;
// scroll data
this.offset = 0;
// range data
this.range = Object.create(null);
// 清空所有缓存
this._scrollPositionCache = new Map();
// 清除滚动动画帧引用
if (this._scrollRAF) {
cancelAnimationFrame(this._scrollRAF);
this._scrollRAF = null;
}
if (this.param && this.callUpdate) {
this.checkRange(0, this.param.keeps - 1);
}
}
destroy() {
// 重置内部状态数据
this.init();
}
// return current render range
getRange() {
const range = Object.create(null);
range.start = this.range.start;
range.end = this.range.end;
range.padFront = this.range.padFront;
range.padBehind = this.range.padBehind;
return range;
}
isBehind() {
return this.direction === DIRECTION_TYPE.BEHIND;
}
isFront() {
return this.direction === DIRECTION_TYPE.FRONT;
}
// return start index offset
getOffset(start) {
return (start < 1 ? 0 : this.getIndexOffset(start)) + this.param.slotHeaderSize;
}
updateParam(key, value) {
if (this.param && Object.keys(this.param).includes(key)) {
// if uniqueIds change, find out deleted id and remove from size map
if (key === 'uniqueIds') {
this.sizes.forEach((v, k) => {
if (!value.includes(k)) {
this.sizes.delete(k);
}
});
}
this.param[key] = value;
}
}
// save each size map by id
saveSize(id, size) {
this.sizes.set(id, size);
// we assume size type is fixed at the beginning and remember first size value
// if there is no size value different from this at next comming saving
// we think it's a fixed size list, otherwise is dynamic size list
if (this.calcType === CALC_TYPE.INIT) {
this.fixedSizeValue = size;
this.calcType = CALC_TYPE.FIXED;
} else if (this.calcType === CALC_TYPE.FIXED && this.fixedSizeValue !== size) {
this.calcType = CALC_TYPE.DYNAMIC;
// it's no use at all
delete this.fixedSizeValue;
}
// calculate the average size only in the first range
if (this.calcType !== CALC_TYPE.FIXED && typeof this.firstRangeTotalSize !== 'undefined') {
if (this.sizes.size < Math.min(this.param.keeps, this.param.uniqueIds.length)) {
this.firstRangeTotalSize = [...this.sizes.values()].reduce((acc, val) => acc + val, 0);
this.firstRangeAverageSize = Math.round(this.firstRangeTotalSize / this.sizes.size);
} else {
// it's done using
delete this.firstRangeTotalSize;
}
}
}
// in some special situation (e.g. length change) we need to update in a row
// try goiong to render next range by a leading buffer according to current direction
handleDataSourcesChange() {
let start = this.range.start;
if (this.isFront()) {
start = start - LEADING_BUFFER;
} else if (this.isBehind()) {
start = start + LEADING_BUFFER;
}
start = Math.max(start, 0);
this.updateRange(this.range.start, this.getEndByStart(start));
}
// when slot size change, we also need force update
handleSlotSizeChange() {
this.handleDataSourcesChange();
}
// calculating range on scroll
handleScroll(offset) {
this.direction = offset < this.offset ? DIRECTION_TYPE.FRONT : DIRECTION_TYPE.BEHIND;
this.offset = offset;
if (!this.param) {
return;
}
// 使用requestAnimationFrame优化滚动性能
if (this._scrollRAF) {
cancelAnimationFrame(this._scrollRAF);
}
this._scrollRAF = requestAnimationFrame(() => {
if (this.direction === DIRECTION_TYPE.FRONT) {
this.handleFront();
} else if (this.direction === DIRECTION_TYPE.BEHIND) {
this.handleBehind();
}
});
}
handleFront() {
const overs = this.getScrollOvers();
// should not change range if start doesn't exceed overs
if (overs > this.range.start) {
return;
}
// move up start by a buffer length, and make sure its safety
const start = Math.max(overs - this.param.buffer, 0);
this.checkRange(start, this.getEndByStart(start));
}
handleBehind() {
const overs = this.getScrollOvers();
// range should not change if scroll overs within buffer
if (overs < this.range.start + this.param.buffer) {
return;
}
this.checkRange(overs, this.getEndByStart(overs));
}
// return the pass overs according to current scroll offset
getScrollOvers() {
// if slot header exist, we need subtract its size
const offset = this.offset - this.param.slotHeaderSize;
if (offset <= 0) {
return 0;
}
// if is fixed type, that can be easily
if (this.isFixedType()) {
return Math.floor(offset / this.fixedSizeValue);
}
let low = 0;
let middle = 0;
let middleOffset = 0;
let high = this.param.uniqueIds.length;
while (low <= high) {
middle = low + Math.floor((high - low) / 2);
middleOffset = this.getIndexOffset(middle);
if (middleOffset === offset) {
return middle;
}
if (middleOffset < offset) {
low = middle + 1;
} else if (middleOffset > offset) {
high = middle - 1;
}
}
return low > 0 ? --low : 0;
}
// 滚动位置计算方法
getIndexOffset(givenIndex) {
if (!givenIndex) {
return 0;
}
// 检查缓存
const cachedOffset = this._scrollPositionCache.get(givenIndex);
if (cachedOffset !== undefined) {
return cachedOffset;
}
let offset = 0;
// 直接计算从0到目标索引的偏移量
for (let index = 0; index < givenIndex; index++) {
const indexSize = this.sizes.get(this.param.uniqueIds[index]);
offset += typeof indexSize === 'number' ? indexSize : this.getEstimateSize();
}
// 缓存结果
this._scrollPositionCache.set(givenIndex, offset);
// 如果缓存过大,清理一半的缓存
if (this._scrollPositionCache.size > 1000) {
const indices = Array.from(this._scrollPositionCache.keys());
indices.slice(0, indices.length / 2).forEach(index => {
this._scrollPositionCache.delete(index);
});
}
return offset;
}
// is fixed size type
isFixedType() {
return this.calcType === CALC_TYPE.FIXED;
}
// return the real last index
getLastIndex() {
return this.param.uniqueIds.length - 1;
}
// in some conditions range is broke, we need correct it
// and then decide whether need update to next range
checkRange(start, end) {
const keeps = this.param.keeps;
const total = this.param.uniqueIds.length;
// datas less than keeps, render all
if (total <= keeps) {
start = 0;
end = this.getLastIndex();
} else if (end - start < keeps - 1) {
// if range length is less than keeps, corrent it base on end
start = end - keeps + 1;
}
if (this.range.start !== start) {
this.updateRange(start, end);
}
}
// setting to a new range and rerender
updateRange(start, end) {
this.range.start = start;
this.range.end = end;
this.range.padFront = this.getPadFront();
this.range.padBehind = this.getPadBehind();
this.callUpdate(this.getRange());
}
// return end base on start
getEndByStart(start) {
const theoryEnd = start + this.param.keeps - 1;
const truelyEnd = Math.min(theoryEnd, this.getLastIndex());
return truelyEnd;
}
// return total front offset
getPadFront() {
if (this.isFixedType()) {
return this.fixedSizeValue * this.range.start;
}
return this.getIndexOffset(this.range.start);
}
// return total behind offset
getPadBehind() {
const end = this.range.end;
const lastIndex = this.getLastIndex();
if (this.isFixedType()) {
return (lastIndex - end) * this.fixedSizeValue;
}
// if not, use a estimated value
return (lastIndex - end) * this.getEstimateSize();
}
// get the item estimate size
getEstimateSize() {
return this.isFixedType() ? this.fixedSizeValue : this.firstRangeAverageSize || this.param.estimateSize;
}
getTotalSize() {
return [...this.sizes.values()].reduce((acc, val) => acc + val, 0);
}
}
export { Virtual as default };