You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
305 lines
8.1 KiB
305 lines
8.1 KiB
const DIRECTION_TYPE = {
|
|
FRONT: 'FRONT', // scroll up or left
|
|
BEHIND: 'BEHIND' // scroll down or right
|
|
}
|
|
const CALC_TYPE = {
|
|
INIT: 'INIT',
|
|
FIXED: 'FIXED',
|
|
DYNAMIC: 'DYNAMIC'
|
|
}
|
|
const LEADING_BUFFER = 2
|
|
|
|
export default class Virtual {
|
|
constructor(param, callUpdate) {
|
|
this.init(param, callUpdate)
|
|
}
|
|
|
|
init(param, callUpdate) {
|
|
// param data
|
|
this.param = param
|
|
this.callUpdate = callUpdate
|
|
|
|
// size data
|
|
this.sizes = new Map()
|
|
this.firstRangeTotalSize = 0
|
|
this.firstRangeAverageSize = 0
|
|
this.lastCalcIndex = 0
|
|
this.fixedSizeValue = 0
|
|
this.calcType = CALC_TYPE.INIT
|
|
|
|
// scroll data
|
|
this.offset = 0
|
|
this.direction = ''
|
|
|
|
// range data
|
|
this.range = Object.create(null)
|
|
if (param) {
|
|
this.checkRange(0, param.keeps - 1)
|
|
}
|
|
|
|
// benchmark test data
|
|
// this.__bsearchCalls = 0
|
|
// this.__getIndexOffsetCalls = 0
|
|
}
|
|
|
|
destroy() {
|
|
this.init(null, null)
|
|
}
|
|
|
|
// 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 && (key in this.param)) {
|
|
// if uniqueIds change, find out deleted id and remove from size map
|
|
if (key === 'uniqueIds') {
|
|
this.sizes.forEach((v, key) => {
|
|
if (!value.includes(key)) {
|
|
this.sizes.delete(key)
|
|
}
|
|
})
|
|
}
|
|
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 coming 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.firstRangeTotalSize + size
|
|
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 going 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.direction === DIRECTION_TYPE.FRONT) {
|
|
this.handleFront()
|
|
} else if (this.direction === DIRECTION_TYPE.BEHIND) {
|
|
this.handleBehind()
|
|
}
|
|
}
|
|
|
|
// ----------- public method end -----------
|
|
|
|
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) {
|
|
// this.__bsearchCalls++
|
|
middle = low + Math.floor((high - low) / 2)
|
|
middleOffset = this.getIndexOffset(middle)
|
|
|
|
if (middleOffset === offset) {
|
|
return middle
|
|
} else if (middleOffset < offset) {
|
|
low = middle + 1
|
|
} else if (middleOffset > offset) {
|
|
high = middle - 1
|
|
}
|
|
}
|
|
|
|
return low > 0 ? --low : 0
|
|
}
|
|
|
|
// return a scroll offset from given index, can efficiency be improved more here?
|
|
// although the call frequency is very high, its only a superposition of numbers
|
|
getIndexOffset(givenIndex) {
|
|
if (!givenIndex) {
|
|
return 0
|
|
}
|
|
|
|
let offset = 0
|
|
let indexSize = 0
|
|
for (let index = 0; index < givenIndex; index++) {
|
|
// this.__getIndexOffsetCalls++
|
|
indexSize = this.sizes.get(this.param.uniqueIds[index])
|
|
offset = offset + (typeof indexSize === 'number' ? indexSize : this.getEstimateSize())
|
|
}
|
|
|
|
// remember last calculate index
|
|
this.lastCalcIndex = Math.max(this.lastCalcIndex, givenIndex - 1)
|
|
this.lastCalcIndex = Math.min(this.lastCalcIndex, this.getLastIndex())
|
|
|
|
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, current it base on end
|
|
start = end - keeps + 1
|
|
}
|
|
|
|
if (this.range.start !== start) {
|
|
this.updateRange(start, end)
|
|
}
|
|
}
|
|
|
|
// setting to a new range and re-render
|
|
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 trulyEnd = Math.min(theoryEnd, this.getLastIndex())
|
|
return trulyEnd
|
|
}
|
|
|
|
// return total front offset
|
|
getPadFront() {
|
|
if (this.isFixedType()) {
|
|
return this.fixedSizeValue * this.range.start
|
|
} else {
|
|
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 it's all calculated, return the exactly offset
|
|
if (this.lastCalcIndex === lastIndex) {
|
|
return this.getIndexOffset(lastIndex) - this.getIndexOffset(end)
|
|
} else {
|
|
// 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)
|
|
}
|
|
} |