響應式的 switchboard:讓又大又慢的Vue/AIpine 頁面爆快

2023-04-30 18:00:51

我的提示: AIpine 是一個js 庫,官網口號是 「一個新的輕量極javascript框架」,其實我之前也沒接觸過,翻譯這篇文章時才注意到

官方地址: [AIpine.js]https://alpinejs.dev


下面開始是譯文:

小提示: 在這篇文章中我將使用Vue/AIpine 術語 ,但是我認為此模式可以應用於更多不同的語言框架

前段時間我就碰到了數千行的超大表格。每一行都是單獨的 AIpine 元件, 你可以通過點選啟用它(css會顯示高亮)。如果你點選了另一行,那麼前一行啟用狀態就會處理非啟用狀態,新行則是啟用狀態。

問題就是:啟用某一個單行居然差不多要耗時整整一秒鐘

此類效能問題幾乎讓這整個效果無法使用,特別是在用鍵盤操作導航至單元格時。

所以 ,我用一個載入了1萬行的頁面去測試尋找以找到儘可能的提高效能方法。 不久, 我想出了一個簡潔的模式讓頁面可以立即更新狀態。我稱它為: Switchboard 模式

下面展示展示…

在此例子中,我不打算用AIpine 。取而代之的是AIpine 底層用到的 vue 提供的一些通用響應式方法。 如果你對 「watchEffect」 和 「ref」 不太熟悉,通過後面的程式碼片斷你應該能憑直覺就知道它們的用法,如果還是不知道,那麼 api 檔案就在這裡檢視

假定給我們一個擁有1萬行的表格,下面簡單的程式碼會在頁面載入時高亮當前啟用的 activeRow

let activeRow = 12

document.querySelectorAll('tr').forEach((row) => {
    if (row.id === activeRow) {
        row.classList.add('active')
    } else {
        row.classList.remove('active')
    }
})

現在,當不同行被點選時,我們可以給行新增點選事件來設定新的高亮行

let activeRow = 12

document.querySelectorAll('tr').forEach((row) => {
    row.addEventListener('click', () => {
        activeRow = row.id
    })

    if (row.id === activeRow) {
        row.classList.add('active')
    } else {
        row.classList.remove('active')
    }
})

以上程式碼的問題是,當一個行被點選,當前啟用行會更新,但在視覺上我們看不到任何變化。

下面展示了我們可以使用 「reactivity」 讓當activeRow 發生變化時,所有行自己觸發自身的更新:

import { ref, watchEffect } from 'vue'

let activeRow = ref(12)

document.querySelectorAll('tr').forEach((row) => {
    row.addEventListener('click', () => {
        activeRow.value = row.id
    })

    watchEffect(() => {
        if (row.id === activeRow.value) {
            row.classList.add('active')
        } else {
            row.classList.remove('active')
        }
    })
})

上面的程式碼片斷做了這麼幾件事

  • 用ref 包裹 activeRow 變數,從而使得它可以被響應依賴追蹤
  • 迴圈1萬行,新增點選事件,用於改變響應式的 actievRow 變數
  • 註冊一個響應式副作用 watchEffect 它會在任意響應依賴變更時重新執行(此處是 activeRow )

這是為何AIpine ( 或Vue 也類似,以我的知識範圍內的理解來講 )能在底層成功工作的原理,如果你要渲染1萬行元件,它們全部依賴一個響應式狀態比如: 「activerRow」

現在,當某個使用者點選某行,那麼被點選行將變成 active 其它行自動變成 deactivated

問題是: 頁面更新超級變

為什麼 ?因為每當activeRow變數發生變化時, 1萬個watchEffect 回撥會被執行。

大多數 app中, 宣告一個狀態,然後它被子元件,這不成問題。 然而,如果你你正在建立非常多的元件(或「效果」),除了被activeRow狀態影響的相關的兩行外,其它9998 完全不需要關心狀態變更, 這非常低效。

解決方案: 一個響應式的switchboard

響應式 switchboard 這術語是我現在為這個概念創造的。 非常有可能這個模式也許已經有了其它的名稱,但是,管它呢…

在當前設定中,我們有單個一個狀態,和1萬個依賴於此狀態的地方。

假如換成一個單獨狀態,和1萬個不同預存的值(和上面一樣),我們擁有了1萬個不同的狀態,每個狀態是一個布林值,代表了每個預設值。舉個栗子:

// Before
let activeRow = ref(4)

// After
let rowStates = {
    1: ref(false),
    2: ref(false),
    3: ref(false),
    4: ref(true),
    5: ref(false),
    ...
}

讓我們稍變動一下上面例子的程式碼來使用此模式:

import { ref, watchEffect } from 'vue'

let rowStates = {}

document.querySelectorAll('tr').forEach((row) => {
    rowStates[row.id] = ref(false)

    row.addEventListener('click', () => {
        rowStates[row.id].value = true
    })

    watchEffect(() => {
        if (rowStates[row.id].value) {
            row.classList.add('active')
        } else {
            row.classList.remove('active')
        }
    })
})

好了,現在你能看到, 不同於activeRow儲存單一的row ID, 我們使用 rowStates 來儲存1萬條資料,每條key就是row ID, 每條資料值就是一個響應式的布林值,代表了當前行是否處於啟用狀態

這行的通且超級快,現在,由於只點選一行,只有被點選的當前行狀態會變更狀態(不會影響到其它9999行)

不過還有一個問題 之前 因為 activeRow 只包含參照一個值,相同時間當前只有一個行被允許啟用。 前一個行會自動變更為非啟用態,因為每行都會自動重新計算。

在這個例子中,「非啟用過程」沒有觸發。 為了讓行擁有非啟用態,我們需要在rowStates裡找到它並標記它的值為false

讓我們新增一丟丟程式碼來實現它:

import { ref, watchEffect } from 'vue'

let rowStates = {}

document.querySelectorAll('tr').forEach((row) => {
    rowStates[row.id] = ref(false)

    row.addEventListener('click', () => {
        // Deactivate the old row...
        for (id in rowStates) {
            if (rowStates[id].value === true) {
                rowStates[id].value = false
                return
            }
        }

        rowStates[row.id].value = true
    })

    watchEffect(() => {
        if (rowStates[row.id].value) {
            row.classList.add('active')
        } else {
            row.classList.remove('active')
        }
    })
})

正如你所看到的,我們新增了一丟丟程式碼在點選事件內,迴圈全部的行並設定為非啟用態

現在我們加上了非啟用態功能 ,但我們的程式碼依然不高效,每次行被點選,就需要回圈rowStates物件的1萬項

結果是我們回頭在之前優化中使用過的,通過新增一點資料來儲存當前啟用的行ID。 它類似於基礎的暫存,使得我們無需再使用迴圈了:

import { ref, watchEffect } from 'vue'

let rowStates = {}
let activeRow

document.querySelectorAll('tr').forEach((row) => {
    rowStates[row.id] = ref(false)

    row.addEventListener('click', () => {
        if (activeRow) rowStates[activeRow].value = false

        activeRow = row.id

        rowStates[row.id].value = true
    })

    watchEffect(() => {
        if (rowStates[row.id].value) {
            row.classList.add('active')
        } else {
            row.classList.remove('active')
        }
    })
})

得了,現在我們新增 了activeRow 變數,我們搞定了完美高效更新

接近完美,但感覺還差點意思,如果我們能簡單抽象一下讓我們少做一些跑腿的活。

小的函數 switchboard 它包含了一個值,並返回一些通用函數用於存取和變更這個值

import { watchEffect } from 'vue'
import { switchboard } from 'reactive-switchboard' // Heads up: this isn't on NPM

let { set: activate, is: isActive } = switchboard(12)

document.querySelectorAll('tr').forEach((row) => {
    row.addEventListener('click', () => {
        activate(row.id)
    })

    watchEffect(() => {
        if (isActive(row.id)) {
            row.classList.add('active')
        } else {
            row.classList.remove('active')
        }
    })
})

現在通過小小的switchboard 函數, 我們擁有了和之前一樣潔淨的程式碼,並且擁有超高效的效能

這裡是全部的 switchboard API

import { switchboard } from 'reactive-switchboard' // Heads up: this isn't on NPM

let { get, set, is } = switchboard(12)

// get() returns "12" in this case (non-reactively)
// set(10) sets the internal value to 10
// is(10) runs a reactive comparison to the underlying value

這對於追蹤類似於active啟用態超級有用,因為只有一個啟用狀態值了

我也找到了類似的需求,在追蹤 ‘selected’ 狀態時,這需要多個狀態

對於這些需求,我新建了一個通用方法 switchboardSet 它擁有類似 Set 物件的API (可能有更好的名字,但管它呢…)

import { switchboardSet } from 'reactive-switchboard' // Heads up: this isn't on NPM

let { get, add, remove, has, clear } = switchboardSet([12])

// get() returns [12] (non-reactively)
// add(10) sets the internal array to [12, 10]
// remove(10) sets the array back to [12]
// has(12) returns a reactive boolean
// clear() reactively clears the internal array: []

老弟你行了,發現問題,找到解決方法,並抽象它。

我把switchboard原始碼放到 github上了

自取!

英文原文連結
https://calebporzio.com/reactive-switchboard


轉載入註明部落格園 王二狗Sheldon
Email: [email protected]
https://github.com/willian12345