Vue2.X版本,通过项目中的实践中,针对以下几点来调优渲染性能,
(1)Vue指令绑定
(2)组件划分
(3)Vuex单向数据流管理与UI响应
NK1 - Retrospect

在进入主题之前,先来回顾上篇文章 《Debounce, Throttle & RequestAnimationFrame》来优化函数触发频率过高,也是性能调优的一部分,针对的是JS执行部分,
而本次主题主要围绕着如何优化HTML的渲染进行展开探讨。
NK2 - Agenda

这次的会议主题主要会先分享:通过Vue的指令监听数据的变化来优化UI的渲染,接着是谈谈Vue组件划分和UI渲染的复杂度,最后会分享下在项目中通过使用Vuex的单向数据流管理对UI数据状态监听和实时响应UI的变化。

一、 Vue指令
(1)特殊属性:v-key
(2)条件渲染:v-if & v-show
(3)列表渲染:v-for
在讲解指令之前,我们先来简单了解下Vue的核心之一双向绑定原理。
NK3 Vue双向绑定
Vue双向绑定是指:数据的变化驱动视图更新,视图的变化来跟新数据,两者是相互影响的。
现在很多框架都采用了双向绑定的原理,但实现方式却是不一样的。

例如,
(1)基于观察者模式: KnockoutJS,BackboneJS
采用发布订阅模式,通常DOM操作需要引用Jquery库来进行双向绑定。
(2)基于数据模型: Ember
将数据与节点元素封装在一起,数据跟新,遍历所有绑定的节点,然后跟新节点
(3)基于脏检查: AngularJS
当触发UI交互事件和异步事件(ajax/timeout),触发脏检查,即检查所有的Watchers有没有变化
(4)基于数据劫持: Vue
而相对于Vue,Vue2.X目前核心的方式是采用Object.defineProperty来实现对属性的劫持,从而达到数据变动的目的。

我们知道当采用字面量来申明一个对象的时候,即 const object={},当添加或者获取属性值的时候,一般不会知道该对象什么时候被赋值,什么时候被获取。
所以Vue在实现双向绑定上,采用了属性拦截器Object.defineproperty()来监听data对象的每个属性的变化。
Object.defineproperty()主要是为每个属性添加Getter和Setter方法,但是它不能监听对象新属性的添加或删除,数组索引和长度的变更,所以vue就开放了set()和delete()两个全局方法来实现对其新属性的监听。
Vue3.X新版本开始将采用ES6的Proxy来进行双向绑定,比较这两种方式的区别,
Object.defineProperty()
(1)监听的是属性的变化,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历。
(2)无法监控到数组下标的变化,即vm.items[indexOfItem] = newValue这种是无法检测的。
ES6 Proxy
(1)Proxy可以直接监听对象而非属性
(2)可以直接监听数组下标的变化
NK4 Vue虚拟DOM
你会发现现代Web页面的大多数逻辑的本质就是不停的修改DOM,但是频繁跟新DOM,会直接导致整个页面掉帧,卡顿甚至失去响应,所以基于Vue的双向绑定,Vue2.X引入了React的虚拟DOM的技术来提升页面的刷新速度。
在Vue里面由于依赖追踪系统的存在,当任意数据变动的时,Vue的每一个组件都精确地知道自己是否需要重绘,所以并不需要手动优化,即针对每个组件是否需要重绘,主要是依赖于VUE 的虚拟DOM来进行优化。

什么是虚拟DOM呢?
虚拟DOM一开始是FaceBook React项目中所提出技术概念,它是由一个由JS模拟了DOM结构而创建的对象,只存在浏览器内存中。它的目的是允许我们能“无所畏惧”地去“刷新”整个页面。它的功能点主要是确保去渲染UI上真正需要改变的部分,具有高效的Diff算法和即时的batching(批处理)算法。
1.Diff算法
当数据发生变化的时候,Diff算法能帮助快速对比前后两个虚拟DOM对象的差异。而它有以下几点特征,
(1)最小的粒度:只比较同一层级的节点元素
如果节点类型不同,直接删掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了;如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。
(2)采用复用策略
复用这个词语在VUE中是个很重要的一个概念,简单来讲就是元素可以被重复使用,很多地方都有用到它,例如对component的划分,我们通常的设计是让这个组件能够被重复使用,而变得只是输入和输出的改变,并且作用域是独立的。而在虚拟DOM优化算法中,它只是想当数据发生改变的时候,Vue只复用数据变化所在的同一层级且已经被渲染过的元素,而不需要移动DOM元素。
2.Patching(批处理)算法
把Diff算法中计算出来的所有差异更新到真实的DOM节点上。
(1)把差异apply到真正的DOM树上
(2)目的是为了减少DOM节点的Reflow & Repaint
(3)即时状态,即并不是将所有的修改一次性去跟新DOM的,而是深度遍历每个差异节点的子节点,然后做出整个节点替换(REPLACE),节点移动(REORDER),节点属性改变(PROPS)或者文本改变(TEXT)。
NK5 复用策略 - 带Key的优化
由于VUE会采用复用策略,所以它提供了 "key"这个特殊属性来最大限度来减少DOM节点的移动,即尝试修复/再利用相同类型元素的算法。
例外"key"也可以用于强制替换元素/组件而不是重复使用它->当与if连用的时候。
接下来我们来研究下当在一个数组中添加一个元素时,带Key与不带Key时,UI渲染情况是怎样的呢?
Case:假设我们有五个元素节点A->B->C->D->E,

UI呈现的情况如下,

而HTML部分如下,

(data-v-xx)-> 这是在标记vue文件中css时使用scoped标记产生的,因为要保证各文件中的css不相互影响,给每个component都做了唯一的标记,所以每引入一个component就会出现一个新的'data-v-xxx'标记.
现在我们的目的想要在位置B和C中间新添加一个新的节点F,如下图,

接着我们来实践带Key与不带Key的渲染效果
(1)不带Key的场景
语法:

UI效果:
可以发现B节点之后的所有节点都被重新渲染了一遍。
(2)带Key的效果
语法:

UI效果:
可以发现UI 仅渲染新增加的F节点元素
结论:

当我们不带Key优化时,你会发现,先是在B和C节点元素中间插入F节点,然后C节点移动到原先D节点的位置,D移动到E的位置,而E成为了最后一个节点。
不带Key的复用情况是,
(1)当迭代的是数组的时候,是以数组的值作为唯一标识.
(2)当迭代的是个对象的话,就以对象的key作为标识
当使用key属性来给每个节点做一个唯一标识,复用情况就不一样了,会复用已有的Dom节点元素,而Diff算法还可以正确的识别此新增的节点,并且能快速找到正确的位置区直接插入,
即key的主要作用是为了能高效的跟新虚拟DOM
NK6 条件渲染: v-if & v-show
v-if 和 v-show都是用来控制元素在视图上显示的状态,两者的使用场景是有区别的。
1.生命周期
(1)v-if 控制着绑定元素或者子组件实例 重新挂载(条件为真)/销毁(条件为假) 到DOM上,并且包含其元素绑定的事件监听器,会重新开启监听。
(2)v-show 控制CSS的切换,元素永远挂载在DOM上
2.权限问题
涉及到权限相关的UI展示无疑用的是v-if.
3.UI操作
(1)初始化渲染,如果要加快首屏渲染,建议用v-if
(2)频次选择,如果是频繁切换使用,建议使用v-show
4.v-if & v-else管理可复用元素
当跟v-else连用时,其复用DOM的作用域范围:指令元素的 子元素并且是同级兄弟单节点
例如,当我们使用v-if 和 v-else 来切换UI展示部分,UI和定义html如下图,

label,input,span就是外层div块下的子标签元素,且是同级兄弟单节点,如下图,

而内部的div属于包裹这其他元素标签,属于复合节点,如下图。

所以v-if 与 v-else 在条件切换,控制UI显示的时候,会按标签元素的顺序进行节点的差异比较,如果是单节点,就直接进行高效的复用 DOM 元素,反而复合节点会整个被替换。
UI效果:
当选择不复用,可以使用key进行控制,用于强制替换元素/组件而不是重复使用它。
即官网提到的:

注意,<label>
元素仍然会被高效地复用,因为它们没有添加key
属性。
NK6 列表渲染: v-for
Vue提供指令 v-for来迭代每一行数据,从而实现列表渲染,最后呈现在UI上。
我们的期望是当数据发生变化时,UI能及时响应,所以对于列表的渲染性能优化问题大体都会结合特殊属性 v-key 来搭配使用,从而达到DOM元素的复用。
对于数组变化的监听响应,有下面两种场景,
1.跟新列表的监听方法
push,pop,shift,unshift,splice,sort,reverse
会直接响应视图的跟新
2.替换列表的监听
(1)数组全新赋值,会直接响应视图跟新
即vm.items = newItems;
(2)VUE2.X利用索引直接替换原有值 =》VUE3.X开始可以忽略这个(ES6 Proxy直接监听对象)
即vm.items[indexOfItem] = newItem;
computed属性监听不了改变,只能采用watch(deep)
解决方案:
- Vue.set(vm.items, indexOfItem, newValue) = vm.$set(vm.items, indexOfItem, newValue)
- vm.items.splice(indexOfItem, 1, newValue)

二、 Vue Component渲染设计
接下来会介绍下组件的分类,组件划分和渲染的复杂度
NK7 组件分类

组件化开发一开始也是由React提出的核心亮点,主要是为了解决 高耦合,低内聚和无复用的问题。
组件化开发主旨:使用组件都封装具有独立功能的UI模块,以组件的方式去重新思考UI构成,整个界面就是一个大组件,然后将小的组件通过组合或者嵌套的方式构成大的组件,最终完成整体UI的构建。
组件化开发规定组件采用单一原则,即理想状态下,一个组件只做一件事,只关心自己部分的逻辑,所以组件化开发过程就是不断优化和拆分界面组件、构造整个组件树的过程。

对于组件的分类大体可以分为以下四种
1.展示型组件 - Display
纯展示型组件只关注数据进,然后直接渲染DOM。
2.接入型组件 - Container
Container主要适用于与数据层service打交道,包含和服务器端或者数据源打交道的逻辑,一般将数据传给简单的展示型组件。
3.交互性组件 - Interaction
Interaction主要是指我们引用的第三方库,例如element-ui,iview等,主旨强调封装和高复用。
4.功能型组件 - Funtion
功能型组件作为一种抽象或者扩展存在的机制,在vue的场景下,例如路由组件,首先它不渲染任何内容,它仅是将URL路径映射到组件树的结构,它的特点允许可以在父组件中采用路由组件来声明式渲染其它组件,是一种第三方的扩展机制存在。
NK8 组件(划分+渲染)的复杂度
引用上一篇的例子:假设我们店铺有100种水果,我们想观察15天实际和我们期望时间内销售完的情况。其功能UI原型如下:

我们来设计该组件的结构,如下

首先针对该功能,我们主要用三个组件来封装相应的模块,但项目中遇到最大的困难时TableTitle + TableBody组件的(设计+渲染)问题,因为外界因素(如条件过滤)影响,会跟新数据,从而导致UI重新渲染,如果数据很多的话会出现卡顿。
我们最初的组件设计是以 天数作为一个组件 为粒度,纵向划分,即如下图

它的渲染复杂度: O(100 * 15) ->水果的数量 * 观察的天数,即粒度为每一天的组件包含所有水果的信息,这就导致了如果每一次的数据change,会导致这15个天数的组件一起重新渲染。
而针对优化,我们若转化另一种角度来设计组件,即以 每种水果作为一个组件 为粒度,横向划分,即如下图,

它的复杂度: O(100) ->水果的数量,即一种水果包含了它所有的观察天数,这样当数据变化的时候,它的重新渲染效率由水果的数量所决定。
总结:组件在使用v-for指令进行渲染的时候,除了考虑v-key的设计,还需要衡量纵横切割组件渲染的复杂度。

三、 Vuex单向数据流管理 & UI响应式
针对Vuex的主题,接下来会从 WHY - WHAT - HOW -OPTIMIZE来分析,
即为什么项目要采用Vuex,Vuex是什么,Vuex怎么使用和实践过程中如何优化Vuex与UI的渲染的响应。

NK9 VUEX - WHY
先了解组件通信的背景,

当我们采用组件化开发模式时,为了能高效管理数据流动的情况,会约束组件与组件之间通信需要采用单向数据流的模式,即组件Container会通过属性绑定把数据传递给子组件,而如果子组件想要修改传入的数据必须通过事件回调($emit)和父组件通信。但是这种模式会有个弊端,即子组件可能会包含自己的子子组件,而子子组件又包含子子子组件……
这样当项目越来越复杂的时候,就会出现以下痛点:
(1)多个组件依赖于同一个状态
(2)来自不同组件的行为需要变更同一个状态
即当组件的层级越来越深,会造成父组件可能会与很远的组件之间共享同份数据,当很远的组件需要修改数据时,就会造成事件回调需要层层返回,这就是灾难,造成代码很难维护。所以有没有方案可以解决这个痛点呢?
答案是有的,就是我们首先先抛开 数据层层传递,我们迫切希望有个这样的一个第三方库,我们所有的组件都能从这个地方进行获取和修改,让这个第三方库能统一维护这份数据的状态,我们称为状态管理库,Vuex是专门为Vue.js设计第三方,以利用 Vue.js 的细粒度数据(属性监听)响应机制来进行高效的状态更新。


NK10 Vuex - WHAT
什么是Vuex?
Vuex是一个数据控制中心,集中式存储管理应用的所有组件的状态,而状态的存储是响应式的,即当状态跟新的时候,它能高效通知组件跟新。
生命周期:在创建Vue实例应用的时候,注入Store,提供了一种机制将状态从根组件“注入”到每一个子组件中。
但当我们组件使用computed + mapGetter/mapMutations…等辅助函数绑定使用时,它只在Vue2.X生命周期created的时候才会被初始化。
核心概念
1.State

State是一个对象,存储该模块的所有状态,相当于data属性,最好在store中申明好所有属性。否则需要用vue.set(state,属性,普通值/对象值/数组值) 来让Vue进行依赖收集。
另外一点,在组件引用的时候,它只提供读的属性,不允许组件直接修改状态。
2.Getter

(1)跟computed计算属性一样,getter返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变,才会被重新计算。
复用:当State状态需要做业务逻辑处理时,而不需要在每个组件都重复申明时,可以在Getter方法中声明,这样当提供给多个组件调用时,可以很好维护。
(2)接受的参数: state ,getters ,rootState, rootGetters =》根据根State或者根Getter可以获取其它模块的数据状态
(3)Getter返回一个函数,来实现给getter传参。 UI可以使用Computer的方式来声明,然后调用的时候直接传参就可以。
3.Mutation

4.Action

5.Module
状态管理库也可以进行模块化分装,使模块内部的Action,State,Mutation和Getter被带上“命名”被注册。
属性配置 -》 namespaced:true
UI其它小技巧实践:

利用computed的特性,用get和set来获取和设值。

NK11 VUEX - HOW
如何在项目中实践Vuex呢?
- 单向数据流
项目中的一个功能组件,原先是在根组件存储所有数据的状态,由于后面需求的增加,需要在其它同级组件维护同一份数据,所以一开始的推翻宏图设计如下,将基础数据先存入Store里面,接着任何组件的改变,都通过提交一个action,在Store里面直接通过业务逻辑更改数据,然后响应所有组件变化。

2. 采用面向对象思想来管理对象的状态
但是由于一个项目会有很多不同领域的开发人员参与进来,其采用的开发模式思想也会跟着不一样,在对此模块重构过程中,有些同事在基于Vuex的基础上,在State对象中添加的属性是采用面向对象的方式封装的一个独立Calculator对象。
实现方式:在Store中先申明该属性为一个对象,该对象封装自己属性和方法,在使用方面,先是在组件中申明调用该对象,通过调用该对象的内部方法,来同步更改对象属性状态。
而其好处是允许多个组件维护同一份业务逻辑处理方式,需要考虑不同的场景是否需要封装可复用的对象。

NK12 单向数据流的状态管理实现

父组件负责渲染Store的数据状态,然后通过props传递数据到子组件中,子组件触发事件提交更改状态的action, Store可以在Dispatcher上监听到Action并做出相应的操作,当数据模型发生变化时,就触发刷新整个父组件界面。

NK13 Vuex State & UI渲染
State存储着UI某个模块的数据状态,所以当在设计一个属性为一个对象的时候,需要特别注意注意,当这个对象属性由于外部条件(数据过滤)的影响,仅发生某条数据状态变化,而其他数据不变的话,是会响应这个对象属性的变化,导致引用该对象属性的组件会被回调, UI会重新渲染。
例如UI功能展示和数据结构如下,

const state = {
shops: {
商铺A: {
startDate: "2018-11-01",
endDate: "2018-11-30",
loading: false,
diplayMoreFruitsLink: true,
fruits: [30],
},
商铺B:{
startDate: "2018-11-01",
endDate: "2018-11-30",
loading: false,
diplayMoreFruitsLink: true,
fruits: [100],
},
商铺C:{……},
}
};
const getters = {
getShopByKey: (state, getters, rootState, rootGetters) => {
return key => {
return state.shops[key];
};
},
};
当模拟将商铺A和展示的UI来进行绑定
圆圈A- Vuex Action
圆圈S- Vuex State
圆圈G- Vuex Getter
圆圈C- Component,其代码调用如下

当用户在对商铺A做一些Filter的UI操作,例如想看其它时间段的销售情况,这时UI会提交一个SET_MULTIPLE_TIME_RANGE的Mutation事件,backend会重新加载商铺A的Fruits的信息。即属性Fruits信息需要改变 -> 商铺A数据跟新 -> State对象中的shops 对象属性跟新,这时Getter监听到变化后,会通知绑定的组件(商铺A,商铺B,商铺C),然后UI响应变化。
-》 问题:应该只需要渲染商铺A的信息,而商铺B和C应该不需要。
在加载Fruits的信息的时候,我们一般会加一个loading的状态,这时也会触发提交一个 SET_MULTIPLE_LOADING 的Mutation事件,即当商铺A的组件处于加载状态,等加载完了,也会触发shops对象属性的跟新,然后Fruits绑定的UI组件(商铺A,商铺B,商铺C)会被触发重新渲染。
-》 问题:商铺A,B和C应该不需要渲染。
这只是一个商铺A的信息跟新情况,试想下该对象属性shops如果挂载很多个商铺的话,UI会发生什么呢?
答案:如果组件绑定Getter的getStopByKey方法的话,都会被触发,例如像商铺B,商铺C的组件会响应变化,这样会造成JS执行的时间太长,导致UI出现卡顿现象。
优化解决方案:主要优化State的属性响应设计

const state = {
shops: {
商铺A: {
startDate: "2018-11-01",
endDate: "2018-11-30",
loading: false,
diplayMoreFruitsLink: true,
},
商铺B:{
startDate: "2018-11-01",
endDate: "2018-11-30",
loading: false,
diplayMoreFruitsLink: true,
},
商铺C:{……},
},
fruits_商铺A: [30],
fruits_商铺B: [30],
fruits_商铺C: [30],
};
总结关键字:
(1)复用策略 -》 移动算法优化
(2)组件划分
(3)Vuex -》变化数据与UI双向绑定
(4)ES6 proxy对对象的监听