使用 Vue 实现一个虚拟列表的方法(2)
该实现可以很好地工作。
但是,该算法存在严重的性能问题,每获取一个列表项的坐标都要遍历列表,复杂度 O(n),非常不划算。
如果换一种方式,直接存储每一项的坐标呢?
其实本质是一样的。因为我们的列表项高度是不固定的,我们快速拖动滚动条到不同的区域时,需要根据局部渲染结果算出高度用于更新数组记录,而在更新某一项时,该项后续的所有条目也需要全部更新,复杂度一样是 O(n)。
所以,使用数组来维护每一项的高度或者坐标,在列表规模比较大的时候,就会消耗大量的 CPU 时间。
也许使用 TypedArray 会有好的表现?
仔细观察上面例子中的数组,结合现实中列表的情况,我们可以观察到一个现象:
列表项往往是相似的,在许多情况下,高度也很可能是一致的。
基于这种经验,我们可以采用区间来存储列表项的高度。
通过折叠记录相邻的,相同高度的列表项,来减少列表遍历操作。
比如以下表示方式:
const range = {
start: 0,
end: 4,
value: 20
}
可以很好地表达列表第 1 项至第 5 项的高度都为 20px。
如果我们需要求第 6 项的高度的话,就只需进行一次简单的四则运算即可,无需遍历累加这 5 项。
很容易得出结论,如果列表大部分情况是相同高度,只有个别条目高度不一致时(例如文本换行),将会有非常优异的性能表现。
当然使用区间,也不是没有代价的。这又会带来数据结构的复杂性。
由于折叠了相邻相同高度的结点,会导致存储的列表无法跟原始条目一一对应。所以,我们就不能简单得知我们想查询的列表项的高度存储在哪里了, 为此需要设计一种专门的存储机制。
这种存储机制,需要拥有这些特性:
- 高效的查询。可以通过列表项序号,快速获得对应的 range,以及该 range 之前的所有 range。
- 高效地修改。可以高效地插入、移除 range,合并 range、拆分 range。
结合我们学过的数据结构知识,可以考虑使用某种 BST 来存储,从而获得良好的查询、插入性能。 而 range 的合并、拆分等,则可以实现一个专门的类来管理。
下面直接给出一个简单的代码实现供参考,代码中已经加上了大量的注释,直接阅读应该比解说要更清晰。
// Avl.ts
const SLIGHTLY_UNBALANCED_RIGHT = -1
const SLIGHTLY_UNBALANCED_LEFT = 1
const UNBALANCED_RIGHT = -2
const UNBALANCED_LEFT = 2
// 树结点
class AvlNode<K extends any = any> {
key: any
left: AvlNode<K> | null
right: AvlNode<K> | null
parent: AvlNode<K> | null
_height: number
_prevHeight: number
constructor(key: K) {
this.key = key
this.left = null
this.right = null
this.parent = null
this._height = 0
this._prevHeight = 0
}
// 刷新前的高度,方便平衡操作
get prevHeight() {
return this._prevHeight | 0
}
get height() {
return this._height | 0
}
set height(value) {
this._prevHeight = this._height | 0
this._height = value | 0
}
// 左子树高度
get leftHeight() {
if (this.left === null) return -1
return this.left.height | 0
}
// 右子树高度
get rightHeight() {
if (this.right === null) return -1
return this.right.height | 0
}
// 平衡因子
get balanceFactor() {
return this.leftHeight - this.rightHeight
}
updateHeight() {
const { leftHeight, rightHeight } = this
const height = ((leftHeight > rightHeight ? leftHeight : rightHeight) + 1) | 0
this.height = height
}
}
// AVL 树
export class Avl<K extends any = any> {
_root: AvlNode<K> | null
_size: number
constructor() {
this._root = null
this._size = 0
}
get size() {
return this._size
}
// 插入节点
insert(key: K) {
const node = new AvlNode<K>(key)
const insertPoint = this._nodeInsert(node)
// 本次插入是重复结点,直接更新 key / value
// 无新结点插入,所以无需进行插入后的调整
if (insertPoint == null) return
// 新增结点成功时,回溯调整搜索路径上的结点
this._adjustAfterInsertion(insertPoint)
}
// 删除节点,返回是否成功删除结点
delete(key: K): boolean {
// 搜索待删除结点
const targetNode = this._nodeSearch(key)
// 未找到 value 对应结点
if (targetNode == null) return false
// 执行删除结点操作
const backtracking = this._nodeErase(targetNode)
const parent = backtracking[0]
// 回溯调整搜索路径上的结点
if (parent !== null) {
this._adjustAfterRemoval(parent)
}
return true
}
// 通过 key 查找包含该 key 范围的节点 key
search(key: K) {
const node = this._nodeSearch(key)
if (node !== null) return node.key
return null
}
// 搜索 start 、end 两个 key 之间的所有 key 列表
searchRange(start: K, end: K) {
const results: K[] = []
// 找到符合条件的 root 节点
let root = this._root
while (root !== null) {
const result1 = start.compareTo(root.key)
const result2 = end.compareTo(root.key)
// 当前节点比 start 小,不再搜索左子树
if (result1 > 0) {
root = root.right
continue
}
// 当前节点大于 end,不再搜索右子树
if (result2 < 0) {
root = root.left
continue
}
break
}
if (!root) return results
const stack = []
let current: AvlNode<K> | null = root
while (stack.length || current) {
while (current) {
stack.push(current)
// 当前节点比 start 小,不再搜索 current 的左子树
if (start.compareTo(current.key) > 0) break
current = current.left
}
if (stack.length) {
// 指向栈顶
current = stack[stack.length - 1]
const gteStart = start.compareTo(current.key) <= 0
const lteEnd = end.compareTo(current.key) >= 0
if (gteStart && lteEnd) {
results.push(current.key)
}
stack.pop()
// 只有 current 比 end 小,才继续搜索 current 的右子树
if (lteEnd) {
current = current.right
}
else {
current = null
}
}
}
return results
}
// 增加结点数量
_increaseSize() {
this._size += 1
}
// 减少结点数量
_decreaseSize() {
this._size -= 1
}
// 设置左子结点,同时维护 parent 关系
_setLeft(node: AvlNode<K>, child: AvlNode<K> | null) {
// 断开旧 left 结点
if (node.left !== null) {
node.left.parent = null
}
// 连接新结点
if (child !== null) {
// 从旧 parent 中断开
if (child.parent !== null) {
child.parent.left === child ? (child.parent.left = null) : (child.parent.right = null)
}
child.parent = node
}
node.left = child
}
// 设置右子结点,同时维护 parent 关系
_setRight(node: AvlNode<K>, child: AvlNode<K> | null) {
// 断开旧 right 结点
if (node.right !== null) {
node.right.parent = null
}
// 连接新结点
if (child !== null) {
// 从旧 parent 中断开
if (child.parent !== null) {
child.parent.left === child ? (child.parent.left = null) : (child.parent.right = null)
}
child.parent = node
}
node.right = child
}
// 获取中序遍历顺序的前驱结点
_inorderPredecessor(node: AvlNode<K> | null) {
if (node == null) return null
// 1. 有左子树,找到左子树最大元素
if (node.left !== null) {
return this._maximumNode(node.left)
}
// 2. 没有左子树,往上搜索
let parent = node.parent
while (parent != null) {
if (node == parent.right) {
return parent
}
node = parent
parent = node.parent
}
// 4. 搜索到根
return null
}
// 获取最大的结点
_maximumNode(subRoot: AvlNode<K>) {
let current = subRoot
while (current.right !== null) current = current.right
return current
}
// 设置根结点
_setRoot(node: AvlNode<K> | null) {
if (node === null) {
this._root = null
return
}
this._root = node
// 如果本身在树中,则从树中脱落,成为独立的树根
if (node.parent !== null) {
node.parent.left === node ? (node.parent.left = null) : (node.parent.right = null)
node.parent = null
}
}
// 将树上某个结点替换成另一个结点
private _replaceNode(node: AvlNode<K>, replacer: AvlNode<K> | null) {
if (node === replacer) return node
// node 为 root 的情况
if (node === this._root) {
this._setRoot(replacer)
}
else {
// 非 root,有父结点的情况
const parent = node.parent as AvlNode<K>
if (parent.left === node) this._setLeft(parent, replacer)
else this._setRight(parent, replacer)
}
return node
}
// 左旋,返回新顶点,注意旋转完毕会从原本的树上脱落
private _rotateLeft(node: AvlNode<K>) {
const parent = node.parent
// 记录原本在树上的位置
const isLeft = parent !== null && parent.left == node
// 旋转
const pivot = node.right as AvlNode<K>
const pivotLeft = pivot.left
this._setRight(node, pivotLeft)
this._setLeft(pivot, node)
// 旋转完毕
// 新顶点接上树上原本的位置
if (parent !== null) {
if (isLeft) this._setLeft(parent, pivot)
else this._setRight(parent, pivot)
}
// ---
if (node === this._root) {
this._setRoot(pivot)
}
node.updateHeight()
pivot.updateHeight()
return pivot
}
// 右旋,返回新顶点,注意旋转完毕会从原本的树上脱落
private _rotateRight(node: AvlNode<K>) {
const parent = node.parent
// 记录原本在树上的位置
const isLeft = parent !== null && parent.left === node
// 旋转
const pivot = node.left as AvlNode<K>
const pivotRight = pivot.right
this._setLeft(node, pivotRight)
this._setRight(pivot, node)
// 旋转完毕
// 新顶点接上树上原本的位置
if (parent !== null) {
if (isLeft) this._setLeft(parent, pivot)
else this._setRight(parent, pivot)
}
// ---
if (node === this._root) {
this._setRoot(pivot)
}
node.updateHeight()
pivot.updateHeight()
return pivot
}
// 搜索 Node
private _nodeSearch(key: K) {
let current = this._root
while (current !== null) {
let result = key.compareTo(current.key)
if (result === 0) return current
if (result < 0) current = current.left
else current = current.right
}
return null
}
// 在树里插入结点或者刷新重复结点
// 返回新插入(或刷新)的结点
private _nodeInsert(node: AvlNode<K>) {
// 空树
if (this._root === null) {
this._setRoot(node)
this._increaseSize()
return null
}
const key = node.key
let current = this._root
// 查找待插入的位置
while (true) {
const result = key.compareTo(current.key)
if (result > 0) {
if (current.right === null) {
this._setRight(current, node)
this._increaseSize()
return current
}
current = current.right
}
else if (result < 0) {
if (current.left === null) {
this._setLeft(current, node)
this._increaseSize()
return current
}
current = current.left
}
else {
// No duplicates, just update key
current.key = key
return null
}
}
}
// 从树上移除一个结点
private _nodeErase(node: AvlNode<K>) {
// 同时拥有左右子树
// 先转换成只有一颗子树的情况再统一处理
if (node.left !== null && node.right !== null) {
const replacer = this._inorderPredecessor(node)!
// 使用前驱结点替换身份
// 此时问题转换成删掉替代结点(前驱),
// 从而简化成只有一个子结点的删除情况
node.key = replacer.key
// 修改 node 指针
node = replacer
}
// 删除点的父结点
const parent = node.parent
// 待删结点少于两颗子树时,使用子树 (或 null,没子树时) 顶替移除的结点即可
const child = node.left || node.right
this._replaceNode(node, child)
this._decreaseSize()
return [ parent, child, node ]
}
// AVL 树插入结点后调整动作
// 自底向上调整结点的高度
// 遇到离 current 最近的不平衡点需要做旋转调整
// 注意: 对最近的不平衡点调整后 或者 结点的高度值没有变化时
// 上层结点便不需要更新
// 调整次数不大于1
_adjustAfterInsertion(backtracking: AvlNode<K> | null) {
let current = backtracking
// 往上回溯,查找最近的不平衡结点
while (current !== null) {
// 更新高度
current.updateHeight()
// 插入前后,回溯途径结点的高度没有变化,则无需继续回溯调整
if (current.height === current.prevHeight) break
// 若找到不平衡结点,执行一次调整即可
if (this._isUnbalanced(current)) {
this._rebalance(current)
// 调整过,则上层无需再调整了
break
}
current = current.parent
}
}
// AVL树删除结点后调整动作
// 自底向上调整结点的高度
// 遇到离 current 最近的不平衡点需要做旋转调整
// 注意: 对最近的不平衡点调整后,其上层结点仍然可能需要调整
// 调整次数可能不止一次
_adjustAfterRemoval(backtracking: AvlNode<K> | null) {
let current = backtracking
while (current !== null) {
// 更新高度
current.updateHeight()
// 删除前后,回溯途径结点的高度没有变化,则无需继续回溯调整
if (current.height === current.prevHeight) break
if (this._isUnbalanced(current)) {
this._rebalance(current)
}
// 与插入不同,调整过后,仍然需要继续往上回溯
// 上层结点(若有)仍需判断是否需要调整
current = current.parent
}
}
// LL
_adjustLeftLeft(node: AvlNode<K>) {
return this._rotateRight(node)
}
// RR
_adjustRightRight(node: AvlNode<K>) {
return this._rotateLeft(node)
}
// LR
_adjustLeftRight(node: AvlNode<K>) {
this._rotateLeft(node.left!)
return this._rotateRight(node)
}
// RL
_adjustRightLeft(node: AvlNode<K>) {
this._rotateRight(node.right!)
return this._rotateLeft(node)
}
// 检查结点是否平衡
_isUnbalanced(node: AvlNode<K>) {
const factor = node.balanceFactor
return factor === UNBALANCED_RIGHT || factor === UNBALANCED_LEFT
}
// 重新平衡
_rebalance(node: AvlNode<K>) {
const factor = node.balanceFactor
// Right subtree longer (node.factor: -2)
if (factor === UNBALANCED_RIGHT) {
let right = node.right!
// RL, node.right.factor: 1
if (right.balanceFactor === SLIGHTLY_UNBALANCED_LEFT) {
return this._adjustRightLeft(node)
}
else {
// RR, node.right.factor: 0|-1
// 即 right.rightHeight >= right.leftHeight
return this._adjustRightRight(node)
}
}
else if (factor === UNBALANCED_LEFT) {
// Left subtree longer (node.factor: 2)
let left = node.left!
// LR, node.left.factor: -1
if (left.balanceFactor === SLIGHTLY_UNBALANCED_RIGHT) {
return this._adjustLeftRight(node)
}
else {
// LL, node.left.factor: 1 | 0
// 即 left.leftHeight >= left.rightHeight
return this._adjustLeftLeft(node)
}
}
return node
}
}
export function createAvl() {
return new Avl()
}
// SparseRangeList.ts
import { createAvl, Avl } from './Avl'
// 区间类
class Range {
start: number
end: number
constructor(start: number, end?: number) {
this.start = start
this.end = end || start
}
// 用于 Avl 中节点的比较
//
// 列表中项目范围是连续的,必定不会重叠的
// 如果传入的 key 为重叠的,则意味着希望通过构造一个子 Range 搜索所在的 RangeValue
// 例如构造一个 { start: 10, end: 10, value: 'any' },搜索树中
// 范围包含 10~10 的 RangeValue,如 { start: 0, end: 20, value: 'any' }
compareTo(other: Range) {
if (other.start > this.end!) return -1
if (other.end! < this.start) return 1
return 0
}
}
// 区间-值 类
class RangeValue<T> extends Range {
value: T
constructor(start: number, end: number, value: T) {
super(start, end)
this.value = value
}
clone(): RangeValue<T> {
return new RangeValue(this.start, this.end!, this.value)
}
}
// 最终存储区间-值的类,内部使用 Avl 存储所有 RangeValue
export default class SparseRangeList<T> {
_size: number
defaultValue: T
valueTree: Avl
constructor(size: number, defaultValue: T) {
this._size = size
this.defaultValue = defaultValue
this.valueTree = createAvl()
}
get size() {
return this._size
}
resize(newSize: number) {
newSize = newSize | 0
// 无调整
if (this._size === newSize) return
// 扩容
if (this._size < newSize) {
this._size = newSize
return
}
// 缩小,清空超出的部分,再缩小
this.setRangeValue(newSize - 1, this._size - 1, this.defaultValue)
this._size = newSize
}
// 返回区间包含 index 的 RangeValue 的值
getValueAt(index: number): T {
const result = this.valueTree.search(new Range(index))
if (result) return result.value
return this.defaultValue
}
/**
* 设值方法,
* 自动与相邻的相同值的合并成更大的 RangeValue,
* 导致原本的 RangeValue 不连续,则会
* 自动切分成两个或者三个 RangeValue。
*
* a-------------a
* |a------------|b------------|c-----------|...
*
* 结果:
* |a-------------------|b-----|c-----------|...
*
*
* d-------------d
* |a------------|b------------|c-----------|...
*
* 结果:
* |a-----|d------------|b-----|c-----------|...
*
*/
setRangeValue(start: number, end: number, value: T) {
if (!this.size) return
if (end >= this.size) end = this.size - 1
// 所有与当前传入区间范围有重叠部分,
// -1,+1 将接壤的毗邻 RangeValue 也纳入(如果存在的话),
// 毗邻的 RangeValue 要检查否要合并。
let prevSiblingEnd = start - 1
let nextSiblingStart = end + 1
let rangeValues = this.treeIntersecting(prevSiblingEnd, nextSiblingStart)
// 如果没有重叠的部分,则作为新的 RangeValue 插入,直接结束
// 如果有重叠的部分,就要处理合并、拆分
if (rangeValues.length) {
let firstRange = rangeValues[0]
let lastRange = rangeValues[rangeValues.length - 1]
// end 边界比传入的 start 小,说明是接壤毗邻的更小的 RangeValue
//
// 1. 如果毗邻的 RangeValue 的值跟当前带插入的值不一致,
// 则直接将毗邻的 RangeValue 从列表中移除,
// 不需要做任何特殊操作,正常的插入操作即可
//
// 2. 否则如果毗邻的 RangeValue 的值跟当前待插入的值一致,
// 则将两个 RangeValue 的 Range 合并(修改 start即可),
// 然后这个毗邻的 RangeValue 也自然变成重叠的,正常执行后续
// 的重叠处理逻辑即可(拆分)
if (firstRange.end < start) {
if (firstRange.value !== value) {
rangeValues.shift()
}
else {
start = firstRange.start
}
}
// 接壤毗邻的更大的 RangeValue,处理思路
// 跟上面处理毗邻的更小的 RangeValue 一样的
if (lastRange.start > end) {
if (lastRange.value !== value) {
rangeValues.pop()
}
else {
end = lastRange.end
}
}
// 结束毗邻 RangeValue 合并逻辑
// 开始处理相交的 RangeValue 流程
const length = rangeValues.length
let index = 0
while (index < length) {
const currentRangeValue = rangeValues[index]
const { value: currentValue, start: currentStart, end: currentEnd } = currentRangeValue
// 先移除掉该重叠的 RangeValue,然后:
this.valueTree.delete(currentRangeValue)
// Case 1. 如果是当前 RangeValue 完整包含在传入的范围内,
// 则不需要处理,因为整个范围都将被传入的值覆盖。
if (currentStart >= start && currentEnd <= end) {
index += 1
continue
}
// Case2. 部分相交,该 RangeValue 的大的一侧在传入的范围内,而小的一侧不在。
// 需要做切分操作,以重叠的位置作为切分点,比较小的一侧(不重叠的部分)重新插入,
// 比较大的的那一部分,会被传入的值覆盖掉
if (currentStart < start) {
// 如果值不一样,则以相交的位置作为切分点,非重叠部分重新插入,重叠部分用待插入的值覆盖。
if (currentValue !== value) {
this._insert(currentStart, start - 1, currentValue)
}
else {
start = currentStart
}
}
// Case3. 部分相交,该 RangeValue 的小的一侧在传入的范围内,而大的一侧不在。
// 同 Case 2 做切分操作,只是反向。
if (currentEnd > end) {
if (currentValue !== value) {
this._insert(end + 1, currentEnd, currentValue)
}
else {
end = currentEnd
}
}
index += 1
}
}
this._insert(start, end, value)
}
setValue(index: number, value: T) {
this.setRangeValue(index, index, value)
}
/**
* 筛选出与指定区间有重叠的 RangeValue,即:
*
* 1. 相互部分重叠
*
* o----------o o---------o
* >start------------------end<
*
*
* 2. 相互完全重叠关系
*
* o----------------o
* >start--------end<
*
*
* 3. 包含或被包含关系
*
* o--------------------------------------o
* o-------------------------------o
* o-------------------------------o
* o-----o o-----o o----o
* >start--------------------end<
*
*/
treeIntersecting(start: number, end: number): RangeValue[] {
const startRange = new Range(start)
const endRange = new Range(end)
return this.valueTree.searchRange(startRange, endRange)
}
/**
* 返回指定范围内所有 RangeValue
* 范围内有无值的 Range 的话,则使用
* 携带默认值的 RangeValue 代替
* 从而确保返回的结果是线性的、每个区间都有值的,如:
*
* start>...<end 范围内有 A、B 两个 RangeValue,所有空洞都用 Default 补足
* +-----------|-----|-----------|-----|-----------+
* | Default | A | Default | B | Default |
* >start------|-----|-----------|-----|--------end<
*
*/
intersecting(start: number, end: number): RangeValue[] {
const ranges = this.treeIntersecting(start, end)
if (!ranges.length) {
if (!this.size) return []
return [ new RangeValue(start, end, this.defaultValue) ]
}
let result = []
let range
let index = 0
let length = ranges.length
while (index < length) {
range = ranges[index].clone()
// 传入的 (start, end) 右侧与某个 RangeValue 重叠,
// 左侧没有命中,则左侧区域手动塞入一个携带默认
// 值的 RangeValue
if (range.start > start) {
result.push(new RangeValue(start, range.start - 1, this.defaultValue))
}
result.push(range)
// 将 start 的位置右移,
// 以便下个 range 的比较
start = range.end + 1
index += 1
}
// 如果最后一个 range,与传入的范围只有左侧重叠,
// 而右侧没有重叠的地方,则手动塞入一个携带默认值
// 的 RangeValue
if (range.end < end) {
result.push(new RangeValue(range.end + 1, end, this.defaultValue))
}
else if (range.end > end) {
// 否则如果最后一个 range 的范围已经超出需要的范围,则裁剪
range.end = end
}
return result
}
values() {
if (!this.size) return []
return this.intersecting(0, this.size - 1)
}
_insert(start: number, end: number, value: T) {
if (value !== this.defaultValue) {
const rangeValue = new RangeValue(start, end, value)
this.valueTree.insert(rangeValue)
}
}
}
export function create<T>(size: number, value: T) {
return new SparseRangeList(size, value)
}
内容版权声明:除非注明,否则皆为本站原创文章。
