使用 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) }
内容版权声明:除非注明,否则皆为本站原创文章。