<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ?一頓操作,我把 Table 組件性能提升了十倍

          共 16370字,需瀏覽 33分鐘

           ·

          2021-09-19 04:23

          背景

          Table 表格組件在 Web 開(kāi)發(fā)中的應(yīng)用隨處可見(jiàn),不過(guò)當(dāng)表格數(shù)據(jù)量大后,伴隨而來(lái)的是性能問(wèn)題:渲染的 DOM 太多,渲染和交互都會(huì)有一定程度的卡頓。

          通常,我們有兩種優(yōu)化表格的方式:一種是分頁(yè),另一種是虛擬滾動(dòng)。這兩種方式的優(yōu)化思路都是減少 DOM 渲染的數(shù)量。在我們公司的項(xiàng)目中,會(huì)選擇分頁(yè)的方式,因?yàn)樘摂M滾動(dòng)不能正確的讀出行的數(shù)量,會(huì)有 Accessibility 的問(wèn)題。

          記得 19 年的時(shí)候,我在 Zoom 已經(jīng)推行了基于 Vue.js 的前后端分離的優(yōu)化方案,并且基于 ElementUI 組件庫(kù)開(kāi)發(fā)了 ZoomUI。其中我們?cè)谥貥?gòu)用戶管理頁(yè)面的時(shí)候使用了 ZoomUI 的 Table 組件替換了之前老的用 jQuery 開(kāi)發(fā)的 Table 組件。

          因?yàn)榻^大部分場(chǎng)景 Table 組件都是分頁(yè)的,所以并不會(huì)有性能問(wèn)題。但是在某個(gè)特殊場(chǎng)景下:基于關(guān)鍵詞的搜索,可能會(huì)出現(xiàn) 200 * 20 條結(jié)果且不分頁(yè)的情況,且表格是有一列是帶有 checkbox 的,也就是可以選中某些行進(jìn)行操作。

          當(dāng)我們?nèi)c(diǎn)選其中一行時(shí),發(fā)現(xiàn)過(guò)了好久才選中,有明顯的卡頓感,而之前的 jQuery 版本卻沒(méi)有這類(lèi)問(wèn)題,這一比較令人大跌眼鏡。難道好好的技術(shù)重構(gòu),卻要犧牲用戶體驗(yàn)嗎?

          Table 組件第一次優(yōu)化嘗試

          既然有性能問(wèn)題,那么我們的第一時(shí)間的思路應(yīng)該是要找出產(chǎn)生性能問(wèn)題的原因。

          列展示優(yōu)化

          首先,ZoomUI 渲染的 DOM 數(shù)量是要多于 jQuery 渲染的 Table 的,因此第一個(gè)思考方向是讓 Table 組件盡可能地減少 DOM 的渲染數(shù)量

          20 列數(shù)據(jù)通常在屏幕下是展示不全的,老的 jQuery Table 實(shí)現(xiàn)很簡(jiǎn)單,底部有滾動(dòng)條,而 ZoomUI 在這種列可滾動(dòng)的場(chǎng)景下,支持了左右列的固定,這樣在左右滑動(dòng)過(guò)程中,可以固定某些列一直展示,用戶體驗(yàn)更好,但這樣的實(shí)現(xiàn)是有一定代價(jià)的。

          想要實(shí)現(xiàn)這種固定列的布局,ElementUI 用了 6 個(gè) table 標(biāo)簽來(lái)實(shí)現(xiàn),那么為什么需要 6 個(gè) table 標(biāo)簽?zāi)兀?/p>

          首先,為了讓 Table 組件支持豐富的表頭功能,表頭和表體都是各自用一個(gè) table 標(biāo)簽來(lái)實(shí)現(xiàn)。因此對(duì)于一個(gè)表格來(lái)說(shuō),就會(huì)有 2 個(gè) table 標(biāo)簽,那么再加上左側(cè) fixed 的表格,和右側(cè) fixed 的表格,總共有 6 個(gè) table 標(biāo)簽。

          在 ElementUI 實(shí)現(xiàn)中,左側(cè) fixed 表格和右側(cè) fixed 表格從 DOM 上都渲染了完整的列,然后從樣式上控制它們的顯隱:


          但這么實(shí)現(xiàn)是有性能浪費(fèi)的,因?yàn)橥耆恍枰秩具@么多列,實(shí)際上只需要渲染固定展示的列的 DOM,然后做好高度同步即可。ZoomUI 就是這么實(shí)現(xiàn)的,效果如下:

          當(dāng)然,僅僅減少 fixed 表格渲染的列,性能的提升還不夠明顯,有沒(méi)有辦法在列的渲染這個(gè)維度繼續(xù)優(yōu)化呢?

          這就是從業(yè)務(wù)層面的優(yōu)化了,對(duì)于一個(gè) 20 列的表格,往往關(guān)鍵的列并沒(méi)有多少,那么我們可不可以初次渲染僅僅渲染關(guān)鍵的列,其它列通過(guò)配置方式的渲染呢?

          根據(jù)上述需求,我給 Table 組件添加了如下功能:

          Table 組件新增一個(gè) initDisplayedColumn 屬性,通過(guò)它可以配置初次渲染的列,同時(shí)當(dāng)用戶修改了初次渲染的列,會(huì)在前端存儲(chǔ)下來(lái),便于下一次的渲染。

          通過(guò)這種方式,我們就可以少渲染一些列。顯然,列渲染少了,表格整體渲染的 DOM 數(shù)就會(huì)變少,對(duì)性能也會(huì)有一定的提升。

          更新渲染的優(yōu)化

          當(dāng)然,僅僅通過(guò)優(yōu)化列的渲染還是不夠的,我們遇到的問(wèn)題是當(dāng)點(diǎn)選某一行引起的渲染卡頓,為什么會(huì)引起卡頓呢?

          為了定位該問(wèn)題,我用 Table 組件創(chuàng)建了一個(gè) 1000 * 7 的表格,開(kāi)啟了 Chrome 的 Performance 面板記錄 checkbox 點(diǎn)選前后的性能。

          在經(jīng)過(guò)幾次 checkbox 選擇框的點(diǎn)選后,可以看到如下火焰圖:

          其中黃色部分是 Scripting 腳本的執(zhí)行時(shí)間,紫色部分是 Rendering 所占的時(shí)間。我們?cè)俳厝∫淮胃碌倪^(guò)程:

          然后觀察 JS 腳本執(zhí)行的 Call Tree,發(fā)現(xiàn)時(shí)間主要花在了 Table 組件的更新渲染上

          我們發(fā)現(xiàn)組件的 render to vnode 花費(fèi)的時(shí)間約 600ms;vnode patch to DOM 花費(fèi)的時(shí)間約 160ms。

          為什么會(huì)需要這么長(zhǎng)時(shí)間呢,因?yàn)辄c(diǎn)選了 checkbox,在組件內(nèi)部修改了其維護(hù)的選中狀態(tài)數(shù)據(jù),而整個(gè)組件的 render 過(guò)程中又訪問(wèn)了這個(gè)狀態(tài)數(shù)據(jù),因此當(dāng)這個(gè)數(shù)據(jù)修改后,會(huì)引發(fā)整個(gè)組件的重新渲染。

          而又由于有 1000 * 7 條數(shù)據(jù),因此整個(gè)表格需要循環(huán) 1000 * 7 次去創(chuàng)建最內(nèi)部的 td,整個(gè)過(guò)程就會(huì)耗時(shí)較長(zhǎng)。

          那么循環(huán)的內(nèi)部是不是有優(yōu)化的空間呢?對(duì)于 ElementUI 的 Table 組件,這里有非常大的優(yōu)化空間。

          其實(shí)優(yōu)化思路主要參考我之前寫(xiě)的 《揭秘 Vue.js 九個(gè)性能優(yōu)化技巧》 其中的 Local variables 技巧。舉個(gè)例子,在 ElementUI 的 Table 組件中,在渲染每個(gè) td 的時(shí)候,有這么一段代碼:

          const data = {
            storethis.store,
            _selfthis.context || this.table.$vnode.context,
            column: columnData,
            row,
            $index
          }

          這樣的代碼相信很多小伙伴隨手就寫(xiě)了,但卻忽視了其內(nèi)部潛在的性能問(wèn)題。

          由于 Vue.js 響應(yīng)式系統(tǒng)的設(shè)計(jì),在每次訪問(wèn) this.store 的時(shí)候,都會(huì)觸發(fā)響應(yīng)式數(shù)據(jù)內(nèi)部的 getter 函數(shù),進(jìn)而執(zhí)行它的依賴收集,當(dāng)這段代碼被循環(huán)了 1000 * 7 次,就會(huì)執(zhí)行 this.store 7000 次的依賴收集,這就造成了性能的浪費(fèi),而真正的依賴收集只需要執(zhí)行一次就足夠了。

          解決這個(gè)問(wèn)題其實(shí)也并不難,由于 Table 組件中的 TableBody 組件是用 render 函數(shù)寫(xiě)的,我們可以在組件 render 函數(shù)的入口處定義一些局部變量:

          render(h) {
            const { store /*...*/} = this
            const context = this.context ||  this.table.$vnode.context
          }

          然后在渲染整個(gè) render 的過(guò)程中,把局部變量當(dāng)作內(nèi)部函數(shù)的參數(shù)傳入,這樣在內(nèi)部渲染 td 的渲染中再次訪問(wèn)這些變量就不會(huì)觸發(fā)依賴收集了:

          rowRender({store, context, /* ...其它變量 */}) {
            const data = {
              store: store,
              _self: context,
              column: columnData,
              row,
              $index,
              disableTransition,
              isSelectedRow
            }
          }

          通過(guò)這種方式,我們把類(lèi)似的代碼都做了修改,就實(shí)現(xiàn)了 TableBody 組件渲染函數(shù)內(nèi)部訪問(wèn)這些響應(yīng)式變量,只觸發(fā)一次依賴收集的效果,從而優(yōu)化了 render 的性能。

          來(lái)看一下優(yōu)化后的火焰圖:

          從面積上看似乎 Scripting 的執(zhí)行時(shí)間變少了,我們?cè)賮?lái)看它一次更新所需要的 JS 執(zhí)行時(shí)間:

          我們發(fā)現(xiàn)組件的 render to vnode 花費(fèi)的時(shí)間約 240ms;vnode patch to DOM 花費(fèi)的時(shí)間約 127ms。

          可以看到,ZoomUI Table 組件的 render 的時(shí)間和 update 的時(shí)間都要明顯少于 ElementUI 的 Table 組件。render 時(shí)間減少是由于響應(yīng)式變量依賴收集的時(shí)間大大減少,update 的時(shí)間的減少是因?yàn)?fixed 表格渲染的 DOM 數(shù)量減少。

          從用戶的角度來(lái)看,DOM 的更新除了 Scripting 的時(shí)間,還有 Rendering 的時(shí)間,它們是共享一個(gè)線程的,當(dāng)然由于 ZoomUI Table 組件渲染的 DOM 數(shù)量更少,執(zhí)行 Rendering 的時(shí)間也更短。

          手寫(xiě) benchmark

          僅僅從 Performance 面板的測(cè)試并不是一個(gè)特別精確的 benchmark,我們可以針對(duì) Table 組件手寫(xiě)一個(gè) benchmark。

          我們可以先創(chuàng)建一個(gè)按鈕,去模擬 Table 組件的選中操作:

          <div>
            <zm-button @click="toggleSelection(computedData[1])
          "
          >
          切換第二行選中狀態(tài)
            </zm-button>
          </div>
          <div>
            更新所需時(shí)間: {{ renderTime }}
          </div>

          然后實(shí)現(xiàn)這個(gè) toggleSelection 函數(shù):

          methods: {
           toggleSelection(row) {
             const s = window.performance.now()
             if (row) {
               this.$refs.table.toggleRowSelection(row)
             }
             setTimeout(() => {
               this.renderTime = (window.performance.now() - s).toFixed(2) + 'ms'
             })
           }
          }

          我們?cè)邳c(diǎn)擊事件的回調(diào)函數(shù)中,通過(guò) window.performance.now() 記錄起始時(shí)間,然后在 setTimeout 的回調(diào)函數(shù)中,再去通過(guò)時(shí)間差去計(jì)算整個(gè)更新渲染需要的時(shí)間。

          由于 JS 的執(zhí)行和 UI 渲染占用同一線程,因此在一個(gè)宏任務(wù)執(zhí)行過(guò)程中,會(huì)執(zhí)行這倆任務(wù),而 setTimeout 0 會(huì)把對(duì)應(yīng)的回調(diào)函數(shù)添加到下一個(gè)宏任務(wù)中,當(dāng)該回調(diào)函數(shù)執(zhí)行,說(shuō)明上一個(gè)宏任務(wù)執(zhí)行完畢,此時(shí)做時(shí)間差去計(jì)算性能是相對(duì)精確的。

          基于手寫(xiě)的 benchmark 得到如下測(cè)試結(jié)果:

          ElementUI Table組件一次更新的時(shí)間約為 900ms

          ZoomUI Table組件一次更新的時(shí)間約為 280ms,相比于 ElementUI 的 Table 組件,性能提升了約三倍

          v-memo 的啟發(fā)

          經(jīng)過(guò)這一番優(yōu)化,基本解決了文章開(kāi)頭提到的問(wèn)題,在 200 * 20 的表格中去選中一列,已經(jīng)并無(wú)明顯的卡頓感了,但相比于 jQuery 實(shí)現(xiàn)的 Table,效果還是要差了一點(diǎn)。

          雖然性能優(yōu)化了三倍,但我還是有個(gè)心結(jié):明明只更新了一行數(shù)據(jù)的選中狀態(tài),卻還是重新渲染了整個(gè)表格,仍然需要在組件 render 的過(guò)程中執(zhí)行多次的循環(huán),在 patch 的過(guò)程中通過(guò) diff 算法來(lái)對(duì)比更新。

          最近我研究了 Vue.js 3.2 v-memo 的實(shí)現(xiàn),看完源碼后,我非常激動(dòng),因?yàn)榘l(fā)現(xiàn)這個(gè)優(yōu)化技巧似乎可以應(yīng)用到 ZoomUI 的 Table 組件中,盡管我們的組件庫(kù)是基于 Vue 2 版本開(kāi)發(fā)的。

          我花了一個(gè)下午的時(shí)間,經(jīng)過(guò)一番嘗試,果然成功了,那么具體是怎么做的呢?先不著急,我們從 v-memo 的實(shí)現(xiàn)原理說(shuō)起。

          v-memo 的實(shí)現(xiàn)原理

          v-memo 是 Vue.js 3.2 版本新增的指令,它可以用于普通標(biāo)簽,也可以用于列表,結(jié)合 v-for 使用,在官網(wǎng)文檔中,有這么一段介紹:

          v-memo 僅供性能敏感場(chǎng)景的針對(duì)性優(yōu)化,會(huì)用到的場(chǎng)景應(yīng)該很少。渲染 v-for 長(zhǎng)列表 (長(zhǎng)度大于 1000) 可能是它最有用的場(chǎng)景:

          <div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
            <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
            <p>...more child nodes</p>
          </div>

          當(dāng)組件的 selected 狀態(tài)發(fā)生變化時(shí),即使絕大多數(shù) item 都沒(méi)有發(fā)生任何變化,大量的 VNode 仍將被創(chuàng)建。此處使用的 v-memo 本質(zhì)上代表著“僅在 item 從未選中變?yōu)檫x中時(shí)更新它,反之亦然”。這允許每個(gè)未受影響的 item 重用之前的 VNode,并完全跳過(guò)差異比較。注意,我們不需要把 item.id 包含在記憶依賴數(shù)組里面,因?yàn)?Vue 可以自動(dòng)從 item:key 中把它推斷出來(lái)。

          其實(shí)說(shuō)白了 v-memo 的核心就是復(fù)用 vnode,上述模板借助于在線模板編譯工具,可以看到其對(duì)應(yīng)的 render 函數(shù):

          import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, isMemoSame as _isMemoSame, withMemo as _withMemo } from "vue"

          const _hoisted_1 = /*#__PURE__*/_createElementVNode("p"null"...more child nodes"-1 /* HOISTED */)

          export function render(_ctx, _cache, $props, $setup, $data, $options{
            return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (item, __, ___, _cached) => {
              const _memo = ([item.id === _ctx.selected])
              if (_cached && _cached.key === item.id && _isMemoSame(_cached, _memo)) return _cached
              const _item = (_openBlock(), _createElementBlock("div", {
                key: item.id
              }, [
                _createElementVNode("p"null"ID: " + _toDisplayString(item.id) + " - selected: " + _toDisplayString(item.id === _ctx.selected), 1 /* TEXT */),
                _hoisted_1
              ]))
              _item.memo = _memo
              return _item
            }, _cache, 0), 128 /* KEYED_FRAGMENT */))
          }

          基于 v-for 的列表內(nèi)部是通過(guò) renderList 函數(shù)來(lái)渲染的,來(lái)看它的實(shí)現(xiàn):

          function renderList(source, renderItem, cache, index{
            let ret
            const cached = (cache && cache[index])
            if (isArray(source) || isString(source)) {
              ret = new Array(source.length)
              for (let i = 0, l = source.length; i < l; i++) {
                ret[i] = renderItem(source[i], i, undefined, cached && cached[i])
              }
            }
            else if (typeof source === 'number') {
              // source 是數(shù)字
            }
            else if (isObject(source)) {
              // source 是對(duì)象
            }
            else {
              ret = []
            }
            if (cache) {
              cache[index] = ret
            }
            return ret
          }

          我們只分析 source,也就是列表 list 是數(shù)組的情況,對(duì)于每一個(gè) item,會(huì)執(zhí)行 renderItem 函數(shù)來(lái)渲染。

          從生成的 render 函數(shù)中,可以看到 renderItem 的實(shí)現(xiàn)如下:

          (item, __, ___, _cached) => {
              const _memo = ([item.id === _ctx.selected])
              if (_cached && _cached.key === item.id && _isMemoSame(_cached, _memo)) return _cached
              const _item = (_openBlock(), _createElementBlock("div", {
                key: item.id
              }, [
                _createElementVNode("p"null"ID: " + _toDisplayString(item.id) + " - selected: " + _toDisplayString(item.id === _ctx.selected), 1 /* TEXT */),
                _hoisted_1
              ]))
              _item.memo = _memo
              return _item
            }

          renderItem 函數(shù)內(nèi)部,維護(hù)了一個(gè) _memo 變量,它就是用來(lái)判斷是否從緩存里獲取 vnode 的條件數(shù)組;而第四個(gè)參數(shù) _cached 對(duì)應(yīng)的就是 item 對(duì)應(yīng)緩存的 vnode。接下來(lái)通過(guò) isMemoSame 函數(shù)來(lái)判斷 memo 是否相同,來(lái)看它的實(shí)現(xiàn):

          function isMemoSame(cached, memo{
            const prev = cached.memo
            if (prev.length != memo.length) {
              return false
            }
            for (let i = 0; i < prev.length; i++) {
              if (prev[i] !== memo[i]) {
                return false
              }
            }
            // ...
            return true
          }

          isMemoSame 函數(shù)內(nèi)部會(huì)通過(guò) cached.memo 拿到緩存的 memo,然后通過(guò)遍歷對(duì)比每一個(gè)條件來(lái)判斷和當(dāng)前的 memo 是否相同。

          而在 renderItem 函數(shù)的結(jié)尾,就會(huì)把 _memo 緩存到當(dāng)前 itemvnode 中,便于下一次通過(guò)  isMemoSame 來(lái)判斷這個(gè) memo 是否相同,如果相同,說(shuō)明該項(xiàng)沒(méi)有變化,直接返回上一次緩存的 vnode

          那么這個(gè)緩存的 vnode 具體存儲(chǔ)到哪里呢,原來(lái)在初始化組件實(shí)例的時(shí)候,就設(shè)計(jì)了渲染緩存:

          const instance = {
            // ...
            renderCache: []
          }

          然后在執(zhí)行 render 函數(shù)的時(shí)候,把這個(gè)緩存當(dāng)做第二個(gè)參數(shù)傳入:

          const { renderCache } = instance
          result = normalizeVNode(
            render.call(
              proxyToUse,
              proxyToUse,
              renderCache,
              props,
              setupState,
              data,
              ctx
            )
          )

          然后在執(zhí)行 renderList 函數(shù)的時(shí)候,把 _cahce 作為第三個(gè)參數(shù)傳入:

          export function render(_ctx, _cache, $props, $setup, $data, $options{
            return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (item, __, ___, _cached) => {
              // renderItem 實(shí)現(xiàn)
            }, _cache, 0), 128 /* KEYED_FRAGMENT */))
          }

          所以實(shí)際上列表緩存的 vnode 都保留在 _cache 中,也就是 instance.renderCache 中。

          那么為啥使用緩存的 vnode 就能優(yōu)化 patch 過(guò)程呢,因?yàn)樵?patch 函數(shù)執(zhí)行的時(shí)候,如果遇到新舊 vnode 相同,就直接返回,什么也不用做了。

          const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = false) => {
            if(n1 === n2) {
              return
            }
            // ...
          }

          顯然,由于使用緩存的 vnode,它們指向同一個(gè)對(duì)象引用,直接返回,節(jié)約了后續(xù)執(zhí)行 patch 過(guò)程的時(shí)間。

          在 Table 組件的應(yīng)用

          v-memo 的優(yōu)化思路很簡(jiǎn)單,就是復(fù)用緩存的 vnode,這是一種空間換時(shí)間的優(yōu)化思路。

          那么,前面我們提到在表格組件中選擇狀態(tài)沒(méi)有變化的行,是不是也可以從緩存中獲取呢?

          順著這思路,我給 Table 組件設(shè)計(jì)了 useMemo 這個(gè) prop,它其實(shí)是專(zhuān)門(mén)用于有選擇列的場(chǎng)景。

          然后在 TableBody 組件的 created 鉤子函數(shù)中,創(chuàng)建了用于緩存的對(duì)象:

          created() {
            if (this.table.useMemo) {
              if (!this.table.rowKey) {
               throw new Error('for useMemo, row-key is required.')
              }
              this.vnodeCache = []
            }
          }

          這里之所以把 vnodeCache 定義到 created 鉤子函數(shù)中,是因?yàn)樗⒉恍枰兂身憫?yīng)式對(duì)象。

          另外注意,我們會(huì)根據(jù)每一行的 key 作為緩存的 key,因此 Table 組件的 rowKey 屬性是必須的。

          然后在渲染每一行的過(guò)程中,添加了 useMemo 相關(guān)的邏輯:

          function rowRender(/* 各種變量參數(shù) */}{
            let memo
            const key = this.getKeyOfRow({ row, rowIndex: $index, rowKey })
            let cached
            if (useMemo) {
              cached = this.vnodeCache[key]
              const currentSelection = store.states.selection
              if (cached && !this.isRowSelectionChanged(row, cached.memo, currentSelection)) {
                return cached
              }
              memo = currentSelection.slice()
            }
            // 渲染 row,返回對(duì)應(yīng)的 vnode
            const ret = rowVnode
            if (useMemo && columns.length) {
              ret.memo = memo
              this.vnodeCache[key] = ret
             }
             return ret
          }

          這里的 memo 變量用于記錄已選中的行數(shù)據(jù),并且它也會(huì)在函數(shù)最后存儲(chǔ)到 vnodememo,便于下一次的比對(duì)。

          在每次渲染 rowvnode 前,會(huì)根據(jù) row 對(duì)應(yīng)的 key 嘗試從緩存中取;如果緩存中存在,再通過(guò) isRowSelectionChanged 來(lái)判斷行的選中狀態(tài)是否改變;如果沒(méi)有改變,則直接返回緩存的 vnode

          如果沒(méi)有命中緩存或者是行選擇狀態(tài)改變,則會(huì)去重新渲染拿到新的 rowVnode,然后更新到 vnodeCache 中。

          當(dāng)然,這種實(shí)現(xiàn)相比于 v-memo 沒(méi)有那么通用,只去對(duì)比行選中的狀態(tài)而不去對(duì)比其它數(shù)據(jù)的變化。你可能會(huì)問(wèn),如果這一行某列的數(shù)據(jù)修改了,但選中狀態(tài)沒(méi)變,再走緩存不就不對(duì)了嗎?

          確實(shí)存在這個(gè)問(wèn)題,但是在我們的使用場(chǎng)景中,遇到數(shù)據(jù)修改,是會(huì)發(fā)送一個(gè)異步請(qǐng)求到后端,然獲取新的數(shù)據(jù)再來(lái)更新表格數(shù)據(jù)。因此我只需要觀測(cè)表格數(shù)據(jù)的變化清空 vnodeCache 即可:

          watch: {
            'store.states.data'() {
              if (this.table.useMemo) {
                this.vnodeCache = []
              }
            }
          }

          此外,我們支持列的可選則渲染功能,以及在窗口發(fā)生變化時(shí),隱藏列也可能發(fā)生變化,于是在這兩種場(chǎng)景下,也需要清空 vnodeCache

          watch:{
            'store.states.columns'() {
              if (this.table.useMemo) {
                this.vnodeCache = []
              }
            },
            columnsHidden(newVal, oldVal) {
              if (this.table.useMemo && !valueEquals(newVal, oldVal)) {
                this.vnodeCache = []
              }
            }
          }

          以上實(shí)現(xiàn)就是基于 v-memo 的思路實(shí)現(xiàn)表格組件的性能優(yōu)化。我們從火焰圖上看一下它的效果:

          我們發(fā)現(xiàn)黃色的 Scripting 時(shí)間幾乎沒(méi)有了,再來(lái)看它一次更新所需要的 JS 執(zhí)行時(shí)間:

          我們發(fā)現(xiàn)組件的 render to vnode 花費(fèi)的時(shí)間約 20msvnode patch to DOM 花費(fèi)的時(shí)間約 1ms,整個(gè)更新渲染過(guò)程, JS 的執(zhí)行時(shí)間大幅減少。

          另外,我們通過(guò)benchmark 測(cè)試,得到如下結(jié)果:

          優(yōu)化后,ZoomUI Table組件一次更新的時(shí)間約為 80ms,相比于 ElementUI 的 Table 組件,性能提升了約十倍

          這個(gè)優(yōu)化效果還是相當(dāng)驚人的,并且從性能上已經(jīng)不輸 jQuery Table 了,我兩年的心結(jié)也隨之解開(kāi)了。

          總結(jié)

          Table 表格性能提升主要是三個(gè)方面:減少 DOM 數(shù)量、優(yōu)化 render 過(guò)程以及復(fù)用 vnode。有些時(shí)候,我們還可以從業(yè)務(wù)角度思考,去做一些優(yōu)化。

          雖然 useMemo 的實(shí)現(xiàn)還比較粗糙,但它目前已滿足我們的使用場(chǎng)景了,并且當(dāng)數(shù)據(jù)量越大,渲染的行列數(shù)越多,這種優(yōu)化效果就越明顯。如果未來(lái)有更多的需求,更新迭代就好。

          由于一些原因,我們公司仍然在使用 Vue 2,但這并不妨礙我去學(xué)習(xí) Vue 3,了解它一些新特性的實(shí)現(xiàn)原理以及設(shè)計(jì)思想,能讓我開(kāi)拓不少思路。

          從分析定位問(wèn)題到最終解決問(wèn)題,希望這篇文章能給你在組件的性能優(yōu)化方面提供一些思路,并應(yīng)用到日常工作中。


          往期推薦

          Vite 太快了,煩死了,是時(shí)候該小睡一會(huì)了。


          如何實(shí)現(xiàn)比 setTimeout 快 80 倍的定時(shí)器?


          萬(wàn)字長(zhǎng)文!總結(jié)Vue 性能優(yōu)化方式及原理


          90 行代碼的 webpack,你確定不學(xué)嗎?





          如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)

          2. 歡迎加我微信「huab119」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

            關(guān)注公眾號(hào)「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。



          點(diǎn)個(gè)在看支持我吧,轉(zhuǎn)發(fā)就更好了

          如果覺(jué)得這篇文章還不錯(cuò),來(lái)個(gè)【轉(zhuǎn)發(fā)、收藏、在看】三連吧,讓更多的人也看到~


          瀏覽 63
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大鸡巴在线| 国产鸡巴操逼视频 | 精品少妇无码视频 | 夜色av最新网址 一本无码免费视频 | 黑人丰满大荫蒂 |