记 Vue 大型表单项目的一个性能问题

问题场景

身为一个表单表格工程师,自然日复一日的写着表单表格,本以为已经没啥难点的时候转眼间就来了一个有意思的情况,在超大量 数据绑定在 vue 的时候出现了表单操作起来卡顿的情况。

这里先贴上本项目出现的情况演示的 github 上的地址,tag1.0.1(https://github.com/everlose/more-form-demo/tree/v1.0.1

如图所见,当在 input 输入数据的时候,连续输入会感觉明显的延迟。

image

那么,这到底是怎么回事?

代码

上述的表单数据项修改频繁由后端返回,于是在前端需要渲染从后端返回的 68kb 的一个 JSON 数据串,包括所有配置表单项以及其可能的选项值,数据见这里

核心渲染是有这么一段

<div class="basic-info ct-form" v-for="(config, configIndex) in formConfig" :key="configIndex">

<h3 class="form__title">{{config.title}}</h3>

<el-form class="form-content" ref="form" label-width="150px">

    <el-form-item

        class="basic-form-item"

        v-for="(item, itemIndex) in config.formItems"

        :key="itemIndex"

        :prop="item.code"

        :label="item.name"

        :required="item.required"

        :rules="item.rules">

        <el-radio-group

            v-if="item.type === 'radio'"

            v-model="formData[item.code]">

            <el-radio

                v-for="(option, radioIndex) in formOptions[item.optionCode]"

                :key="option.value"

                :label="option.value"

                :disabled="item.disabled">

                {{ option.label }}

            </el-radio>

        </el-radio-group>

        <el-input

            v-else-if="item.type === 'input'"

            :class="{ longInput: item.isLongInput }"

            :placeholder="item.placeholder || '请输入'"

            v-model="formData[item.code]"

            :label="item.label"

            :disabled="item.disabled"

            :maxlength="item.maxLength">

        </el-input>

        <el-select

            v-else-if="item.type === 'select'"

            v-model="formData[item.code]"

            :disabled="item.disabled"

            :placeholder="item.placeholder || '请选择'">

            <el-option

                v-for="(option, optionsIndex) in formOptions[item.optionCode]"

                :key="option.value"

                :label="option.label"

                :value="option.value">

            </el-option>

        </el-select>

    </el-form-item>

</el-form>

</div>

这就是一个简单的双层遍历渲染所有表单配置项的模版代码,其中的 formConfig 正是所有配置表单项,数据量极多。formOptions 挂载了所有表单选项值,也是动辄几千项。

思路

正当我对着这么高的操作延时发愁的时候,组里一个大佬提醒我,可能是 Vue.prototype._update 这个触发的太频繁了。

我急忙找到这一段打了个断点调试

image

Vue.prototype._update 这函数里触发的是 VNode 虚拟节点的比对更新,打断点调试后发现实际上这是一个循环,在控制台里输出 this.$el 的时候能得到正在深度遍历中的节点,沿着根结点 App(也是 formConfig 数据绑定的作用域) 开始直到具体触发输入的那个表单元素。

在本项目里是使用了遍历输出所有的表单元素,并且当前组件的作用域是直接挂在根结点上的,是否就是这个遍历引发了如此高的延时呢?于是我找到上图右侧的调用堆栈,发现正是 flushSchedulerQueue 函数写着一个 for 循环。

image

在 flushSchedulerQueue 函数中的 for 循环里头尾插入代码来获取耗费时间。

结果得知输入时的延迟大概在 300ms 之上。

image

似乎问题就找到了,flushSchedulerQueue 函数针对 data 中数据的修改把 watcher 推送进队列里在更新,这一循环耗费的时间比较长。

解决

其实早在调试 Vue.prototype._update 函数就初见端倪,循环中的 this.$el 从当前组件的根部开始深度遍历,遍历了太多次,那么只要想办法缩小当前组件所绑定的数据量就解决了。

于是核心代码调整为

<div class="basic-info ct-form" v-for="(config, configIndex) in formConfig" :key="configIndex">

<edit-form :config="config" :data="formData" :options="formOptions"></edit-form></div>

只是用一个 edit-form 包裹刚刚所有的 el-form-item 的渲染代码就解决了,再次调试 Vue.prototype._update 得出遍历节点 this.$el 已经变为下图所示的 div.edit-form 了,flushSchedulerQueue 函数 for 循环的延迟也变为 10ms 左右

image

修复版的代码在2.0.0的tag上,这里贴上链接(https://github.com/everlose/more-form-demo/tree/v2.0.0

后记

本质上这就是一个原则,最好不要在一个vue组件上直接绑定如此多的数据,如果有大量数据请分多个组件绑定。这么浅尝辄止实在让人不够尽兴,于是这里贴上 Vue.prototype._update 前的关键部分调用堆栈以及其函数作用。

找到项目中 node_modules 下的 vue.esm.js

往input里输入将会触发model data的更新

978 set: function reactiveSetter (newVal)

订阅器dep是数据绑定和视图更新的关键,这里触发去通知相关视图的更新

994 dep.notify();

673 Dep.prototype.notify

notify函数里的subs实际上是Watcher对象的实例,这里触发视图更新操作

677 subs[i].update(); subs实际上是包裹watcher的数组

3093 Watcher.prototype.update

把watcher塞进一个队列里,这里是和异步更新视图有关。

3100 queueWatcher(this);

2945 function queueWatcher (watcher) push到队列里

nextTick是具体做异步更新的部分

2963 nextTick(flushSchedulerQueue);

1778 function nextTick (cb, ctx)

异步操作实际上是原生 H5 MessageChannel API 通道通信来推送消息来实现变化。

1738 port.postMessage(1);

注意在异步操作中,最终传入的回调函数被执行来进行下面视图的更新。这里是执行一个任务调度队列的调度过程,需要循环遍历。

2856 function flushSchedulerQueue

3108 Watcher.prototype.run

Evaluate the getter, and re-collect dependencies.

3043 Watcher.prototype.get

watcher中的getter的name就叫updateComponent,于是被执行

2689 updateComponent

2690 vm._update(vm._render(), hydrating);

进入vue的生命周期中的update函数

2548 Vue.prototype._update

patch做的是vnode的节点比对,最终把新的vnode结构渲染到具体视图,不再多做描述。

2572 vm.$el = vm.patch(prevVnode, vnode);

贴上提供思路的大佬的github地址: https://github.com/answershuto

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容