浅谈Vue 性能优化之深挖数组(2)

其实就是利用 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 的执行速度,是不可能这么慢的。