我的提示: 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')
}
})
})
上面的程式碼片斷做了這麼幾件事
這是為何AIpine ( 或Vue 也類似,以我的知識範圍內的理解來講 )能在底層成功工作的原理,如果你要渲染1萬行元件,它們全部依賴一個響應式狀態比如: 「activerRow」
現在,當某個使用者點選某行,那麼被點選行將變成 active 其它行自動變成 deactivated
問題是: 頁面更新超級變
為什麼 ?因為每當activeRow變數發生變化時, 1萬個watchEffect 回撥會被執行。
大多數 app中, 宣告一個狀態,然後它被子元件,這不成問題。 然而,如果你你正在建立非常多的元件(或「效果」),除了被activeRow狀態影響的相關的兩行外,其它9998 完全不需要關心狀態變更, 這非常低效。
響應式 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