Vue 实用开发技巧

1. 长列表性能优化

在2.x版本中Vue会经过Object.defineProperty对数据进行劫持, 以实现双向数据绑定. 但在一些特定的业务场景, 组件只须要进行纯数据展现, 不会有任何变化, 此时咱们可能不须要Vue对来数据进行劫持. 在大量数据须要进行呈现时, 若是禁止Vue对数据进行劫持, 会明显减小组件初始化的时间.javascript

::: tip 经过Object.freeze方法冻结对象, 对象一旦被冻结就不能再被修改了. :::css

export default {
  data: () => ({
    userList: []
  }),
  async created() {
    const userList = await this.$service.get("/getuserList");
    this.userList = Object.freeze(userList);
  }
};
复制代码

2. Vue组件渲染性能分析

基于上面的案例(长列表性能优化), 能够经过Object.freeze来实现纯呈现的列表性能优化, 那如何来确认呢?html

咱们能够经过Chrome Devtools来检测. 但为了得到准确的性能分析数据, 咱们须要开启Vue应用的性能模式.vue

开启Vue性能模式(适用于开发模式)

在工程中的main.js中(Vue根实例初始化以前), 添加如下代码:java

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

固然, 你也能够根据须要对当前环境进行判断, 来决定是否开启性能模式.webpack

const isDev = process.env.NODE_ENV !== "production";
Vue.config.performance = isDev;
复制代码

这样, 将会激活Vue在内部用于标记组件性能的 Timing API. 以下图所示: git

images.png

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

images.png

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

images.png

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

  • 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>
复制代码

相关连接

相关文章
相关标签/搜索