未加星标

Vue 性能优化之深挖数组

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二05 | 时间 2018 | 作者 红领巾 ] 0人收藏点击收藏

最近在用 Vue 重构一个历史项目,一个考试系统,题目量很大,所以核心组件的性能成为了关注点。先来两张图看下最核心的组件 Paper 的样式。


Vue 性能优化之深挖数组
Vue 性能优化之深挖数组

从图中来看,分为答题区与选择面板区。

稍微对交互逻辑进行下拆解:

答题模式与学习模式可以相互切换,控制正确答案显隐。 单选与判断题直接点击就记录答案正确性,多选是选择答案之后点击确定才能记录正确性。 选择面板则是记录做过的题目的情况,分为六种状态(未做过的,未做过且当前选择的,做错的,做错的且当前选择的,做对的,做对的且当前选择的),用不同的样式去区别。 点击选择面板,答题区能切到对应的题号。

基于以上考虑,我觉得我必须有三个响应式的数据:

currentIndex questions cardData

数组每一项数据结构如下:

currentIndex = 0 // 用来标记当前选中题目的索引 questions = [{ secId: 1, // 所属章节的 id tid: 1, // 题目 id content: '题目内容' // 题目描述 type: 1, // 题型,1 ~ 3 (单选,多选,判断) options: ['选项1', '选项2', '选项3', '选项4',] // 每个选项的描述 choose: [1, 2, 4], // 多选――记录用户未提交前的选项 done: true, // 标记当前题目是否已做 answerIsTrue: undefined // 标记当前题目的正确与否 }] cardData = [{ startIndex: 0, // 用来记录循环该分组数据的起始索引,这个值等于前面数据的长度累加。 secName: '章节名称', secId: '章节id', tids: [1, 2, 3, 11] // 该章节下面的所有题目的 id }] 复制代码

由于题目可以左右滑动切换,所以我每次从 questions 取了三个数据去渲染,用的是 cube-ui 的 Slide 组件,只要自己根据 this.currentIndex 结合 computed 特性去动态的切割三个数据就行。

这一切都显得很美好,尤其是即将结束了一个历史项目的核心组件的编写之前,心情特别的舒畅。

然而转折点出现在了渲染选择面板样式这一步

代码逻辑很简单,但是发生了让我懵逼的事情。

<div class="card-content"> <div class="block" v-for="item in cardData" :key="item.secName"> <div class="sub-title">{{item.secName}}</div> <div class="group"> <span @click="cardClick(index + item.startIndex)" class="item" :class="getItemClass(index + item.startIndex)" v-for="(subItem, index) in item.secTids" :key="subItem">{{index + item.startIndex + 1}}</span> </div> </div> </div> 复制代码

其实就是利用 cardData 去生成 DOM 元素,这是个分组数据(先是以章节为维度,章节下面还有对应的题目),上面的代码其实是一个循环里面嵌套了另一个循环。

但是,只要我切换题目或者点击面板,抑或是触发任意响应式数据的改变,都会让页面卡死!! 探索

当下的第一反应,肯定是 js 在某一步的执行时间过长,所以利用 Chrome 自带的 Performance 工具 追踪了一下,发现问题出在 getItemClass 这个函数调用,占据了 99% 的时间,而且时间都超过 1s 了。瞅了眼自己的代码:

getItemClass (index) { const ret = {} // 如果是做对的题目,但并不是当前选中 ret['item_true'] = this.questions[index]...... // 如果是做对的题目,并且是当前选中 ret['item_true_active'] = this.questions[index]...... // 如果是做错的题目,但并不是当前选中 ret['item_false'] = this.questions[index]...... // 如果是做错的题目,并且是当前选中 ret['item_false_active'] = this.questions[index]...... // 如果是未做的题目,但不是当前选中 ret['item_undo'] = this.questions[index]...... // 如果是未做的题目,并且是当前选中 ret['item_undo_active'] = this.questions[index]...... return ret }, 复制代码

这个函数主要是用来计算选择面板每一个小圆圈该有的样式。每一步都是对 questions 进行了 getter 操作。初看,好像没什么问题,但是由于之前看过 Vue 的源码,细想之下,觉得不对。

首先,webpack 会将 .vue 文件的 template 转换成 render 函数,也就是实例化组件的时候,其实是对响应式属性求值的过程,这样响应式属性就能将 renderWatcher 加入依赖当中,所以当响应式属性改变的时候,能触发组件重新渲染。

我们先来了解下 renderWatcher 是什么概念,首先在 Vue 的源码里面是有三种 watcher 的。我们只看 renderWatcher 的定义。

// 位于 vue/src/core/instance/lifecycle.js new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) updateComponent = () => { vm._update(vm._render(), hydrating) } // 位于 vue/src/core/instance/render.js Vue.prototype._render = function (): VNode { ...... const { render, _parentVnode } = vm.$options try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { ...... } return vnode } 复制代码

稍微分析下流程:实例化 Vue 实例的时候会走到 options 取到由 template 编译生成的 render 函数,进而执行 renderWatcher 收集依赖。_render 返回的是组件的 vnode,传入 _update 函数从而执行组件的 patch,最终生成视图。

其次,从我写的 template 来分析,为了渲染选择面板的 DOM,是有两层 for 循环的,内部每次循环都会执行 getItemClass 函数,而函数的内部又是对 questions 这个响应式数组进行了 getter 求值,从目前来看,时间复杂度是 O(n),如上图所示,我们大概有 2000 多道题目,我们假设有 10 个章节,每个章节有 200 道题目,getItemClass 内部是对 questions 进行了 6 次求值,这样一算,粗略也是 12000 左右,按 js 的执行速度,是不可能这么慢的。

那么问题是不是出现在对 questions 进行 getter 的过程中,出现了 O(n) 的复杂度呢?

于是,我打开了 Vue 的源码,由于之前深入研究过源码,所以轻车熟路地找到了 vue/src/core/instance/state.js 里面将 data 转换成 getter/setter 的部分。

function initData (vm: Component) { ...... // observe data observe(data, true /* asRootData */) } 复制代码

定义一个组件的 data 的响应式,都是从 observe 函数开始,它的定义是位于 vue/src/core/observer/index.js 。

export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } 复制代码

observe 函数接受对象或者数组,内部会实例化 Observer 类。

export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } 复制代码

Observer 的构造函数很简单,就是声明了 dep、value 属性,并且将 value 的 _ ob _ 属性指向当前实例。举个栗子:

// 刚开始的 options export default { data : { msg: '消息', arr: [1], item: { text: '文本' } } } // 实例化 vm 的时候,变成了以下 data: { msg: '消息', arr: [1, __ob__: { value: ..., dep: new Dep(), vmCount: ... }], item: { text: '文本', __ob__: { value: ..., dep: new Dep(), vmCount: ... } }, __ob__: { value: ..., dep: new Dep(), vmCount: ... } } 复制代码

也就是每个对象或者数组被 observe 之后,多了一个 _ ob _ 属性,它是 Observer 的实例。那么这么做的意义何在呢,稍后分析。

继续分析 Observer 构造函数的下面部分:

// 如果是数组,先篡改数组的一些方法(push,splice,shift等等),使其能够支持响应式 if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } // 数组里面的元素还是数组或者对象,递归地调用 observe 函数,使其成为响应式数据 this.observeArray(value) } else { // 遍历对象,使其每个键值也能成为响应式数据 this.walk(value) } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { // 将对象的键值转换成 getter / setter, // getter 收集依赖 // setter 通知 watcher 更新 defineReactive(obj, keys[i]) } } observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } 复制代码

我们再捋一下思路,首先在 initState 里面调用 initData,initData 得到用户配置的 data 对象后调用了 observe,observe 函数里面会实例化 Observer 类,在其构造函数里面,首先将对象的 _ ob _ 属性指向 Observer 实例(这一步是为了检测到对象添加或者删除属性之后,能触发响应式的伏笔),之后遍历当前对象的键值,调用 defineReactive 去转换成 getter / setter。

所以,来分析下 defineReactive。

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 每个属性收集 watcher 的管理器 const dep = new Dep() ...... // 递归

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

代码区博客精选文章
分页:12
转载请注明
本文标题:Vue 性能优化之深挖数组
本站链接:https://www.codesec.net/view/621369.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(27)