2

响应式的 switchboard:让又大又慢的Vue/AIpine 页面爆快 - 王二狗Sheldon

 1 year ago
source link: https://www.cnblogs.com/willian/p/17365367.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

响应式的 switchboard:让又大又慢的Vue/AIpine 页面爆快


我的提示: 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



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK