亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

使用 Vue 實(shí)現(xiàn)一個虛擬列表的方法

 更新時間:2019年08月20日 08:57:51   作者:dawencilv-1  
這篇文章主要介紹了使用 Vue 實(shí)現(xiàn)一個虛擬列表的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下

因?yàn)?DOM 性能瓶頸,大型列表存在難以克服的性能問題。 因此,就有了 “局部渲染” 的優(yōu)化方案,這就是虛擬列表的核心思想。

虛擬列表的實(shí)現(xiàn),需要重點(diǎn)關(guān)注的問題一有以下幾點(diǎn):

  • 可視區(qū)域的計算方法
  • 可視區(qū)域的 DOM 更新方案
  • 事件的處理方案

下面逐一分解說明。

可視區(qū)域計算

可視區(qū)域的計算,就是使用當(dāng)前視口的高度、當(dāng)前滾動條滾過的距離,得到一個可視區(qū)域的坐標(biāo)區(qū)間。 算出可視區(qū)域的坐標(biāo)區(qū)間之后,在去過濾出落在該區(qū)間內(nèi)的列表項(xiàng),這個過程,列表項(xiàng)的坐標(biāo)也是必須能算出的。

思考以下情況,

  • 我們的視口高度為 100px
  • 我們當(dāng)前已經(jīng)滾動了 100px
  • 我們的列表項(xiàng),每一項(xiàng)高度為 20px

根據(jù)這些條件,我們可以計算出,當(dāng)前可視區(qū)域?yàn)榈?11 項(xiàng)至第 20 項(xiàng)。

01 - 05,可視區(qū)域上方
+----+-----------+--------
| 06 | 100 ~ 120 |
+----+-----------+
| 07 | 120 ~ 140 |
+----+-----------+
| 08 | 140 ~ 160 | 可視區(qū)域
+----+-----------+
| 09 | 160 ~ 180 |
+----+-----------+
| 10 | 180 ~ 200 |
+----+-----------+--------
  11 - N,可視區(qū)域下方

這是因?yàn)榱斜眄?xiàng)高度是固定的,我們可以通過簡單的四則運(yùn)算算出已經(jīng)滾動過去的 100px 中,已經(jīng)滾動走了 5 個列表項(xiàng),因此可視區(qū)域是從第 6 項(xiàng)開始,而視口高度為 100px,能容納 100 / 20 即 5 個條目。

上面例子的情況非常簡單,也不存在性能問題,因此實(shí)際上并沒有展開討論的價值。 而還有另一種復(fù)雜很多的情況,那就是,列表項(xiàng)高度不固定,根據(jù)內(nèi)容決定高度。

此時,我們就沒辦法直接使用四則運(yùn)算一步到位算出可視區(qū)域?qū)?yīng)的條目了。

而必須實(shí)現(xiàn)一種機(jī)制,記錄所有列表項(xiàng)的坐標(biāo),再通過檢查列表項(xiàng)是否落在視口內(nèi)。

下面重點(diǎn)討論該問題。

列表項(xiàng)的坐標(biāo)

列表項(xiàng)的坐標(biāo),可以通過以下公式定義:

<列表項(xiàng) top 坐標(biāo)值> = <上一項(xiàng) top 坐標(biāo)值> + <上一項(xiàng)的高度值>

第一個列表項(xiàng)的 top 坐標(biāo)值為 0,因此,只要記錄所有列表項(xiàng)目的高度,即可算出任意一個列表項(xiàng)的 top 坐標(biāo)值。 于是,問題就變成了,必須使用某種方式來存儲每個條目的高度。

我想,最容易想到的方案就是,使用一個數(shù)組,一一對應(yīng)地存儲列表每項(xiàng)的高度值。 然后獲取特定項(xiàng)的坐標(biāo)值時,提取第一項(xiàng)到目標(biāo)項(xiàng)的值,進(jìn)行累加運(yùn)算。參考下面代碼進(jìn)行理解:

// 假設(shè)使用該數(shù)組存儲列表每一項(xiàng)的高度
const itemHeightStore = [20, 20, 20, 20, 20]

// 使用該方法,可以算出列表中指定項(xiàng)的 top 坐標(biāo)值
const getTop = (index) => {
 let sum = 0
 while (index--) sum += itemHeightStore[index] || 0
 return sum
}
// 第一項(xiàng)
getTop(0) // 0

// 第二項(xiàng)
getTop(1) // 20
// ...

該實(shí)現(xiàn)可以很好地工作。

但是,該算法存在嚴(yán)重的性能問題,每獲取一個列表項(xiàng)的坐標(biāo)都要遍歷列表,復(fù)雜度 O(n),非常不劃算。

如果換一種方式,直接存儲每一項(xiàng)的坐標(biāo)呢?

其實(shí)本質(zhì)是一樣的。因?yàn)槲覀兊牧斜眄?xiàng)高度是不固定的,我們快速拖動滾動條到不同的區(qū)域時,需要根據(jù)局部渲染結(jié)果算出高度用于更新數(shù)組記錄,而在更新某一項(xiàng)時,該項(xiàng)后續(xù)的所有條目也需要全部更新,復(fù)雜度一樣是 O(n)。

所以,使用數(shù)組來維護(hù)每一項(xiàng)的高度或者坐標(biāo),在列表規(guī)模比較大的時候,就會消耗大量的 CPU 時間。

也許使用 TypedArray 會有好的表現(xiàn)?

仔細(xì)觀察上面例子中的數(shù)組,結(jié)合現(xiàn)實(shí)中列表的情況,我們可以觀察到一個現(xiàn)象:

列表項(xiàng)往往是相似的,在許多情況下,高度也很可能是一致的。
基于這種經(jīng)驗(yàn),我們可以采用區(qū)間來存儲列表項(xiàng)的高度。

通過折疊記錄相鄰的,相同高度的列表項(xiàng),來減少列表遍歷操作。
比如以下表示方式:

const range = {
 start: 0,
 end: 4,
 value: 20
}

可以很好地表達(dá)列表第 1 項(xiàng)至第 5 項(xiàng)的高度都為 20px。

如果我們需要求第 6 項(xiàng)的高度的話,就只需進(jìn)行一次簡單的四則運(yùn)算即可,無需遍歷累加這 5 項(xiàng)。

很容易得出結(jié)論,如果列表大部分情況是相同高度,只有個別條目高度不一致時(例如文本換行),將會有非常優(yōu)異的性能表現(xiàn)。

當(dāng)然使用區(qū)間,也不是沒有代價的。這又會帶來數(shù)據(jù)結(jié)構(gòu)的復(fù)雜性。

由于折疊了相鄰相同高度的結(jié)點(diǎn),會導(dǎo)致存儲的列表無法跟原始條目一一對應(yīng)。所以,我們就不能簡單得知我們想查詢的列表項(xiàng)的高度存儲在哪里了, 為此需要設(shè)計一種專門的存儲機(jī)制。

這種存儲機(jī)制,需要擁有這些特性:

  • 高效的查詢??梢酝ㄟ^列表項(xiàng)序號,快速獲得對應(yīng)的 range,以及該 range 之前的所有 range。
  • 高效地修改。可以高效地插入、移除 range,合并 range、拆分 range。

結(jié)合我們學(xué)過的數(shù)據(jù)結(jié)構(gòu)知識,可以考慮使用某種 BST 來存儲,從而獲得良好的查詢、插入性能。 而 range 的合并、拆分等,則可以實(shí)現(xiàn)一個專門的類來管理。

下面直接給出一個簡單的代碼實(shí)現(xiàn)供參考,代碼中已經(jīng)加上了大量的注釋,直接閱讀應(yīng)該比解說要更清晰。

// Avl.ts
const SLIGHTLY_UNBALANCED_RIGHT = -1
const SLIGHTLY_UNBALANCED_LEFT = 1
const UNBALANCED_RIGHT = -2
const UNBALANCED_LEFT = 2
// 樹結(jié)點(diǎn)
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
 }
 // 插入節(jié)點(diǎn)
 insert(key: K) {
  const node = new AvlNode<K>(key)
  const insertPoint = this._nodeInsert(node)
  // 本次插入是重復(fù)結(jié)點(diǎn),直接更新 key / value
  // 無新結(jié)點(diǎn)插入,所以無需進(jìn)行插入后的調(diào)整
  if (insertPoint == null) return
  // 新增結(jié)點(diǎn)成功時,回溯調(diào)整搜索路徑上的結(jié)點(diǎn)
  this._adjustAfterInsertion(insertPoint)
 }
 // 刪除節(jié)點(diǎn),返回是否成功刪除結(jié)點(diǎn)
 delete(key: K): boolean {
  // 搜索待刪除結(jié)點(diǎn)
  const targetNode = this._nodeSearch(key)
  // 未找到 value 對應(yīng)結(jié)點(diǎn)
  if (targetNode == null) return false
  // 執(zhí)行刪除結(jié)點(diǎn)操作
  const backtracking = this._nodeErase(targetNode)
  const parent = backtracking[0]
  // 回溯調(diào)整搜索路徑上的結(jié)點(diǎn)
  if (parent !== null) {
   this._adjustAfterRemoval(parent)
  }
  return true
 }
 // 通過 key 查找包含該 key 范圍的節(jié)點(diǎn) 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 節(jié)點(diǎn)
  let root = this._root
  while (root !== null) {
   const result1 = start.compareTo(root.key)
   const result2 = end.compareTo(root.key)
   // 當(dāng)前節(jié)點(diǎn)比 start 小,不再搜索左子樹
   if (result1 > 0) {
    root = root.right
    continue
   }
   // 當(dāng)前節(jié)點(diǎn)大于 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)
    // 當(dāng)前節(jié)點(diǎn)比 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 小,才繼續(xù)搜索 current 的右子樹
    if (lteEnd) {
     current = current.right
    }
    else {
     current = null
    }
   }
  }
  return results
 }
 // 增加結(jié)點(diǎn)數(shù)量
 _increaseSize() {
  this._size += 1
 }
 // 減少結(jié)點(diǎn)數(shù)量
 _decreaseSize() {
  this._size -= 1
 }
 // 設(shè)置左子結(jié)點(diǎn),同時維護(hù) parent 關(guān)系
 _setLeft(node: AvlNode<K>, child: AvlNode<K> | null) {
  // 斷開舊 left 結(jié)點(diǎn)
  if (node.left !== null) {
   node.left.parent = null
  }
  // 連接新結(jié)點(diǎn)
  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
 }
 // 設(shè)置右子結(jié)點(diǎn),同時維護(hù) parent 關(guān)系
 _setRight(node: AvlNode<K>, child: AvlNode<K> | null) {
  // 斷開舊 right 結(jié)點(diǎn)
  if (node.right !== null) {
   node.right.parent = null
  }
  // 連接新結(jié)點(diǎn)
  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
 }
 // 獲取中序遍歷順序的前驅(qū)結(jié)點(diǎn)
 _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
 }
 // 獲取最大的結(jié)點(diǎn)
 _maximumNode(subRoot: AvlNode<K>) {
  let current = subRoot
  while (current.right !== null) current = current.right
  return current
 }
 // 設(shè)置根結(jié)點(diǎn)
 _setRoot(node: AvlNode<K> | null) {
  if (node === null) {
   this._root = null
   return
  }
  this._root = node
  // 如果本身在樹中,則從樹中脫落,成為獨(dú)立的樹根
  if (node.parent !== null) {
   node.parent.left === node ? (node.parent.left = null) : (node.parent.right = null)
   node.parent = null
  }
 }
 // 將樹上某個結(jié)點(diǎn)替換成另一個結(jié)點(diǎn)
 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,有父結(jié)點(diǎn)的情況
   const parent = node.parent as AvlNode<K>
   if (parent.left === node) this._setLeft(parent, replacer)
   else this._setRight(parent, replacer)
  }
  return node
 }
 // 左旋,返回新頂點(diǎn),注意旋轉(zhuǎn)完畢會從原本的樹上脫落
 private _rotateLeft(node: AvlNode<K>) {
  const parent = node.parent
  // 記錄原本在樹上的位置
  const isLeft = parent !== null && parent.left == node
  // 旋轉(zhuǎn)
  const pivot = node.right as AvlNode<K>
  const pivotLeft = pivot.left
  this._setRight(node, pivotLeft)
  this._setLeft(pivot, node)
  // 旋轉(zhuǎn)完畢
  // 新頂點(diǎn)接上樹上原本的位置
  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
 }
 // 右旋,返回新頂點(diǎn),注意旋轉(zhuǎn)完畢會從原本的樹上脫落
 private _rotateRight(node: AvlNode<K>) {
  const parent = node.parent
  // 記錄原本在樹上的位置
  const isLeft = parent !== null && parent.left === node
  // 旋轉(zhuǎn)
  const pivot = node.left as AvlNode<K>
  const pivotRight = pivot.right
  this._setLeft(node, pivotRight)
  this._setRight(pivot, node)
  // 旋轉(zhuǎn)完畢
  // 新頂點(diǎn)接上樹上原本的位置
  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
 }
 // 在樹里插入結(jié)點(diǎn)或者刷新重復(fù)結(jié)點(diǎn)
 // 返回新插入(或刷新)的結(jié)點(diǎn)
 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
   }
  }
 }
 // 從樹上移除一個結(jié)點(diǎn)
 private _nodeErase(node: AvlNode<K>) {
  // 同時擁有左右子樹
  // 先轉(zhuǎn)換成只有一顆子樹的情況再統(tǒng)一處理
  if (node.left !== null && node.right !== null) {
   const replacer = this._inorderPredecessor(node)!
   // 使用前驅(qū)結(jié)點(diǎn)替換身份
   // 此時問題轉(zhuǎn)換成刪掉替代結(jié)點(diǎn)(前驅(qū)),
   // 從而簡化成只有一個子結(jié)點(diǎn)的刪除情況
   node.key = replacer.key
   // 修改 node 指針
   node = replacer
  }
  // 刪除點(diǎn)的父結(jié)點(diǎn)
  const parent = node.parent
  // 待刪結(jié)點(diǎn)少于兩顆子樹時,使用子樹 (或 null,沒子樹時) 頂替移除的結(jié)點(diǎn)即可
  const child = node.left || node.right
  this._replaceNode(node, child)
  this._decreaseSize()
  return [ parent, child, node ]
 }
 // AVL 樹插入結(jié)點(diǎn)后調(diào)整動作
 // 自底向上調(diào)整結(jié)點(diǎn)的高度
 // 遇到離 current 最近的不平衡點(diǎn)需要做旋轉(zhuǎn)調(diào)整 
 // 注意: 對最近的不平衡點(diǎn)調(diào)整后 或者 結(jié)點(diǎn)的高度值沒有變化時
 // 上層結(jié)點(diǎn)便不需要更新 
 // 調(diào)整次數(shù)不大于1
 _adjustAfterInsertion(backtracking: AvlNode<K> | null) {
  let current = backtracking
  // 往上回溯,查找最近的不平衡結(jié)點(diǎn)
  while (current !== null) {
   // 更新高度
   current.updateHeight()
   // 插入前后,回溯途徑結(jié)點(diǎn)的高度沒有變化,則無需繼續(xù)回溯調(diào)整
   if (current.height === current.prevHeight) break
   // 若找到不平衡結(jié)點(diǎn),執(zhí)行一次調(diào)整即可
   if (this._isUnbalanced(current)) {
    this._rebalance(current)
    // 調(diào)整過,則上層無需再調(diào)整了
    break
   }
   current = current.parent
  }
 }
 // AVL樹刪除結(jié)點(diǎn)后調(diào)整動作
 // 自底向上調(diào)整結(jié)點(diǎn)的高度
 // 遇到離 current 最近的不平衡點(diǎn)需要做旋轉(zhuǎn)調(diào)整
 // 注意: 對最近的不平衡點(diǎn)調(diào)整后,其上層結(jié)點(diǎn)仍然可能需要調(diào)整
 // 調(diào)整次數(shù)可能不止一次
 _adjustAfterRemoval(backtracking: AvlNode<K> | null) {
  let current = backtracking
  while (current !== null) {
   // 更新高度
   current.updateHeight()
   // 刪除前后,回溯途徑結(jié)點(diǎn)的高度沒有變化,則無需繼續(xù)回溯調(diào)整
   if (current.height === current.prevHeight) break
   if (this._isUnbalanced(current)) {
    this._rebalance(current)
   }
   // 與插入不同,調(diào)整過后,仍然需要繼續(xù)往上回溯
   // 上層結(jié)點(diǎn)(若有)仍需判斷是否需要調(diào)整
   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)
 }
 // 檢查結(jié)點(diǎn)是否平衡
 _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'
// 區(qū)間類
class Range {
 start: number
 end: number
 constructor(start: number, end?: number) {
  this.start = start
  this.end = end || start
 }
 // 用于 Avl 中節(jié)點(diǎn)的比較
 //
 // 列表中項(xiàng)目范圍是連續(xù)的,必定不會重疊的
 // 如果傳入的 key 為重疊的,則意味著希望通過構(gòu)造一個子 Range 搜索所在的 RangeValue
 // 例如構(gòu)造一個 { 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
 }
}
// 區(qū)間-值 類
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)
 }
}
// 最終存儲區(qū)間-值的類,內(nèi)部使用 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
  // 無調(diào)整
  if (this._size === newSize) return
  // 擴(kuò)容
  if (this._size < newSize) {
   this._size = newSize
   return
  }
  // 縮小,清空超出的部分,再縮小
  this.setRangeValue(newSize - 1, this._size - 1, this.defaultValue)
  this._size = newSize
 }
 // 返回區(qū)間包含 index 的 RangeValue 的值
 getValueAt(index: number): T {
  const result = this.valueTree.search(new Range(index))
  if (result) return result.value
  return this.defaultValue
 } 
 /**
  * 設(shè)值方法,
  * 自動與相鄰的相同值的合并成更大的 RangeValue,
  * 導(dǎo)致原本的 RangeValue 不連續(xù),則會
  * 自動切分成兩個或者三個 RangeValue。
  *
  *     a-------------a
  * |a------------|b------------|c-----------|...
  * 
  * 結(jié)果:
  * |a-------------------|b-----|c-----------|...
  * 
  * 
  *     d-------------d
  * |a------------|b------------|c-----------|...
  * 
  * 結(jié)果:
  * |a-----|d------------|b-----|c-----------|...
  * 
  */
 setRangeValue(start: number, end: number, value: T) {
  if (!this.size) return
  if (end >= this.size) end = this.size - 1
  // 所有與當(dāng)前傳入?yún)^(qū)間范圍有重疊部分,
  // -1,+1 將接壤的毗鄰 RangeValue 也納入(如果存在的話),
  // 毗鄰的 RangeValue 要檢查否要合并。
  let prevSiblingEnd = start - 1
  let nextSiblingStart = end + 1
  let rangeValues = this.treeIntersecting(prevSiblingEnd, nextSiblingStart)
  // 如果沒有重疊的部分,則作為新的 RangeValue 插入,直接結(jié)束
  // 如果有重疊的部分,就要處理合并、拆分
  if (rangeValues.length) {
   let firstRange = rangeValues[0]
   let lastRange = rangeValues[rangeValues.length - 1]
   // end 邊界比傳入的 start 小,說明是接壤毗鄰的更小的 RangeValue
   //
   // 1. 如果毗鄰的 RangeValue 的值跟當(dāng)前帶插入的值不一致,
   // 則直接將毗鄰的 RangeValue 從列表中移除,
   // 不需要做任何特殊操作,正常的插入操作即可
   //
   // 2. 否則如果毗鄰的 RangeValue 的值跟當(dāng)前待插入的值一致,
   // 則將兩個 RangeValue 的 Range 合并(修改 start即可),
   // 然后這個毗鄰的 RangeValue 也自然變成重疊的,正常執(zhí)行后續(xù)
   // 的重疊處理邏輯即可(拆分)
   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
    }
   }
   // 結(jié)束毗鄰 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. 如果是當(dāng)前 RangeValue 完整包含在傳入的范圍內(nèi),
    // 則不需要處理,因?yàn)檎麄€范圍都將被傳入的值覆蓋。
    if (currentStart >= start && currentEnd <= end) {
     index += 1
     continue
    }
    // Case2. 部分相交,該 RangeValue 的大的一側(cè)在傳入的范圍內(nèi),而小的一側(cè)不在。
    // 需要做切分操作,以重疊的位置作為切分點(diǎn),比較小的一側(cè)(不重疊的部分)重新插入,
    // 比較大的的那一部分,會被傳入的值覆蓋掉
    if (currentStart < start) {
     // 如果值不一樣,則以相交的位置作為切分點(diǎn),非重疊部分重新插入,重疊部分用待插入的值覆蓋。
     if (currentValue !== value) {
      this._insert(currentStart, start - 1, currentValue)
     }
     else {
      start = currentStart
     }
    }
    // Case3. 部分相交,該 RangeValue 的小的一側(cè)在傳入的范圍內(nèi),而大的一側(cè)不在。
    // 同 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)
 }
 /**
  * 篩選出與指定區(qū)間有重疊的 RangeValue,即:
  * 
  *       1. 相互部分重疊
  * 
  * o----------o       o---------o
  *   >start------------------end<
  * 
  * 
  *      2. 相互完全重疊關(guān)系
  * 
  *      o----------------o
  *      >start--------end<
  * 
  * 
  *      3. 包含或被包含關(guān)系
  * 
  * 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)
 }
 /**
  * 返回指定范圍內(nèi)所有 RangeValue
  * 范圍內(nèi)有無值的 Range 的話,則使用
  * 攜帶默認(rèn)值的 RangeValue 代替
  * 從而確保返回的結(jié)果是線性的、每個區(qū)間都有值的,如:
  * 
  * start>...<end 范圍內(nèi)有 A、B 兩個 RangeValue,所有空洞都用 Default 補(bǔ)足
  * +-----------|-----|-----------|-----|-----------+
  * | 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) 右側(cè)與某個 RangeValue 重疊,
   // 左側(cè)沒有命中,則左側(cè)區(qū)域手動塞入一個攜帶默認(rèn)
   // 值的 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,與傳入的范圍只有左側(cè)重疊,
  // 而右側(cè)沒有重疊的地方,則手動塞入一個攜帶默認(rèn)值
  // 的 RangeValue
  if (range.end < end) {
   result.push(new RangeValue(range.end + 1, end, this.defaultValue))
  }
  else if (range.end > end) {
   // 否則如果最后一個 range 的范圍已經(jīng)超出需要的范圍,則裁剪
   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)
}

有了這套存儲機(jī)制之后,我們就可以更高效地管理列表項(xiàng)的高度,和統(tǒng)計列表高度了。

看代碼理解:

import { create as createSparseRangeList } from './SparseRangeList'
// 創(chuàng)建一個默認(rèn)預(yù)估高度為 20 的列表項(xiàng)存儲對象
const itemHeightStore = createSparseRangeList(wrappedItems.length, 20)
// 設(shè)置第二項(xiàng)為 40px
itemHeightStore.setValue(1, 40)
// 獲取第二項(xiàng)的高度
itemHeightStore.getValueAt(1) // 40
// 獲取列表項(xiàng)的 top 坐標(biāo)
const top = (index: number): number => {
 if (index === 0) return 0
 // 0 ~ 上一項(xiàng)的高度累加
 const rangeValues = itemHeightStore.intersecting(0, index - 1)
 const sumHeight = rangeValues.reduce((sum: number, rangeValue: any) => {
  const span = rangeValue.end - rangeValue.start + 1
  return sum + rangeValue.value * span
 }, 0)
 return sumHeight
}
top(1) // 20
// 計算列表總高度:
const listHeight = itemHeightStore
 .values()
 .reduce((acc: number, rangeValue: any) => {
  const span = rangeValue.end - rangeValue.start + 1
  const height = rangeValue.value * span
  return acc + height
 }, 0)

計算可視條目

完成了列表項(xiàng)高度的管理,接下來需要解決的重點(diǎn),就是計算出哪些條目是可視的。

最簡單的實(shí)現(xiàn)方式,就是直接遍歷我們的結(jié)點(diǎn)高度存儲列表,逐個去跟視口的坐標(biāo)區(qū)間比較,過濾出落在(或部分落在)視口內(nèi)部的條目。 基于性能考慮,我們當(dāng)然不能這么簡單粗暴。我們可以做以下嘗試來提高性能:

一、預(yù)估起點(diǎn)條目 + 二分法修正。

通過條目的預(yù)估高度或默認(rèn)高度,算出可能出現(xiàn)在視口的第一條條目。 比如,我們視口上沿坐標(biāo)(即滾動條滾過的距離)為 100px,我們條目預(yù)估高度為 20px,那么,我們可以猜測第一個出現(xiàn)在視口中的條目為 100 / 20 + 1,即第 6 條。 我們直接計算第 6 條的坐標(biāo),檢查是否落在視口中,根據(jù)結(jié)果差距,再進(jìn)行二分法猜測,直到找到真正的起點(diǎn)條目。

二、預(yù)估終點(diǎn)條目 + 二分法修正

在算出起點(diǎn)條目后,在使用視口高度除以預(yù)估條目高度,算出視口內(nèi)部可能顯示多少項(xiàng),將起點(diǎn)序號加上這個數(shù)量,就是預(yù)估的終點(diǎn)條目序號。使用上述一樣的修正邏輯,直到找到正確的視口終點(diǎn)條目。

描述可能比較難以理解,下面給出關(guān)鍵片段:

// 內(nèi)部方法,計算局部渲染數(shù)據(jù)切片的起止點(diǎn)
private _calcSliceRange() {
 if (!this.dataView.length) {
  return { sliceFrom: 0, sliceTo: 0 }
 }
 // 數(shù)據(jù)總量
 const MAX = this.dataView.length
 // 視口上邊界
 const viewportTop = (this.$refs.viewport as any).scrollTop || 0
 // 視口下邊界
 const viewportBottom = viewportTop + this.viewportHeight
 // 預(yù)估條目高度
 const estimatedItemHeight = this.defaultItemHeight
 // 從估算值開始計算起始序號
 let sliceFrom = Math.floor(viewportTop / estimatedItemHeight!)
 if (sliceFrom > MAX - 1) sliceFrom = MAX - 1
 while (sliceFrom >= 0 && sliceFrom <= MAX - 1) {
  const itemTop = this._top(sliceFrom)
  // 條目頂部相對于 viewport 頂部的偏移
  const itemOffset = itemTop - viewportTop
  // 1. 該條目距離視口頂部有距離,說明上方還有條目元素需要顯示,繼續(xù)測試上一條
  if (itemOffset > 0) {
   // 二分法快速估算下一個嘗試位置
   const diff = itemOffset / estimatedItemHeight!
   sliceFrom -= Math.ceil(diff / 2)
   continue
  }
  // 2. 恰好顯示該條目的頂部,則該條目為本次視口的首條元素
  if (itemOffset === 0) break
  // 以下都是 itemOffset < 0
  const itemHeight = this._itemHeight(sliceFrom)
  // 3. 該條目在頂部露出了一部分,則該條目為本次視口的首條元素
  if (itemOffset < itemHeight) break
  // 4. 該條目已被滾出去視口,繼續(xù)測試下一條
  // 二分法快速估算下一個嘗試位置
  const diff = -itemOffset / estimatedItemHeight!
  sliceFrom += Math.ceil(diff / 2)
 }
 // 從估算值開始計算結(jié)束序號
 let sliceTo = sliceFrom + 1 + Math.floor(this.viewportHeight / estimatedItemHeight!)
 if (sliceTo > MAX) sliceTo = MAX
 while (sliceTo > sliceFrom && sliceTo <= MAX) {
  const itemTop = this._top(sliceTo)
  const itemHeight = this._itemHeight(sliceTo)
  const itemBottom = itemTop + itemHeight
  // 條目底部相對于 viewport 底部的偏移
  const itemOffset = itemBottom - viewportBottom
  // 1. 該條目的底部距離視口底部有距離,說明下方還有條目元素需要顯示,繼續(xù)測試下一條
  if (itemOffset < 0) {
   // 二分法快速估算下一個嘗試位置
   const diff = -itemOffset / estimatedItemHeight!
   sliceTo += Math.ceil(diff / 2)
   continue
  }
  // 2. 恰好顯示該條目的底部,則該條目為視口中最后一項(xiàng)
  if (itemOffset === 0) break
  // 3. 該條目在底部被裁剪了一部分,則該條目為本次視口的末項(xiàng)
  if (itemOffset < itemHeight) break
  // 該條目還未出場,繼續(xù)測試上一條
  // 二分法快速估算下一個嘗試位置
  const diff = itemOffset / estimatedItemHeight!
  sliceTo -= Math.ceil(diff / 2)
 }
 // slice 的時候,不含 end,所以 + 1
 sliceTo += 1
 return { sliceFrom, sliceTo }
}

以上就是計算可視區(qū)域的核心部分。完整的代碼,會在后續(xù)給出。

DOM 更新
由于我們是使用 Vue 來實(shí)現(xiàn)虛擬列表的,所以 DOM 的更新方面,可以省去大量繁瑣的細(xì)節(jié)管理。 我們只需要關(guān)心列表滾動到某處之后,如何計算出當(dāng)前視口應(yīng)該出現(xiàn)哪些條目即可。

盡管如此,考慮到滾動的流暢性,以及 IE11 等瀏覽器的 DOM 操作性能,我們不得不多做很多事情。

批量 DOM 操作

我們可以在 IE11 的開發(fā)者工具面板中看到,滾動過程,頻繁地往虛擬列表首尾插入、移除結(jié)點(diǎn),會帶來非常嚴(yán)重的性能問題。 所以,我們必須控制 DOM 操作的頻率。

批量可以部分解決這個問題。

具體的思路是,在滾動回調(diào)中,我們計算出可視區(qū)域的結(jié)點(diǎn)起止序號,不直接應(yīng)用,而是加上一個額外渲染的數(shù)量。 比如我們計算出當(dāng)前應(yīng)該渲染 20 ~ 30 這些條目,我們可以在前后各加上 10 個額外渲染的條目,即 10 ~ 40,這樣就一次性渲染了 30 個結(jié)點(diǎn)。在繼續(xù)滾動時,我們檢查新的起止范圍,是否還在 10 ~ 40 范圍內(nèi),如果是,我們就不做新的結(jié)點(diǎn)增刪操作。

核心實(shí)現(xiàn):

// 刷新局部渲染數(shù)據(jù)切片范圍
private _updateSliceRange(forceUpdate?: boolean) {
 // 上下方額外多渲染的條目波動量
 const COUNT = this._preRenderingCount()

 // 預(yù)渲染觸發(fā)閾值
 const THRESHOLD = this._preRenderingThreshold()  

 // 數(shù)據(jù)總量
 const MAX = this.dataView.length

 // 計算出準(zhǔn)確的切片區(qū)間
 const range = this._calcSliceRange()  

 // 檢查計算出來的切片范圍,是否被當(dāng)前已經(jīng)渲染的切片返回包含了
 // 如果是,無需更新切片,(如果 forceUpdate,則無論如何都需要重新切片)
 let fromThreshold = range.sliceFrom - THRESHOLD
 if (fromThreshold < 0) fromThreshold = 0
 let toThreshold = range.sliceTo + THRESHOLD
 if (toThreshold > MAX) toThreshold = MAX

 // 無需強(qiáng)制刷新,且上下兩端都沒有觸達(dá)閾值時,無需重新切片
 if (!forceUpdate && ((this.sliceFrom <= fromThreshold) && (this.sliceTo >= toThreshold))) {
  return
 }

 // 下面是更新切片的情況

 // 在切片區(qū)間頭部、尾部,追加預(yù)渲染的條目
 let { sliceFrom, sliceTo } = range
 sliceFrom = sliceFrom > COUNT ? sliceFrom - COUNT : 0
 sliceTo = sliceTo + COUNT > MAX ? MAX : sliceTo + COUNT

 this.sliceFrom = sliceFrom
 this.sliceTo = sliceTo
 if (forceUpdate) this._doSlice()
}

使用了這種批量操作之后,可以看到,正常的鼠標(biāo)滾動下,IE 也能比較順暢地滾動了。

事件

由于虛擬列表的 DOM 需要不停地生成和銷毀,因此,直接在列表項(xiàng)目上綁定事件是非常低效的。 所以,使用事件代理就成了很不錯的方案,將事件注冊在組件根結(jié)點(diǎn)上,再根據(jù) event.target 來區(qū)分是由哪個列表項(xiàng)冒泡出來的事件,即可高效處理。

組件實(shí)現(xiàn)

import { Component, Vue, Prop, Watch } from 'vue-property-decorator'
import { createSparseRangeList } from './SparseRangeList'
// 列表項(xiàng)數(shù)據(jù)包裹,data 字段存放原始數(shù)據(jù)
// 組件所有操作不應(yīng)該改變 data 的內(nèi)容,而是修改該包裹對象的屬性
class ItemWrapper {
 // 原始數(shù)據(jù)
 data: any
 // 數(shù)據(jù)唯一 key
 key: any
 // 條目高度
 // 1. 正數(shù)代表已經(jīng)計算出來的高度
 // 2. 0 代表未計算的高度,不顯示
 // 3. 負(fù)數(shù)代表需要隱藏的高度,絕對值為已經(jīng)計算出來的高度,方便取消隱藏
 height: number
 // 記錄是否已經(jīng)根據(jù)實(shí)際 DOM 計算過高度
 realHeight: boolean
 // 條目在當(dāng)前過濾視圖中的序號
 viewIndex: number
 constructor(data: any, key: any, height: number) {
  this.data = data
  // 數(shù)據(jù)的唯一id,是初始化數(shù)據(jù)時候的序號
  // 每次傳入的 data 改變,都會重新生成
  this.key = key
  // 條目的高度緩存
  // 1. 用于重建高度存儲時快速恢復(fù)
  // 2. 用于快速通過數(shù)據(jù)取高度
  this.height = height >> 0
  this.realHeight = false
  // 每次生成 dataView 都刷新
  this.viewIndex = -1
 }
}
@Component({ name: 'VList' })
export default class VList extends Vue {
 [key: string]: any
 // 高度存儲 不響應(yīng)式
 private itemHeightStore: any
 // 組件寬度,不設(shè)置則為容器的 100%
 @Prop({ type: Number })
 private width?: number
 // 組件高度,不設(shè)置則為容器的 100%
 @Prop({ type: Number })
 private height?: number
 // 傳入高度值,固定條目高度
 @Prop({ type: Number })
 private fixedItemHeight?: number
 // 預(yù)估元素高度,
 // 在高度不確定的列表中,未計算出高度時使用,
 // 該值與元素平均高度越相近,則越高效(修正時估算次數(shù)越少)
 @Prop({ type: Number, default: 30 })
 private estimatedItemHeight!: number
 // 數(shù)據(jù)列表
 @Prop({ type: Array, default: () => ([]) })
 private data!: any[]
 // 計算條目高度的方法
 @Prop({
  type: Function,
  default(node: Node, wrappedData: ItemWrapper) {
   return (node as HTMLElement).clientHeight
  }
 })
 private itemHeightMethod!: (node: Node, wrappedItem: ItemWrapper) => number
 // 數(shù)據(jù)過濾方法(可以用于外部實(shí)現(xiàn)搜索框過濾)
 @Prop({ type: Function })
 private filterMethod?: (data: any) => boolean
 // 數(shù)據(jù)排序方法(可以用于外部實(shí)現(xiàn)數(shù)據(jù)自定義過濾)
 @Prop({ type: Function })
 private sortMethod?: (a: any, b: any) => number
 // 包裹后的數(shù)據(jù)列表(必須 freeze,否則大列表性能撐不?。?
 private wrappedData: ReadonlyArray<ItemWrapper> = Object.freeze(this._wrapData(this.data))
 // 真實(shí)渲染上屏的數(shù)據(jù)列表切片
 private dataSlice: ReadonlyArray<ItemWrapper> = []
 // viewport 寬度
 private viewportWidth = this.width || 0
 // viewport 高度
 private viewportHeight = this.height || 0
 // 當(dāng)前 viewport 中第一條數(shù)據(jù)的序號
 private sliceFrom = 0
 // 當(dāng)前 viewport 中最后一條數(shù)據(jù)的序號
 private sliceTo = 0
 // 列表高度
 private listHeight = 0
 // 檢查是否固定高度模式
 private get isFixedHeight() {
  return this.fixedItemHeight! >= 0
 }
 // 獲取默認(rèn)條目高度
 private get defaultItemHeight() {
  return this.isFixedHeight ? this.fixedItemHeight! : this.estimatedItemHeight
 }
 // 當(dāng)前篩選條件下的數(shù)據(jù)列表
 // 依賴:wrappedData, filterMethod, sortMethod
 private get dataView() {
  const { wrappedData, filterMethod, sortMethod } = this
  let data = []
  if (typeof filterMethod === 'function') {
   const len = wrappedData.length
   for (let index = 0; index < len; index += 1) {
    const item = wrappedData[index]
    if (filterMethod(item.data)) {
     data.push(item)
    }
   }
  } else {
   data = wrappedData.map(i => i)
  }
  if (typeof sortMethod === 'function') {
   data.sort((a, b) => {
    return sortMethod(a, b)
   })
  }
  // 重新記錄數(shù)據(jù)在視圖中的位置,用于隱藏部分條目時,可以精確計算高度、坐標(biāo)
  const size = data.length
  for (let index = 0; index < size; index += 1) {
   const wrappedItem = data[index]
   wrappedItem.viewIndex = index
  }
  return Object.freeze(data)
 }
 // 原始列表數(shù)據(jù)變化,重新包裹數(shù)據(jù)
 @Watch('data')
 private onDataChange(data: any[]) {
  this.wrappedData = Object.freeze(this._wrapData(data))
 }
 // 當(dāng)前過濾、排序視圖變化,重新布局
 @Watch('dataView')
 private onDataViewChange(wrappedItems: ItemWrapper[]) {
  // 重建高度存儲
  const estimatedItemHeight = this.defaultItemHeight
  this.itemHeightStore = createSparseRangeList(wrappedItems.length, estimatedItemHeight)
  // 從緩存中快速恢復(fù)已計算出高度的條目的高度
  wrappedItems.forEach((wrappedItem, index) => {
   // 小于零的需要隱藏,所以高度為 0
   this.itemHeightStore.setValue(index,
    wrappedItem.height > 0 ? wrappedItem.height : 0)
  })
  // 刷新列表高度
  this.updateListHeight()
  // 重置滾動位置
  // TODO, 錨定元素
  const { viewport } = this.$refs as any
  if (viewport) viewport.scrollTop = 0
  // 重新切片當(dāng)前 viewport 需要的數(shù)據(jù)
  this._updateSliceRange(true)
  this.$emit('data-view-change', this.dataSlice.map((wrappedItem) => wrappedItem.data))
 }
 private created() {
  const estimatedItemHeight = this.defaultItemHeight
  this.itemHeightStore = createSparseRangeList(this.dataView.length, estimatedItemHeight)
  this.layoutObserver = new MutationObserver(this.redraw.bind(this))
  this.childObserver = new MutationObserver((mutations: MutationRecord[]) => {
   this._updateHeightWhenItemInserted(mutations)
  })
  this.$watch(((vm: any) => `${vm.sliceFrom},${vm.sliceTo}`) as any, this._doSlice)
 }
 private mounted() {
  this.redraw()
  this.layoutObserver.observe(this.$el, { attributes: true })
  // 非固定高度場景,監(jiān)聽子元素插入,提取高度
  if (!this.isFixedHeight) {
   this.childObserver.observe(this.$refs.content, { childList: true })
  }
 }
 private beforeDestory() {
  this.layoutObserver.disconnect()
  if (!this.isFixedHeight) {
   this.childObserver.disconnect()
  }
  this.itemHeightStore = null
 }
 // DOM 結(jié)構(gòu)比較簡單,無需 template,直接使用渲染函數(shù)輸出 VDOM
 private render(createElement: any) {
  return createElement(
   'div', // 組件容器,與外部布局
   {
    class: 'VList',
    style: {
     'box-sizing': 'border-box',
     display: 'inline-block',
     margin: '0',
     padding: '0',
     width: this.width ? this.width + 'px' : '100%',
     height: this.height ? this.height + 'px' : '100%',
    }
   },
   [
    createElement(
     'div', // 滾動區(qū)域的可見范圍
     {
      ref: 'viewport',
      class: 'VList_viewport',
      style:
       'box-sizing:border-box;position:relative;overflow:hidden;width:100%;height:100%;margin:0;padding:0;overflow:auto;overflow-scrolling:touch;',
      on: { scroll: this._onScroll }
     },
     [
      createElement(
       'div', // 內(nèi)容容器,內(nèi)容真實(shí)高度由此容器體現(xiàn)
       {
        class: 'VList_scollable',
        ref: 'content',
        style: {
         'box-sizing': 'border-box',
         position: 'relative',
         margin: '0',
         padding: '0',
         height: this.listHeight + 'px'
        }
       },
       // 列表項(xiàng)
       this.dataSlice.map((wrappedItem) => {
        return createElement(
         'div',
         {
          key: wrappedItem.key,
          class: `VList_item VList_item-${wrappedItem.key % 2 === 0 ? 'even' : 'odd'}`,
          attrs: {
           'data-key': wrappedItem.key
          },
          style: {
           'box-sizing': 'border-box',
           'z-index': '1',
           position: 'absolute',
           right: '0',
           bottom: 'auto',
           left: '0',
           margin: '0',
           padding: '0',
           cursor: 'default',
           // 注:使用 transfrom 有黑屏 bug
           // transform: `translate(0, ${top})`
           // transform: `translate3d(0, ${top}, 0)`
           top: this._top(wrappedItem.viewIndex) + 'px'
          }
         },
         // 將原始數(shù)據(jù),key 注入到 slot 里,
         // 以便自定義條目內(nèi)容使用
         this.$scopedSlots.default!({
          item: wrappedItem.data,
          listKey: wrappedItem.key
         })
        )
       })
      )
     ]
    )
   ]
  )
 }
 // 重繪界面,確保列表渲染正確
 public redraw() {
  const viewport = this.$refs.viewport as HTMLElement
  const { clientWidth, clientHeight } = viewport
  this.viewportWidth = clientWidth
  this.viewportHeight = clientHeight
  this.updateListHeight()
  this._updateSliceRange(true)
 }
 // 刷新列表總高度
 public updateListHeight() {
  const { itemHeightStore } = this
  const rangeValues = itemHeightStore.values()
  if (!rangeValues.length) {
   this.listHeight = 0
   return
  }
  const listHeight = rangeValues.reduce((sum: number, rangeValue: any) => {
   const span = rangeValue.end - rangeValue.start + 1
   const height = rangeValue.value * span
   return sum + height
  }, 0)
  this.listHeight = listHeight
 }
 // Dom 插入時候,計算高度,然后
 // 批量刷新高度,避免頻繁調(diào)整列表高度帶來性能問題
 public batchUpdateHeight(records: Array<{ wrappedItem: ItemWrapper, height: number }>) {
  records.forEach(({ wrappedItem, height }) => {
   this._updateHeight(wrappedItem, height, true)
  })
  this.updateListHeight()
  this._updateSliceRange()
 }
 // 通過數(shù)據(jù) key,設(shè)置對應(yīng)條目的高度
 public updateHeightByKey(key: any, height: number) {
  const wrappedItem = this.wrappedData[key]
  if (!wrappedItem) return
  this._updateHeight(wrappedItem, height)
  this.updateListHeight()
  this._updateSliceRange()
 }
 // 通過數(shù)據(jù) key,設(shè)置對應(yīng)條目的顯示狀態(tài)
 public showByKey(key: any) {
  const wrappedItem = this.wrappedData[key]
  if (!wrappedItem) return
  if (wrappedItem.height <= 0) {
   const height = -wrappedItem.height || this.defaultItemHeight
   this._updateHeight(wrappedItem, height!)
   this.updateListHeight()
   this._updateSliceRange()
   // 強(qiáng)制重繪
   this._doSlice()
  }
 }
 // 通過數(shù)據(jù) key,設(shè)置對應(yīng)條目的顯示狀態(tài)
 public hideByKey(key: any) {
  const wrappedItem = this.wrappedData[key]
  if (!wrappedItem) return
  if (wrappedItem.height > 0) {
   const height = -wrappedItem.height
   wrappedItem.height = height
   this._updateHeight(wrappedItem, height)
   this.updateListHeight()
   // 強(qiáng)制重繪
   this._updateSliceRange(true)
  }
 }
 // 通過數(shù)據(jù) key 列表,設(shè)置對應(yīng)條目的顯示狀態(tài)
 public showByKeys(keys: any[]) {
  const wrappedItems = keys.map((key) => this.wrappedData[key])
   .filter((wrappedItem) => wrappedItem && wrappedItem.height <= 0)
  wrappedItems.forEach((wrappedItem) => {
   const height = (-wrappedItem.height || this.defaultItemHeight)!
   this._updateHeight(wrappedItem, height)
  })
  this.updateListHeight()
  // 強(qiáng)制重繪
  this._updateSliceRange(true)
 }
 // 通過數(shù)據(jù) key 列表,設(shè)置對應(yīng)條目的顯示狀態(tài)
 public hideByKeys(keys: any[]) {
  const wrappedItems = keys.map((key) => this.wrappedData[key])
   .filter(wrappedItem => wrappedItem && wrappedItem.height > 0)
  wrappedItems.forEach((wrappedItem) => {
   // 設(shè)置為負(fù)數(shù),表示隱藏
   const height = -wrappedItem.height
   wrappedItem.height = height
   this._updateHeight(wrappedItem, height)
  })
  this.updateListHeight()
  // 強(qiáng)制重繪
  this._updateSliceRange(true)
 }
 // 內(nèi)部方法,計算局部渲染數(shù)據(jù)切片的起止點(diǎn)
 private _calcSliceRange() {
  if (!this.dataView.length) {
   return { sliceFrom: 0, sliceTo: 0 }
  }
  // 數(shù)據(jù)總量
  const MAX = this.dataView.length
  // 視口上邊界
  const viewportTop = (this.$refs.viewport as any).scrollTop || 0
  // 視口下邊界
  const viewportBottom = viewportTop + this.viewportHeight
  // 預(yù)估條目高度
  const estimatedItemHeight = this.defaultItemHeight
  // 從估算值開始計算起始序號
  let sliceFrom = Math.floor(viewportTop / estimatedItemHeight!)
  if (sliceFrom > MAX - 1) sliceFrom = MAX - 1
  while (sliceFrom >= 0 && sliceFrom <= MAX - 1) {
   const itemTop = this._top(sliceFrom)
   // 條目頂部相對于 viewport 頂部的偏移
   const itemOffset = itemTop - viewportTop
   // 1. 該條目距離視口頂部有距離,說明上方還有條目元素需要顯示,繼續(xù)測試上一條
   if (itemOffset > 0) {
    // 二分法快速估算下一個嘗試位置
    const diff = itemOffset / estimatedItemHeight!
    sliceFrom -= Math.ceil(diff / 2)
    continue
   }
   // 2. 恰好顯示該條目的頂部,則該條目為本次視口的首條元素
   if (itemOffset === 0) break
   // 以下都是 itemOffset < 0
   const itemHeight = this._itemHeight(sliceFrom)
   // 3. 該條目在頂部露出了一部分,則該條目為本次視口的首條元素
   if (itemOffset < itemHeight) break
   // 4. 該條目已被滾出去視口,繼續(xù)測試下一條
   // 二分法快速估算下一個嘗試位置
   const diff = -itemOffset / estimatedItemHeight!
   sliceFrom += Math.ceil(diff / 2)
  }
  // 從估算值開始計算結(jié)束序號
  let sliceTo = sliceFrom + 1 + Math.floor(this.viewportHeight / estimatedItemHeight!)
  if (sliceTo > MAX) sliceTo = MAX
  while (sliceTo > sliceFrom && sliceTo <= MAX) {
   const itemTop = this._top(sliceTo)
   const itemHeight = this._itemHeight(sliceTo)
   const itemBottom = itemTop + itemHeight
   // 條目底部相對于 viewport 底部的偏移
   const itemOffset = itemBottom - viewportBottom
   // 1. 該條目的底部距離視口底部有距離,說明下方還有條目元素需要顯示,繼續(xù)測試下一條
   if (itemOffset < 0) {
    // 二分法快速估算下一個嘗試位置
    const diff = -itemOffset / estimatedItemHeight!
    sliceTo += Math.ceil(diff / 2)
    continue
   }
   // 2. 恰好顯示該條目的底部,則該條目為視口中最后一項(xiàng)
   if (itemOffset === 0) break
   // 3. 該條目在底部被裁剪了一部分,則該條目為本次視口的末項(xiàng)
   if (itemOffset < itemHeight) break
   // 該條目還未出場,繼續(xù)測試上一條
   // 二分法快速估算下一個嘗試位置
   const diff = itemOffset / estimatedItemHeight!
   sliceTo -= Math.ceil(diff / 2)
  }
  // slice 的時候,不含 end,所以 + 1
  sliceTo += 1
  return { sliceFrom, sliceTo }
 }
 // 上下兩端預(yù)先批量渲染的項(xiàng)目波動量
 // 原理是,每次插入刪除都是一個小批量動作,
 // 而不是每次只插入一條、銷毀一條
 // 計算出的局部渲染數(shù)據(jù)范圍,跟上一次計算出來的結(jié)果,差距
 // 在這個波動量范圍內(nèi),則不重新切片渲染,用于
 // 防止 IE 11 頻繁插入內(nèi)容導(dǎo)致性能壓力
 private _preRenderingCount() {
  // 默認(rèn)預(yù)渲染 2 屏
  return Math.ceil(this.viewportHeight / this.defaultItemHeight!) * 2
 }
 // 滾動到上下方剩下多少個條目時,加載下一批
 // 緩解 Macbook & iOS 觸摸滾動時的白屏
 private _preRenderingThreshold() {
  // 默認(rèn)觸達(dá)預(yù)渲染的一半數(shù)量時,加載下一批切片
  return Math.floor(this._preRenderingCount() / 2)
 }
 // 刷新局部渲染數(shù)據(jù)切片范圍
 private _updateSliceRange(forceUpdate?: boolean) {
  // 上下方額外多渲染的條目波動量
  const COUNT = this._preRenderingCount()
  // 預(yù)渲染觸發(fā)閾值
  const THRESHOLD = this._preRenderingThreshold()  
  // 數(shù)據(jù)總量
  const MAX = this.dataView.length
  // 計算出準(zhǔn)確的切片區(qū)間
  const range = this._calcSliceRange()  
  // 檢查計算出來的切片范圍,是否被當(dāng)前已經(jīng)渲染的切片返回包含了
  // 如果是,無需更新切片,(如果 forceUpdate,則無論如何都需要重新切片)
  let fromThreshold = range.sliceFrom - THRESHOLD
  if (fromThreshold < 0) fromThreshold = 0
  let toThreshold = range.sliceTo + THRESHOLD
  if (toThreshold > MAX) toThreshold = MAX
  // 無需強(qiáng)制刷新,且上下兩端都沒有觸達(dá)閾值時,無需重新切片
  if (!forceUpdate && ((this.sliceFrom <= fromThreshold) && (this.sliceTo >= toThreshold))) {
   return
  }
  // 更新切片的情況
  // 在切片區(qū)間頭部、尾部,追加預(yù)渲染的條目
  let { sliceFrom, sliceTo } = range
  sliceFrom = sliceFrom > COUNT ? sliceFrom - COUNT : 0
  sliceTo = sliceTo + COUNT > MAX ? MAX : sliceTo + COUNT
  this.sliceFrom = sliceFrom
  this.sliceTo = sliceTo
  if (forceUpdate) this._doSlice()
 }
 // 當(dāng)前需要渲染的數(shù)據(jù)切片
 private _doSlice() {
  const { dataView, sliceFrom, sliceTo } = this
  const slice = dataView.slice(sliceFrom, sliceTo)
   .filter((wrappedItem) => wrappedItem.height > 0)
  this.dataSlice = Object.freeze(slice)
  this.$emit('slice', slice.map((wrappedItem) => wrappedItem.data))
 }
 // `index` 數(shù)據(jù)在 dataView 中的 index
 private _itemHeight(index: number): number {
  return this.itemHeightStore.getValueAt(index)
 }
 // `index` 數(shù)據(jù)在 dataView 中的 index
 private _top(index: number): number {
  if (index === 0) return 0
  // 0 ~ 上一項(xiàng)的高度累加
  const rangeValues = this.itemHeightStore.intersecting(0, index - 1)
  const sumHeight = rangeValues.reduce((sum: number, rangeValue: any) => {
   const span = rangeValue.end - rangeValue.start + 1
   return sum + rangeValue.value * span
  }, 0)
  return sumHeight
 }
 // 包裹原始數(shù)據(jù)列表
 private _wrapData(list: any[]): ItemWrapper[] {
  return list.map((item, index) => new ItemWrapper(item, index, this.defaultItemHeight!))
 }
 // 通過 DOM Node 獲取對應(yīng)的數(shù)據(jù)
 private _getDataByNode(node: Node): ItemWrapper {
  return this.wrappedData[(node as any).dataset.key]
 }
 // 刷新列表項(xiàng)高度
 private _updateHeight(wrappedItem: ItemWrapper, height: number, isRealHeight?: boolean) {
  height = height >> 0
  // 更新結(jié)點(diǎn)高度緩存
  wrappedItem.height = height
  if (isRealHeight) {
   wrappedItem.realHeight = true
  }
  // 如果 wrappedItem 為當(dāng)前過濾下的項(xiàng)目,
  // 則同時刷新高度存儲 store
  const index = this.dataView.indexOf(wrappedItem)
  if (index !== -1) {
   // 小于等于零表示折疊不顯示,計算高度為零
   // 負(fù)值存在 wrappedItem 中,用于反折疊時恢復(fù)
   this.itemHeightStore.setValue(index, height > 0 ? height : 0)
  }
 }
 // 節(jié)點(diǎn)插入時,檢查是否首次插入,如果是,計算高度并更新對應(yīng)的 ItemWrapper
 private _updateHeightWhenItemInserted(mutations: MutationRecord[]) {
  const addedNodes: Node[] = mutations
   .map((mutation: MutationRecord) => mutation.addedNodes)
   .reduce((result: any, items: NodeList) => {
    result.push(...items)
    return result
   }, [])
  const batch: Array<{ wrappedItem: ItemWrapper, height: number }> = []
  addedNodes.forEach((node: Node) => {
   const wrappedItem = this._getDataByNode(node)
   // 如果 wrappedItem 中已經(jīng)存儲了計算過的高度,
   // 則直接返回,不訪問 clientHeight
   // 以避免性能開銷(IE 11 中訪問 clientHeight 性能非常差)
   if (wrappedItem.realHeight) {
    return
   }
   const height = this.itemHeightMethod(node, wrappedItem) >> 0
   if (wrappedItem.height !== height) {
    batch.push({ wrappedItem, height })
   } else {
    // 計算出來的高度跟默認(rèn)值一致,
    // 則無需更新,但是設(shè)置已經(jīng)計算狀態(tài)
    // 以便下次可以直接使用緩存
    wrappedItem.realHeight = true
   }
  })
  if (batch.length) {
   this.batchUpdateHeight(batch)
  }
 }
 // 滾動事件處理器
 private _onScroll() {
  this._updateSliceRange()
 }
}

總結(jié)

以上所說是小白給大家介紹的使用 Vue 實(shí)現(xiàn)一個虛擬列表的方法,希望對大家有所幫助,如果大家有任何疑問歡迎給我留言,小編會及時回復(fù)大家的!

相關(guān)文章

  • vue項(xiàng)目引入本地bootstrap不生效問題及解決

    vue項(xiàng)目引入本地bootstrap不生效問題及解決

    這篇文章主要介紹了vue項(xiàng)目引入本地bootstrap不生效問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-10-10
  • Element基于el-input數(shù)字范圍輸入框的實(shí)現(xiàn)

    Element基于el-input數(shù)字范圍輸入框的實(shí)現(xiàn)

    本文主要介紹了?Element基于el-input數(shù)字范圍輸入框的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • VUE多個下拉框?qū)崿F(xiàn)雙向聯(lián)動效果

    VUE多個下拉框?qū)崿F(xiàn)雙向聯(lián)動效果

    這篇文章主要為大家詳細(xì)介紹了VUE多個下拉框?qū)崿F(xiàn)雙向聯(lián)動效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-07-07
  • VUE+node(express)實(shí)現(xiàn)前后端分離

    VUE+node(express)實(shí)現(xiàn)前后端分離

    在本篇文章里小編給大家分享的是關(guān)于VUE+node(express)前后端分離實(shí)例內(nèi)容,有需要的朋友們參考下。
    2019-10-10
  • vue使用cesium創(chuàng)建數(shù)據(jù)白模方式

    vue使用cesium創(chuàng)建數(shù)據(jù)白模方式

    這篇文章主要介紹了vue使用cesium創(chuàng)建數(shù)據(jù)白模方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-10-10
  • vue實(shí)現(xiàn)四級導(dǎo)航及驗(yàn)證碼的方法實(shí)例

    vue實(shí)現(xiàn)四級導(dǎo)航及驗(yàn)證碼的方法實(shí)例

    我們在做項(xiàng)目經(jīng)常會遇到多級導(dǎo)航這個需求,所以下面這篇文章主要給大家介紹了關(guān)于vue實(shí)現(xiàn)四級導(dǎo)航及驗(yàn)證碼的相關(guān)資料,文章通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2021-07-07
  • vue.js實(shí)現(xiàn)簡單輪播圖效果

    vue.js實(shí)現(xiàn)簡單輪播圖效果

    這篇文章主要為大家詳細(xì)介紹了vue.js實(shí)現(xiàn)簡單輪播圖效果的相關(guān)代碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-10-10
  • 淺析Vue實(shí)例以及生命周期

    淺析Vue實(shí)例以及生命周期

    這篇文章給大家分享了Vue實(shí)例以及生命周期的相關(guān)知識點(diǎn)內(nèi)容,有興趣的朋友們可以學(xué)習(xí)下。
    2018-08-08
  • Vue-router路由判斷頁面未登錄跳轉(zhuǎn)到登錄頁面的實(shí)例

    Vue-router路由判斷頁面未登錄跳轉(zhuǎn)到登錄頁面的實(shí)例

    下面小編就為大家?guī)硪黄猇ue-router路由判斷頁面未登錄跳轉(zhuǎn)到登錄頁面的實(shí)例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-10-10
  • Vue.js之render函數(shù)使用詳解

    Vue.js之render函數(shù)使用詳解

    這篇文章主要介紹了Vue.js之render函數(shù)使用詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-09-09

最新評論