Vue 实用开发技巧,性能

1. 长列表性能优化
在2.x版本中Vue会通过Object.defineProperty对数据进行劫持, 以实现双向数据绑定.
但在一些特定的业务场景, 组件只需要进行纯数据展示, 不会有任何变化, 此时我们可能不需要Vue对来数据进行劫持.
在大量数据需要进行呈现时, 如果禁止Vue对数据进行劫持, 会明显减少组件初始化的时间.
::: tip
通过Object.freeze方法冻结对象, 对象一旦被冻结就不能再被修改了.
:::
export default {
  data: () => ({
    userList: []
  }),
  async created() {
    const userList = await this.$service.get("/getuserList");
    this.userList = Object.freeze(userList);
  }
};
复制代码2. Vue组件渲染性能分析
基于上面的案例(长列表性能优化), 可以通过Object.freeze来实现纯呈现的列表性能优化, 那如何来确认呢?
我们可以通过Chrome Devtools来检测. 但为了获得准确的性能分析数据, 我们需要开启Vue应用的性能模式.
开启Vue性能模式(适用于开发模式)
在工程中的main.js中(Vue根实例初始化之前), 添加以下代码:
Vue.config.performance = true;
复制代码当然, 你也可以根据需要对当前环境进行判断, 来决定是否开启性能模式.
const isDev = process.env.NODE_ENV !== "production";
Vue.config.performance = isDev;
复制代码这样, 将会激活Vue在内部用于标记组件性能的 Timing API. 如下图所示:

假设, 此时我们创建好了一个demo工程, 并有一个Hello.vue的组件, 用于验证长列表渲染性能问题. 运行本地工程后, 打开浏览器到指定路由(确认有加载Hello.vue组件). 打开控制台, 并点击"reload"按钮, 如下图所示:

此时, 将会记录页面性能. 因为已经在main.js上添加了Vue.config.performance设置,此时你将能够在分析中看到时序部分. 如下图所示.

此时, 你会发现这里有3个指标:

init, 创建组件实例所花费的时间
render, 创建vDOM结构所花费的时间
patch, 将vDOM结构渲染成实际的DOM元素所花费的时间

验证性能
在此例中, http://localhost:8080/#/hello 路由下, 只有两个组件:
App.vue
  Hello.vue
复制代码App.vue是视图组件, 只有一个<router-view/>
Hello.vue只做一个简单的长列表(100000条件数据)展示, 代码如下:
<template>
 <div>
   <span v-for="(item, idx) in users" :key="idx">
     {{item.name}}
   </span>
 </div>
</template>

<script>
export default {
  data () {
    return {
      users: []
    }
  },
  components: {

  },
  created () {
    let users = Array.from({ length: 100000 }, (item, index) => ({ name: index }))
    this.users = users
  }
}
</script>
复制代码此时, Hello.vue组件render&patch的时间为:

render -> 924ms
patch  -> 1440ms


修改Hello.vue的created钩子函数中的代码如下:
created () {
  let users = Array.from({ length: 100000 }, (item, index) => ({ name: index }))
  this.users = Object.freeze(users)
}
复制代码再次点击"reload"按钮, 重新测试性能.

此时, Hello.vue组件render&patch的时间为:

render -> 397ms (上一次测试结果为: 924ms, 节省时间: 527ms, 性能提供约为 57%)
patch  -> 782ms (上一次测试结果为: 1440ms, 节省时间: 658ms, 性能提供约为: 45.7%)

这里仅测试了一次, 但从结果来看, 增加Object.freeze冻结后, 整体性能会有明显提升.
3. 不使用Vuex创建Store(Vue.observable)

2.6.0 新增


参数:{Object} object
用法:让一个对象可响应。Vue 内部会用它来处理 data 函数返回的对象。

返回的对象可以直接用于渲染函数和计算属性内,并且会在发生改变时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:
const state = Vue.observable({ count: 0 })

const Demo = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `count is: ${state.count}`)
  }
}
复制代码我们可以利用这个API来应对一些简单的跨组件数据状态共享的情况.
// miniStore.js

import Vue from "vue";
 
export const miniStore = Vue.observable({ count: 0 });
 
export const actions = {
  setCount(count) {
    miniStore.count = count;
  }
}

export const getters = {
  count: () => miniStore.count
}

复制代码// Demo.vue
<template>
  <div>
    <p>count:{{count}}</p>
    <button @click="add"> +1 </button>
    <button @click="sub"> -1 </button>
  </div>
</template>
 
<script>
import { actions, getters } from "./store";
export default {
  name: "App",
  computed: {
    count() {
      return getters.count;
    }
  },
  methods: {
    add: actions.setCount(this.count+1),
    sub: actions.setCount(this.count-1)
  }
};
</script>
 
复制代码4. 属性&事件传递
在写Vue组件时, 经常会遇到:

组件层层传递props或listerers
动态绑定props或listerers

有没有什么办法可以解决以上两种场景的问题呢?
::: tip
v-bind和v-on, 可以实现解决上述问题
:::
代码示例如下:
<template>
  <Child v-bind="$props" v-on="$listeners"> </Child>
</template>
 
<script>
  import Child from "./Child";
  export default {
    props: {
      title: {
        required: true,
        type: String
      }
    }
    components: {
      Child
    }
  };
</script>
复制代码5. 监听函数的生命周期函数
有时, 需要在父组件监听子组件挂载后mounted, 做一些逻辑处理.
例如:
加载远端组件时, 想抓取组件从远端加载到挂载的耗时.
此时, 就不能用常规的写法, 在每个子组件中去this.$emit事件了.
有没有办法, 只需要在父组件中监听各子组件的生命周期钩子函数呢?
::: tip
@hook可以监听到子组件的生命周期钩子函数(created, updated等等).
例如: @hook:mounted="doSomething"
:::
// Parent.vue
<template>
  <Child v-bind="$props" v-on="$listeners" @hook:mounted="doSomething"> </Child>
</template>
 
<script>
  import Child from "./Child";
  export default {
    props: {
      title: {
        required: true,
        type: String
      }
    }
    components: {
      Child
    },
    methods: {
      doSomething(){
        console.log("child component has mounted!");
      }
    }
  };
</script>
复制代码6. 函数式组件
::: tip
函数式组件, 无状态,无法实例化,内部没有任何生命周期处理方法,非常轻量,因而渲染性能高,特别适合用来只依赖外部数据传递而变化的组件。
:::
写法如下:

在template标签里面标明functional
只接受props值
不需要script标签

<!-- App.vue -->
<template>
  <div>
    <UserList :users="users" :click-handler="clickHandler.bind(this)"></UserList>
  </div>
</template>
 
<script>
import UserList from "./UserList";
 
export default {
  name: "App",
  data: () => {
    users: ['james', 'ian']
  }
  components: { UserList },
  methods: {
    clickHandler(name){
      console.log(`clicked: ${name}`);
    }    
  }
};
</script>
复制代码// UserList.vue
<template functional>
  <div>
    <p v-for="(name, idx) in props.users" @click="props.clickHandler(name)" :key="idx">
      {{ name }}
    </p>
  </div>
</template>
复制代码7. 作用域插槽

在 2.6.0 中,Vue为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的特性。新语法的由来可查阅这份 RFC。

简单示例
如何使用作用域插槽呢? 请先看如下示例:
<template>
  <List :items="items">
    <template slot-scope="{ filteredItems }">
      <p v-for="item in filteredItems" :key="item">{{ item }}</p>
    </template>
  </List>
</template>
复制代码使用v-slot, 可以直接在组件标签上写入该插槽的scope.
<template>
  <List v-slot="{ filteredItems }" :items="items">
    <p v-for="item in filteredItems" :key="item">{{ item }}</p>
  </List>
</template>
复制代码::: tip
v-slot只能在组件或template标签上使用, 不能使用在普通原生的HTML标签上.
:::
这样使得代码可读性增强, 特别是在一些很难说明模板变量来源的场景中.
v-slot 高级使用
v-slot指令还引入了一种方法来组合使用slot&scoped-slot, 但需要用":"来分隔.
<template>
  <Promised :promise="usersPromise">
    <p slot="pending">Loading...</p>

    <ul slot-scope="users">
      <li v-for="user in users">{{ user.name }}</li>
    </ul>

    <p slot="rejected" slot-scope="error">Error: {{ error.message }}</p>
  </Promised>
</template>
复制代码使用v-slot重写:
<template>
  <Promised :promise="usersPromise">
    <template v-slot:pending>
      <p>Loading...</p>
    </template>

    <template v-slot="users">
      <ul>
        <li v-for="user in users">{{ user.name }}</li>
      </ul>
    </template>

    <template v-slot:rejected="error">
      <p>Error: {{ error.message }}</p>
    </template>
  </Promised>
</template>
复制代码v-slot还可以简写为 # , 重写上面的例子:
<template>
  <Promised :promise="usersPromise">
    <template #pending>
      <p>Loading...</p>
    </template>

    <template #default="users">
      <ul>
        <li v-for="user in users">{{ user.name }}</li>
      </ul>
    </template>

    <template #rejected="error">
      <p>Error: {{ error.message }}</p>
    </template>
  </Promised>
</template>
复制代码::: tip
注意, v-slot的简写是 #default
:::
8. watch
虽然Vue.js为我们提供了有用的computed, 但在某些场景下, 仍然还是需要使用到watch.
::: tip
默认情况下, watch只在被监听的属性值发生变化时执行.
:::
例如:
export default {
  data: () => ({
    dog: ""
  }),
  watch: {
    dog(newVal, oldVal) {
      console.log(`Dog changed: ${newVal}`);
    }
  }
};
复制代码如上代码所示, 只有当dog的值有发生改变时, watch中的dog函数才会执行.
但是, 在某些情况下, 你可能需要在创建组件后立即运行监听程序.
当然, 你可以将逻辑迁移至methods中, 然后从watch和created钩子函数中分别调用它, 但有没有更简单一点的办法呢?
你可以在使用watch时, 使用immediate: true选项, 这样它就会在组件创建时立即执行.
export default {
  data: () => ({
    dog: ""
  }),
  watch: {
    dog: {
      handler(newVal, oldVal) {
        console.log(`Dog changed: ${newVal}`);
      },
      immediate: true
    }
  }
};
复制代码9. 图片懒加载
v-lazy-image图片懒加载组件.
安装:
npm install v-lazy-image
使用:
// main.js
import Vue from "vue";
import { VLazyImagePlugin } from "v-lazy-image";

Vue.use(VLazyImagePlugin);
复制代码<template>
  <v-lazy-image src="http://lorempixel.com/400/200/" />
</template>
复制代码你也可以使用渐进式图像加载方式来加载图片, 通过设置src-placeholder先加载缩略图, 同时使用CSS应用自己的过滤效果.
<template>
  <v-lazy-image
    src="http://demo.com/demo.jpeg"
    src-placeholder="http://demo.com/min-demo.jpeg"
  />
</template>

<style scoped>
  .v-lazy-image {
    filter: blur(10px);
    transition: filter 0.7s;
  }
  .v-lazy-image-loaded {
    filter: blur(0);
  }
</style>
复制代码10. .sync 修饰符

2.3.0+ 新增

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。
不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。
这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。
举个例子,在一个包含 title的 prop属性的组件中,我们可以用以下方法表达对其赋新值的意图:
this.$emit('update:title', newTitle)
复制代码然后父组件可以监听那个事件并根据需要更新一个本地的数据属性。例如:
<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>
复制代码为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:
<text-document v-bind:title.sync="doc.title"></text-document>
复制代码::: danger
带有 .sync 修饰符的 v-bind 不能和表达式一起使用.
例如:
v-bind:title.sync=”doc.title + ‘!’” 是无效的。
取而代之的是,你只能提供你想要绑定的属性名,类似 v-model。
:::
当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:
<text-document v-bind.sync="doc"></text-document>
复制代码这样会把 doc 对象中的每一个属性 (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。
注意
将 v-bind.sync 用在一个字面量的对象上.
例如:
v-bind.sync=”{ title: doc.title }”,是无法正常工作的.
因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
11. 调试 Vue template
在Vue开发过程中, 经常会遇到template模板渲染时JavaScript变量出错的问题, 此时也许你会通过console.log来进行调试. 例如:
<template>
  <h1>
    {{ log(message) }}
  </h1>
</template>
<script>
methods: {
  log(message) {
    console.log(message);
  }
}
</script>
复制代码每次调试模板渲染时, 都类似重复这样写, 可能会很无聊, 有没有更好的办法呢?
在Vue.prototype原型链上添加一个自定义的方法.
// main.js
Vue.prototype.$log = window.console.log;
复制代码至止, 我们可以在每个组件的模板中使用$log, 如果我们不想影响模板的渲染, 也可以:
<h1>
  {{ log(message) || message }}
</h1>
复制代码这样是不是很方便的调试模板了?
那延展一下, 有没有办法增加一个断点, 以调试模板渲染时, 查看相关联的变量?
我们在使用模板时放入一个debugger.
<h1>
  {{ debugger }}
</h1>
复制代码你会发现, 组件根本就没有编译模板. 有没有办法呢?
我们可以尝试在模板中添加一个自执行的函数, 例如:
<h1>
  {{ (function(){degugger;}) || message }}
</h1>
复制代码此时, 我们将可以看到断点定位到了模板的渲染函数中了.

此时的_vm, 就是我们组件的实例对象.
检查编译的模板虽然很有意思, 但由于某些原因, 变量在被我们放在debugger后, 在chrome devtools的函数范围内变得不可用.
修改下写法:
<h1>
  {{ (function(){degugger; message}) || message }}
</h1>
复制代码此时, 你就可以随心所欲了.

12. Vue组件局部样式 scoped
Vue中style标签的scoped属性表示它的样式只作用于当前模块,是样式私有化, 设计的初衷就是让样式变得不可修改.
渲染的规则/原理:

给HTML的DOM节点添加一个 不重复的data属性 来表示 唯一性
在对应的 CSS选择器 末尾添加一个当前组件的 data属性选择器来私有化样式,如:.demo[data-v-2311c06a]{}
若组件内部包含其他组件,只会给其他组件的最外层标签加上当前组件的 data-v 属性

例如, 如下代码所示:
<template>
  <div class="demo">
    <span class="content">
      Vue.js scoped
    </span>
  </div>
</template>

<style  scoped>
  .demo{
    font-size: 14px;
    .content{
      color: red;
    }
  }
</style>

复制代码浏览器渲染后的代码:
<div data-v-fed36922>
  Vue.js scoped
</div>
<style type="text/css">
.demo[data-v-039c5b43] {
  font-size: 14px;
}
.demo .content[data-v-039c5b43] {
  color: red;
}
</style>
复制代码::: tip 注意
添加scoped属性后, 父组件无法修改子组件的样式.
:::
13. Vue组件样式之 deep选择器
如上例中, 若想在父组件中修改子组件的样式, 怎么办呢?

1.采用全局属性和局部属性混合的方式
2.每个组件在最外层添加一个唯一的class区分不同的组件
3.使用深层选择器deep

这里我们主要讲解使用deep修改子组件的样式. 将上例的代码修改为:
<template>
  <div class="demo">
    <span class="content">
      Vue.js scoped
    </span>
  </div>
</template>

<style  scoped>
  .demo{
    font-size: 14px;
  }
  .demo /deep/ .content{
    color: blue;
  }
</style>

复制代码最终style编译后的输出为:
<style type="text/css">
.demo[data-v-039c5b43] {
  font-size: 14px;
}
.demo[data-v-039c5b43] .content {
  color: blue;
}
</style>
复制代码从编译可以看出, 就是.content后有无添加CSS属性data-v-xxx的区别, 属性CSS选择器权重问题的同学, 对此应该立即明白了吧!
14. Vue组件局部样式 Modules
CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统。vue-loader 提供了与 CSS Modules 的一流集成,可以作为模拟 scoped CSS 的替代方案。
用法
首先,CSS Modules 必须通过向 css-loader 传入 modules: true 来开启:
// webpack.config.js
{
  module: {
    rules: [
      // ... 其它规则省略
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          {
            loader: 'css-loader',
            options: {
              // 开启 CSS Modules
              modules: true,
              // 自定义生成的类名
              localIdentName: '[local]_[hash:base64:8]'
            }
          }
        ]
      }
    ]
  }
}
复制代码然后在你的 <style> 上添加 module 特性:
<style module>
.red {
  color: red;
}
.bold {
  font-weight: bold;
}
</style>
复制代码这个 module 特性指引 Vue Loader 作为名为 $style 的计算属性,向组件注入 CSS Modules 局部对象。然后你就可以在模板中通过一个动态类绑定来使用它了:
<template>
  <p :class="$style.red">
    This should be red
  </p>
</template>
复制代码因为这是一个计算属性,所以它也支持 :class 的对象/数组语法:
<template>
  <div>
    <p :class="{ [$style.red]: isRed }">
      Am I red?
    </p>
    <p :class="[$style.red, $style.bold]">
      Red and bold
    </p>
  </div>
</template>
复制代码你也可以通过 JavaScript 访问到它:
<script>
export default {
  created () {
    console.log(this.$style.red)
    // -> "red_1VyoJ-uZ"
    // 一个基于文件名和类名生成的标识符
  }
}
</script>
复制代码你可以查阅 CSS Modules 规范了解更多细节,诸如 global exceptions 和 composition 等。
可选用法
如果你只想在某些 Vue 组件中使用 CSS Modules,你可以使用 oneOf 规则并在 resourceQuery 字符串中检查 module 字符串:
// webpack.config.js -> module.rules
{
  test: /\.css$/,
  oneOf: [
    // 这里匹配 `<style module>`
    {
      resourceQuery: /module/,
      use: [
        'vue-style-loader',
        {
          loader: 'css-loader',
          options: {
            modules: true,
            localIdentName: '[local]_[hash:base64:5]'
          }
        }
      ]
    },
    // 这里匹配普通的 `<style>` 或 `<style scoped>`
    {
      use: [
        'vue-style-loader',
        'css-loader'
      ]
    }
  ]
}
复制代码和预处理器配合使用
CSS Modules 可以与其它预处理器一起使用:
// webpack.config.js -> module.rules
{
  test: /\.scss$/,
  use: [
    'vue-style-loader',
    {
      loader: 'css-loader',
      options: { modules: true }
    },
    'sass-loader'
  ]
}
复制代码自定义的注入名称
在 .vue 中你可以定义不止一个 <style>,为了避免被覆盖,你可以通过设置 module 属性来为它们定义注入后计算属性的名称。
<style module="a">
  /* 注入标识符 a */
</style>

<style module="b">
  /* 注入标识符 b */
</style>

作者:James Zhang
链接:https://juejin.im/post/5d790819e51d453b5e465bc7
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。