问题场景
身为一个表单表格工程师, 自然日复一日的写着表单表格, 本以为已经没啥难点的时候转眼间就来了一个有意思的情况, 在超大量 数据绑定在 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, 于是被执行
- updateComponent
- 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
来源: http://www.jianshu.com/p/e07ac8062677