阅读 102

深入Vue.js之代码优化

注:本篇文章是为自己为了方便学习,从各大文章中摘抄的,文章末尾已标注文章来源,可查看原文。

1. 利用v-if和v-show减少初始化渲染和切换渲染的性能开销

在页面加载时,利用v-if来控制组件仅在首次使用时渲染减少初始化渲染,随后用v-show来控制组件显示隐藏减少切换渲染的性能开销

demo:

<template>
    <div>
        <Button type="primary" @click.native="add">添加</Button>
        <add v-model="add.show" v-bind="add"></add>
    </div>
</template>
<script>
export default{
    data(){
        return{
            add:{
                show:false,
                init:false
            }
        }
    },
    components:{
        add:() =>import('./add.vue')
    },
    methods:{
        add(){
            this.add.show=true;
            this.add.init=true
        }
    }
}
</script>
复制代码
<template>
    <div v-if="init">
        <Modal v-model="show" title="添加" @on-cancel="handleClose"></Modal>
    </div>
</template>
<script>
export default{
    props:{
        value:{
            type:Boolean,
            default:false
        },
        init:{
            type:Boolean,
            default:false
        }
    },
    data(){
        return{
            show:false,
        }
    },
    watch:{
        value(val){
            if(val){
                this.show = val;
            }
        }  
    },
    methods:{
        handleClose(val) {
            this.$emit('input', val);
        },
    }
}
</script>
复制代码

v-if绑定值为false时,初始渲染时,不会渲染其条件块。

v-if绑定值,在true和false之间切换时,会销毁和重新渲染其条件块。

v-show绑定值不管为true还是为false,初始渲染时,总是会渲染其条件块。

v-show绑定值,在true和false之间切换时,不会销毁和重新渲染其条件块,只是用display:none样式来控制其显示隐藏。

2、computed、watch、methods区分使用场景

对于有些需求,computedwatchmethods都可以实现,但是还是要区分一下使用场景。用错场景虽然功能实现了但是影响了性能。

  • computed:

    • 一个数据受多个数据影响的。
    • 该数据要经过性能开销比较大的计算,如它需要遍历一个巨大的数组并做大量的计算才能得到,这时就可以利用computed的缓存特性,只有它计算时依赖的数据发现变化时才会重新计算,否则直接返回缓存值。
  • watch:

    • 一个数据影响多个数据的。
    • 当数据变化时,需要执行异步或开销较大的操作时。如果数据变化时请求一个接口。
  • methods:

    • 希望数据是实时更新,不需要缓存。

3、提前处理好数据解决v-if和v-for必须同级的问题

因为当Vue处理指令时,v-forv-if 具有更高的优先级,意味着 v-if 将分别重复运行于每个 v-for 循环中。

可以在computed中提前把要 v-for 的数据中 v-if 的数据项给过滤处理了。

//userList.vue
<template>
    <div>
        <div v-for="item in userList" :key="item.id" v-if="item.age > 18">{{ item.name }}</div>
    </div>
</template>
复制代码
//userList.vue
<template>
    <div>
        <div v-for="item in userComputedList" :key="item.id">{{ item.name }}</div>
    </div>
</template>
export default {
    computed:{
        userComputedList:function(){
            return this.userList.filter(function (item) {
                return item.age > 18
            })
        }
    }
}
复制代码

也许面试官还会为什么v-for比v-if具有更高的优先级?这个问题已经涉及到原理层次,如果这个也会回答,会给面试加分不少。这里不再详述,可参考原文:提前处理好数据解决v-if和v-for必须同级的问题

4、给v-for循环项加上key提高diff计算速度

具体原理参考原文:给v-for循环项加上key提高diff计算速度

8、避免在v-for循环中读取data中数组类型的数据

export function defineReactive(obj,key,val,customSetter,shallow){
    const dep = new Dep()
    const property = Object.getOwnPropertyDescriptor(obj, key)
    const getter = property && property.get;
    const setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }
    let childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
        const value = getter ? getter.call(obj) : val
        if (Dep.target) {
            dep.depend()
            if (childOb) {
                childOb.dep.depend()
                if (Array.isArray(value)) {
                    dependArray(value)
                }
            }
        }
        return value
    }
    })
}
function dependArray (value: Array<any>) {
    for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i]
        e && e.__ob__ && e.__ob__.dep.depend()
        if (Array.isArray(e)) {
            dependArray(e)
        }
    }
}
export function observe (value, asRootData){
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    //...
}
复制代码

为什么要避免在v-for循环中读取data中数组类型的数据,因为在数据劫持中会调用defineReactive函数中。由于 getter是函数,并且引用了 depchildOb,形成了闭包,所以 depchildOb 一直存在于内存(每个数据的getter函数)中,dep是每个数据的依赖收集容器,childOb是经过响应式处理后的数据。

渲染视图使用watch监听、使用计算属性过程中读取数据,都会对Dep.target进行赋值,其值为Watcher(依赖),例如在渲染视图过程中读取数据时,Dep.target为renderWatcher。

接着先调用dep.depend()给自身收集依赖,如果val(自身的值)不是对象,则childObfalse。如果val(自身的值)是对象,用childOb.dep.depend()收集依赖,若val(自身的值)是数组用dependArray(value)递归每一项来收集依赖。

为什么要避免在v-for循环中读取data中数组类型的数据,其原因就是若val(自身的值)是数组用dependArray(value)递归每一项来收集依赖

举个简单的栗子,表格中每行有两个输入框,分别可以输入驾驶员和电话,代码这么实现。

<template>
    <div class="g-table-content">
        <el-table :data="tableData">
            <el-table-column prop="carno" label="车牌号"></el-table-column>
            <el-table-column prop="cartype" label="车型"></el-table-column>
            <el-table-column label="驾驶员">
                <template slot-scope="{row,column,$index}">
                    <el-input v-model="driverList[$index].name"></el-input>
                </template>
            </el-table-column>
            <el-table-column label="电话">
                <template slot-scope="{row,column,$index}">
                    <el-input v-model="driverList[$index].phone"></el-input>
                </template>
            </el-table-column>
        </el-table>
    </div>
</template>
复制代码

假设表格有500条数据,那么读取driverList共500次,每次都读取driverList都会进入dependArray(value)中,总共要循 500*500=25万次,若有分页,每次切换页码,都会至少循环25万次。 如果我们在从服务获取到数据后,做了如下预处理,在赋值给this.tableData,会是怎么样?

res.data.forEach(item =>{
    item.name='';
    item.phone='';
})
复制代码

模板这样实现

<template>
    <div class="g-table-content">
        <el-table :data="tableData">
            <el-table-column prop="carno" label="车牌号"></el-table-column>
            <el-table-column prop="cartype" label="车型"></el-table-column>
            <el-table-column label="驾驶员">
                <template slot-scope="{row}">
                    <el-input v-model="row.name"></el-input>
                </template>
            </el-table-column>
            <el-table-column label="电话">
                <template slot-scope="{row,column,$index}">
                    <el-input v-model="row.phone"></el-input>
                </template>
            </el-table-column>
        </el-table>
    </div>
</template>
复制代码

也可以实现需求,渲染过程中求值时也不会进入dependArray(value)中,也不会造成25万次的不必要的循环。大大提高了性能。

5、利用v-once处理只会渲染一次的元素或组件

只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

例如某个页面是合同范文,里面大部分内容从服务端获取且是固定不变,只有姓名、产品、金额等内容会变动。这时就可以把v-once添加到那些包裹固定内容的元素上,当生成新的合同可以跳过那些固定内容,只重新渲染姓名、产品、金额等内容即可。

v-if一起使用时,v-once不生效。在v-for循环内的元素或组件上使用,必须加上key

讲到此优化,要防止面试官问你v-once怎么实现只渲染一次元素或组件?

说到渲染应该和render函数有关,那要去生成render函数的地方去寻找答案。

具体原理参考原文:利用v-once处理只会渲染一次的元素或组件

6. v-pre

场景:vue 是响应式系统,但是有些静态的标签不需要多次编译,这样可以节省性能

<span v-pre>{{ this will not be compiled }}</span>   显示的是{{ this will not be compiled }}
<span v-pre>{{msg}}</span>     即使data里面定义了msg这里仍然是显示的{{msg}}
复制代码

7. v-cloak

场景:在网速慢的情况下,在使用vue绑定数据的时候,渲染页面时会出现变量闪烁
用法:这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕

// template 中
<div class="#app" v-cloak>
    <p>{{value.name}}</p>
</div>

// css 中
[v-cloak] {
    display: none;
}
复制代码

这样就可以解决闪烁,但是会出现白屏,这样可以结合骨架屏使用

8. Vue.config.errorHandler

  1. 场景:指定组件的渲染和观察期间未捕获错误的处理函数
  2. 规则:

从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子是 undefined 时,被捕获的错误会通过 console.error 输出而避免应用崩溃
从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了
从 2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理
3. 使用

Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 只在 2.2.0+ 可用
}
复制代码

9. Vue.config.warnHandler

2.4.0 新增

  1. 场景:为 Vue 的运行时警告赋予一个自定义处理函数,只会在开发者环境下生效
  2. 用法:
Vue.config.warnHandler = function (msg, vm, trace) {
  // `trace` 是组件的继承关系追踪
}
复制代码

10. 利用Object.freeze()冻结不需要响应式变化的数据

Vue初始化过程中,会把data传入observe函数中进行数据劫持,把data中的数据都转换成响应式的。
复制代码

observe函数内部调用defineReactive函数处理数据,配置getter/setter属性,转成响应式,如果使用Object.freeze()将data中某些数据冻结了,也就是将其configurable属性(可配置)设置为false

defineReactive函数中有段代码,检测数据上某个key对应的值的configurable属性是否为false,若是就直接返回,若不是继续配置getter/setter属性。

export function defineReactive(obj,key,val,customSetter,shallow){
    //...
    const property = Object.getOwnPropertyDescriptor(obj, key)//获取obj[key]的属性
    if (property && property.configurable === false) {
        return
    }
    //...
}
复制代码

在项目中如果遇到不需要响应式变化的数据,可以用Object.freeze()把该数据冻结了,可以跳过初始化时数据劫持的步骤,大大提高初次渲染速度。

11. 提前过滤掉非必须数据,优化data选项中的数据结构

Vue初始化时,会将选项data传入observe函数中进行数据劫持

initData(vm){
    let data = vm.$options.data
    //...
    observe(data, true)
}
复制代码

在observe函数会调用

observe(value,asRootData){
   //...
   ob = new Observer(value);
}
复制代码

Observer原型中defineReactive函数处理数据,配置getter/setter属性,转成响应式

walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
    }
}
复制代码

defineReactive函数中,会将数据的值再次传入observe函数中

export function defineReactive(obj,key,val,customSetter,shallow){
    //...
    if (arguments.length === 2) {
        val = obj[key]
    }
    let childOb = observe(val);
    //...
}
复制代码

observe函数中有段代码,将数据传入,Observer类中。

export function observe(value,asRootData){
  //...
  ob = new Observer(value)
  //...
  return ob
}
复制代码

以上构成了一个递归调用。

接收服务端传来的数据,都会有一些渲染页面时用不到的数据。服务端的惯例,宁可多传也不会少传。

所以要先把服务端传来的数据中那些渲染页面用不到的数据先过滤掉。然后再赋值到data选项中。可以避免去劫持那些非渲染页面需要的数据,减少循环和递归调用,从而提高渲染速度。

12. 防抖和节流

防抖和节流是针对用户操作的优化。首先来了解一下防抖和节流的概念。

  • 防抖:触发事件后规定时间内事件只会执行一次。简单来说就是防止手抖,短时间操作了好多次。
  • 节流:事件在规定时间内只执行一次。
  • 应用场景: 节流不管事件有没有触发还是频繁触发,在规定时间内一定会只执行一次事件,而防抖是在规定时间内事件被触发,且是最后一次被触发才执行一次事件。假如事件需要定时执行,但是其他操作也会让事件执行,这种场景可以用节流。假如事件不需要定时执行,需被触发才执行,且短时间内不能执行多次,这种场景可以用防抖。

可以通过引用Lodash工具库里面的debounce防抖函数和throttle节流函数。

import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
export default{
    methods:{
        a: debounce(function (){
            //...
        },200,{
            'leading': false,
            'trailing': true
        }),
        b: throttle(function (){
            //...
        },200,{
            'leading': false,
            'trailing': true
        })
    }
}
复制代码
  • debounce(func, [wait=0], [options={}]) 创建一个防抖函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法。返回一个防抖函数debounceFndebounce.cancel取消防抖,debounce.flush 立即调用该func。

    • options.leading为false时,func在延迟开始前调用。
    • options.trailing为true时,func在延迟开始结束后调用。
    • options.maxWait设置func 允许被延迟的最大值。
  • throttle(func, [wait=0], [options={}]) 创建一个节流函数,在 wait 秒内最多执行 func 一次的函数。 返回一个节流函数throttleFnthrottleFn.cancel取消节流,throttleFn.flush 立即调用该func。

    • options.leading为true时,func在节流开始前调用。
    • options.trailing为true时,func在节流结束后调用。
    • leading和trailing都为true,func在wait期间多次调用。

13. 图片大小优化和懒加载

关于图片大小的优化,可以用image-webpack-loader进行压缩图片,在webpack插件中配置。

关于图片懒加载,可以用vue-lazyload插件实现。

执行命令npm install vue-lazyload --save安装vue-lazyload插件。在main.js中引入配置

import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload, {
  preLoad: 1.3,//预载高度比例
  error: 'dist/error.png',//加载失败显示图片
  loading: 'dist/loading.gif',//加载过程中显示图片
  attempt: 1,//尝试次数
})
复制代码

在项目中使用

<img v-lazy="/static/img/1.png">
复制代码

14. 组件懒加载

性能优化之组件懒加载: Vue Lazy Component 介绍

15. 异步组件

场景: 项目过大就会导致加载缓慢,所以异步组件实现按需加载就是必须要做的事啦

  1. 异步注册组件
// 工厂函数执行 resolve 回调
Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包, 这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})

// 工厂函数返回 Promise
Vue.component(
  'async-webpack-example',
  // 这个 `import` 函数会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

// 工厂函数返回一个配置化组件对象
const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})
复制代码

异步组件的渲染本质上其实就是执行2次或者2次以上的渲染, 先把当前组件渲染为注释节点, 当组件加载成功后, 通过 forceRender 执行重新渲染。或者是渲染为注释节点, 然后再渲染为loading节点, 在渲染为请求完成的组件

  1. 路由的按需加载
webpack< 2.4 时
{
  path:'/',
  name:'home',
  components:resolve=>require(['@/components/home'],resolve)
}

webpack> 2.4 时
{
  path:'/',
  name:'home',
  components:()=>import('@/components/home')
}

import()方法由es6提出,import()方法是动态加载,返回一个Promise对象,then方法的参数是加载到的模块。类似于Node.js的require方法,主要import()方法是异步加载的。
复制代码

16. 动态组件

解决组件的if,else太多问题。看如下demo:

<template>
  <div class="info">
    <component :is="roleComponent" v-if="roleComponent" />
  </div>
</template>
<script>
import AdminInfo from './admin-info'
import BookkeeperInfo from './bookkeeper-info'
import HrInfo from './hr-info'
import UserInfo from './user-info'
export default {
  components: {
    AdminInfo,
    BookkeeperInfo,
    HrInfo,
    UserInfo
  },
  data() {
    return {
      roleComponents: {
        admin: AdminInfo,
        bookkeeper: BookkeeperInfo,
        hr: HrInfo,
        user: UserInfo
      },
      role: 'user',
      roleComponent: undefined
    }
  },
  created() {
    const { role, roleComponents } = this
    this.roleComponent = roleComponents[role]
  }
}
</script>
复制代码

17. 利用挂载节点会被替换的特性优化白屏问题

import Vue from 'vue'
import App from './App.vue'
new Vue({
    render: h => h(App)
}).$mount('#app')
复制代码
Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板
编译渲染函数。
复制代码

也就是说渲染时,会直接用render渲染出来的内容替换<div id="app"></div>

Vue项目有个缺点,首次渲染会有一段时间的白屏原因是首次渲染时需要加载一堆资源,如js、css、图片。很多优化策略,最终目的是提高这些资源的加载速度。但是如果遇上网络慢的情况,无论优化到极致还是需要一定加载时间,这时就会出现白屏现象。

首先加载是index.html页面,其实没有内容,就会出现白屏。如果<div id="app"></div>里面有内容,就不会出现白屏。所以我们可以在<div id="app"></div>里添加首屏的静态页面。等真正的首屏加载出来后就会把<div id="app"></div>这块结构都替换掉,给人一种视觉上的误差,就不会产生白屏。

12、组件库的按需引入 这里不再详述,可参考各ui库官方网站。或查看原文:组件库的按需引入

18. 安装VuePerformanceDevtool(vue性能开发工具)

这个Chrome扩展可以让你检测vue组件的性能,也是一个非常有用的工具,可以通过这个链接安装://chrome.google.com/webstore/detail/vue-performance-devtool/koljilikekcjfeecjefimopfffhkjbne

安装好了,你得在代码里加上这么一句,才能启用它:

Vue.config.performance = true;
复制代码

记得要将其加在new Vue实例之前,当然了,这样的话,你生产环境上也就开启这个工具了,这往往不是我们想要的,你可以基于环境监测来决定是否开启这个,可以用下面的代码实现:

Vue.config.performance = process.env.NODE_ENV !== 'production'
复制代码

19. 混入

1. 不同位置的混入规则

在Vue中,一个混入对象可以包含任意组件选项,但是对于不同的组件选项,会有不同的合并策略。

  1. data

对于data,在混入时会进行递归合并,如果两个属性发生冲突,则以组件自身为主 2. 生命周期钩子函数
对于生命周期钩子函数,混入时会将同名钩子函数加入到一个数组中,然后在调用时依次执行。混入对象里面的钩子函数会优先于组件的钩子函数执行。如果一个组件混入了多个对象,对于混入对象里面的同名钩子函数,将按照数组顺序依次执行,如下代码:

const mixin1 = {
  created() {
    console.log('我是第一个输出的')
  }
}

const mixin2 = {
  created() {
    console.log('我是第二个输出的')
  }
}
export default {
  mixins: [mixin1, mixin2],
  created() {
    console.log('我是第三个输出的')
  }
}
复制代码
  1. 其它选项

对于值为对象的选项,如methods,components,filter,directives,props等等,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

2.全局混入

混入也可以进行全局注册。一旦使用全局混入,那么混入的选项将在所有的组件内生效,如下代码所示:

Vue.mixin({
  methods: {
    /**
     * 将埋点方法通过全局混入添加到每个组件内部
     * 
     * 建议将埋点方法绑定到Vue的原型链上面,如: Vue.prototype.$track = () => {}
     * */
    track(message) {
      console.log(message)
    }
  }
})
复制代码

请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项

20. vue-loader 小技巧

1. preserveWhitespace

场景:开发 vue 代码一般会有空格,这个时候打包压缩如果不去掉空格会加大包的体积 配置preserveWhitespace可以减小包的体积

{
  vue: {
    preserveWhitespace: false
  }
}
复制代码

2. transformToRequire

场景:以前在写 Vue 的时候经常会写到这样的代码:把图片提前 require 传给一个变量再传给组件

// page 代码
<template>
  <div>
    <avatar :img-src="imgSrc"></avatar>
  </div>
</template>
<script>
  export default {
    created () {
      this.imgSrc = require('./assets/default-avatar.png')
    }
  }
</script>
复制代码

现在:通过配置 transformToRequire 后,就可以直接配置,这样vue-loader会把对应的属性自动 require 之后传给组件

// vue-cli 2.x在vue-loader.conf.js 默认配置是
transformToRequire: {
    video: ['src', 'poster'],
    source: 'src',
    img: 'src',
    image: 'xlink:href'
}

// 配置文件,如果是vue-cli2.x 在vue-loader.conf.js里面修改
  avatar: ['default-src']

// vue-cli 3.x 在vue.config.js
// vue-cli 3.x 将transformToRequire属性换为了transformAssetUrls
module.exports = {
  pages,
  chainWebpack: config => {
    config
      .module
        .rule('vue')
        .use('vue-loader')
        .loader('vue-loader')
        .tap(options => {
      options.transformAssetUrls = {
        avatar: 'img-src',
      }
      return options;
      });
  }
}

// page 代码可以简化为
<template>
  <div>
    <avatar img-src="./assets/default-avatar.png"></avatar>
  </div>
</template>
复制代码

21 .img 加载失败

场景:有些时候后台返回图片地址不一定能打开,所以这个时候应该加一张默认图片

// page 代码
<img :src="imgUrl" @error="handleError" alt="">
<script>
export default{
  data(){
    return{
      imgUrl:''
    }
  },
  methods:{
    handleError(e){
      e.target.src=reqiure('图片路径') //当然如果项目配置了transformToRequire,参考上面 27.2
    }
  }
}
</script>
复制代码

22. 构建优化

项目打包的优化
项目部署的优化
总结我对Vue项目上线做的一些基本优化

系列文章:
深入Vue.js之实战技巧
深入Vue.js之vueJs原理

摘自:
前端面试之如何说Vue项目性能优化(万字长文)
Vue 开发必须知道的 36 个技巧【近1W字】