Vue 实用开发技巧

9,715 阅读11分钟

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. 如下图所示:

images.png

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

images.png

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

images.png

此时, 你会发现这里有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

images.png

修改Hello.vuecreated钩子函数中的代码如下:

created () {
  let users = Array.from({ length: 100000 }, (item, index) => ({ name: index }))
  this.users = Object.freeze(users)
}

再次点击"reload"按钮, 重新测试性能.

images.png

此时, 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组件时, 经常会遇到:

  • 组件层层传递propslisterers
  • 动态绑定propslisterers

有没有什么办法可以解决以上两种场景的问题呢?

::: tip v-bindv-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中, 然后从watchcreated钩子函数中分别调用它, 但有没有更简单一点的办法呢?

你可以在使用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. provide / inject

2.2.0 新增

类型:

  • provide:Object | () => Object
  • inject:Array | { [key: string]: string | Symbol | Object }

注意: provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。

inject 选项应该是:

  • 一个字符串数组,或
  • 一个对象,对象的 key 是本地的绑定名,value 是:
    • 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
    • 一个对象,该对象的:
      • from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
      • default 属性是降级情况下使用的 value

提示: provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

示例:

// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

利用 ES2015 Symbols、函数 provide 和对象 inject:

const s = Symbol()

const Provider = {
  provide () {
    return {
      [s]: 'foo'
    }
  }
}

const Child = {
  inject: { s },
  // ...
}

在 2.5.0+ 的注入可以通过设置默认值使其变成可选项:

const Child = {
  inject: {
    foo: { default: 'foo' }
  }
}

如果它需要从一个不同名字的属性注入,则使用 from 来表示其源属性:

const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: 'foo'
    }
  }
}

与 prop 的默认值类似,你需要对非原始值使用一个工厂方法:

const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: () => [1, 2, 3]
    }
  }
}

12. 调试 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>

此时, 我们将可以看到断点定位到了模板的渲染函数中了.

images.png

此时的_vm, 就是我们组件的实例对象.

检查编译的模板虽然很有意思, 但由于某些原因, 变量在被我们放在debugger后, 在chrome devtools的函数范围内变得不可用.

修改下写法:

<h1>
  {{ (function(){degugger; message}) || message }}
</h1>

此时, 你就可以随心所欲了.

images.png

13. 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 lang="less" 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属性后, 父组件无法修改子组件的样式. :::

14. Vue组件样式之 deep选择器

如上例中, 若想在父组件中修改子组件的样式, 怎么办呢?

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

这里我们主要讲解使用deep修改子组件的样式. 将上例的代码修改为:

<template>
  <div class="demo">
    <span class="content">
      Vue.js scoped
    </span>
  </div>
</template>

<style lang="less" 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选择器权重问题的同学, 对此应该立即明白了吧!

15. 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 exceptionscomposition 等。

可选用法

如果你只想在某些 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>

16. 始终验证Props

验证Props是Vue中的基本做法. 可能你知道如何做一些原始数据类型的验证, 如: String, Number等, 当然, 你也可以自定义验证函数, 例如:

props: {
  name: {
    type: String,
    required: true,
    validator: function(value) {
      return [
        '张三',
        '李四',
        '王五'
      ].indexOf(value) !== -1
    }
  }
}

17. 自定义v-model

默认情况下, v-model@input事件侦听器和:value属性的语法糖. 但是, 你也可以在你的Vue组件中指定一个模型属性来定义什么事件和value属性.

export defaut {
  model: {
    event: 'change',
    prop: 'checked'
  }
}

18. 动态指令参数

2.6+新增 Vue 2.6可以将指令参数动态传递给组件. 例如, 你有一个按钮组件, 并在某些情况下需要监听单击事件, 某些情况下需要监听双击事件, 可以考虑这么写:

<template>
  <div>
    <div>hellojames</div>
    <button v-on:[eventType]="eventHandler($event)">xxx</button>
  </div>
</template>
<script>
export default {
  data () {
    return {
      someCondition: 1
    }
  },
  computed: {
    eventType () {
      return this.someCondition ? 'click' : 'dblclick'
    }
  },
  methods: {
    eventHandler ($event) {
      // todo something
      window.console.log($event)
    }
  }
}
</script>

这样, 你就可以将相同的模式应用于动态HTML属性, props等

相关链接