記 vue 表單的一個性能問題


背景

產品反饋表單頁太卡了,這是一個有意思的情況,讓我看看。

如圖所見,當在 input 輸入數據的時候,連續輸入會感覺明顯的延遲。

那個項目最多情況下,表單數量達到千數。筆者在 demo 里簡化實現,並把表單數量提升到 10000,把下面的代碼粘貼運行一邊就能得到卡頓效果。

<!DOCTYPE html>
<html>
<head>
    <title>Form Demo</title>
</head>
<body>
    <div id="app">
        <template v-for="item in options">
            <input type="text" v-model="item.data">    
        </template>
    </div>

    <!-- Vue.js v2.6.11 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
        let options = []
        for (let i = 0; i < 10000; i++) {
            options.push({
                data: '',
            })
        }
        var app = new Vue({
            el: '#app',
            data: {
                options: options,
            },
        })
        window.app = app;
        console.log(app);

        // 接着控制台里輸入
        // var event = new CustomEvent('test', { 'detail': ['a', 'c', 'e', 'f', 'b', 'd'] }); window.dispatchEvent(event);
        // 能把 message 改為這個數組
    </script>
</body>
</html>

前置知識梳理

眾所周知,vue2 里的數據使用 Object.defineProperty 設定 get/set 來進行劫持,而當數據改變時,將會觸發 set,在 set 里觸發廣播通知被觀察者進行更新的。

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
        // ...
    },
    set: function reactiveSetter (newVal) {
        // ...
        dep.notify();
    }
})

// ------ dep 的結構如下
/*{
    id: 118,
    subs: [Watcher]
}*/

// ------- Watcher 的結構
/*{
    vm,
    cb,
    deps,
    express,
    ...
}*/

Vue 把更新收集到隊列里,並每隔一段時間去執行,一般是這些被觀察者 Watcher 的表達式。

總之在這里執行的更新語句是

expression: "function () { vm._update(vm._render(), hydrating); }"

其中 _render 執行完后得出此組件的 Vnode,並傳給 Vue.prototype._update 語句進行更新。

ok,知識梳理完畢,那么到底 _update 里怎么做的,讓頁面更新渲染如此之慢呢?

調試過程

Vue.prototype._update 打斷點調試,如下圖:

省略函數進入步驟 patch -> patchVnode -> updateChildren,直到 updateChildren 發現核心比對邏輯

比對這 10000 個節點。簡單來說相同 key 和標簽名的被判定為相同節點,相同節點還得繼續去遞歸比對其子節點是否相同。並且比對過程中,還需要判定更新的內容里有 attr、 class、listener、style 等等信息,由此產生的計算量還是挺大的。筆者下圖與本次調試無關,但能簡單揭示下比對邏輯。

筆者在這里 pachVnode 里執行 updateChildren 的地方,打印耗時,發現當 input 為 1000 項的時候每輸入一個字符耗時一般是個位數的毫秒。

而 input 為 10000 項時,每個字符輸入響應需要 50~100 毫秒的話,快速輸入一串字符,產生的卡頓感就會比較厲害。

而在我們實際的項目中,表單復雜的多,比對的層級深,或許 1000 不到的表單就能產生這樣的效果。

解決

既然更新 10000 個節點費力,那何不縮小更新范圍呢。把表單拆成若干組,每組包裹在組件中,輸入時只會更新那個組件,影響范圍就笑得多。由此產生的更新如下:

<!DOCTYPE html>
<html>
<head>
    <title>Form Demo</title>
</head>
<body>
    <div id="app">
        <input-group :forms="forms" v-for="(forms, index) in options" :key="index"></input-group>
    </div>

    <!-- Vue.js v2.6.11 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
        Vue.component('input-group', {
            props: ['forms'],
            template: `<div>
                <template v-for="item in forms">
                    <input type="text" v-model="item.data">
                </template>
            </div>`
        })

        let options = []
        for (let i = 0; i < 100; i++) {
            for (let j = 0; j < 100; j++) {
                options[i] = options[i] || [];
                options[i].push({
                    data: '',
                })
            }
            
        }
        var app = new Vue({
            el: '#app',
            data: {
                options: options,
            },
        })
        window.app = app;
        console.log(app);

        // 接着控制台里輸入
        // var event = new CustomEvent('test', { 'detail': ['a', 'c', 'e', 'f', 'b', 'd'] }); window.dispatchEvent(event);
        // 能把 message 改為這個數組
    </script>
</body>
</html>

每個字符的更新就降低到 3ms 的樣子,響應快得多了。

總結

本質上這就是一個原則,不要在一個 vue 組件上綁定那么多的元素,請拆分成多個子組件。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM