# Vue 开发技巧

# 1. 化繁为简的 Watch

watch: {
  myProperty() {
    this.doSomething();
  }
},
created() {
  this.doSomething();
},
methods: {
  doSomething() {
     console.log('doing something...');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

尽管上面这段代码看起来没什么问题,但created钩子里面执行的方法是多余的。我们可以把所需要执行的方法放到watch里面,避免在created钩子里写重复代码,那将会在组件实例化的时候触发多一次。 比如像下面这样:

首先,在 watch 中,可以直接使用函数的字面量名称;其次,声明 immediate:true表示创建组件时立马执行一次。

watch: {
  myProperty: {
    immediate: true, // 该回调将会在侦听开始之后被立即调用
    handler() {
      this.doSomething();
    }
  }
},
methods: {
  doSomething() {
     console.log('doing something...');
  }
},

// 更好的方案
watch: {
  myProperty: {
    immediate: true,
    handler() {
      console.log('doing something...'); // 只用一次的方法没必要在methods里面声明了
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2. v-model 语法糖

Vue 内置了v-model指令,它其实是一个语法糖,可以拆解为 props: valueevents: input。就是说组件只要提供一个名为 value 的 prop,以及名为 input 的自定义事件,满足这两个条件,使用者就能在自定义组件上使用 v-model

<template>
  <div>
    <button @click="changeValue(-1)">-1</button>
    <span>{{ currentVal }}</span>
    <button @click="changeValue(1)">+1</button>
  </div>
</template>

<script>
export default {
  props: {
    value: {
      type: Number // 定义value属性
    }
  },
  data() {
    return {
      currentVal: this.value
    }
  },
  methods: {
    changeVal(val) {
      this.currentVal += parseInt(val)
      this.$emit('input', this.currentVal) // 定义input事件
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

然后调用的时候只需要传入 v-model 指令即可

<counter v-model="counerVal" />
1

使用 v-model,可以很方便地在子组件中同步父组件的数据。在 2.2 之后的版本中,可以定制 v-model 指令的 prop 和 event 名称,参考 model 配置项

export default {
  model: {
    prop: 'value',
    event: 'input'
  }
  // ...
}
1
2
3
4
5
6
7

# 3. createNamespacedHelpers

由函数名称不难想到该 api 的作用,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数。

这样可以省去在 mapX 方法中多次定义模块, 推荐在组件仅使用单一模块时使用此方法。

如果组件要用到多个模块,在结构时就需要使用as关键字重命名一下 mapX 方法。

import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState(['a', 'b'])
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions(['foo', 'bar'])
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
/**
 * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
 * @param {String} namespace
 * @return {Object}
 */
export const createNamespacedHelpers = namespace => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})
1
2
3
4
5
6
7
8
9
10
11

vuex 的核心函数 helpers.js

# 4. 利用hook监听子组件的生命周期

仅能在 vue2.x 版本里可以使用,在 3.x 里不识别该方法,出现报错

<!-- parent component -->
<template>
  <child @hook:created="listenCreated" />
  <child @hook:mounted="listenMounted" />
</template>
1
2
3
4
5

# 5. CSS Module

.vue文件中使用module,CSS Modules 为每一个局部类赋予带 hash 的全局唯一的类名,这样组件样式间就不会相互影响了。

# 5.1 使用 Boolean 值传入对象

<el-input :class="{ [$style.readonly]: isUpdate }" />
1

# 5.2 传入单个

<el-input :class="$style.isUpdate" />
1

# 5.3 传入多个

# 5.3.1 使用数组

<el-input :class="[isUpdate && [$style.readonly], $style.test]" />
1

# 5.3.2 使用对象

<el-input :class="{ [$style.readonly]: isUpdate, [$style.test]: true }"/>
1

# 5.4 穿透

// 作用于当前整个组件
:global
 .el-input__inner
   border-color $color-backgounrd
// 作用于顶级root类名下
.root
  :global(.el-dialog)
    display flex
    flex-direction column
  :global(.el-input__inner[readonly])
    border none
1
2
3
4
5
6
7
8
9
10
11

# 6. 组件内利用hook钩子回调

# 6.1 常规方法的缺陷:

  1. vue实例中需要额外声明一个定时器变量实例timer
  2. 创建的定时器代码和销毁定时器的代码没有放在一起,容易遗漏
//
export default{
  data(){
    timer:null  
  },
  mounted(){
	this.timer = setInterval(()=>{
	  //some code
	},1000)
  },
  beforeDestory(){
    clearInterval(this.timer)
    this.timer = null
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 6.2 利用hook钩子:

  1. 该钩子配合$once函数
  2. 优先级低于选项参数 beforeDestroy()
//
export default{
  mounted(){
	this._launch()
  },
  methods: {
    _launch(){
      let timer = setInterval(()=>{
		//some code
	  },1000)
	  
	  // 同样的回调逻辑情况下,相对于选项参数 beforeDestroy(), 下面的处理方式较后执行
	  this.$once('hook:beforeDestroy',()=>{
    	 clearInterval(timer)
    	 timer = null
	  })
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 7.高阶组件

高阶组件可以看做是函数式编程中的组合。可以把高阶组件看做是一个函数,他接收一个组件作为参数,并返回一个功能增强的组件。

高阶组件是一个接替Mixin实现抽象组件公共功能的方法,不会因为组件的使用而污染 DOM(添加并不想要的div标签等)、可以包裹任意的单一子元素等等

React中高阶组件是比较常用的组件封装形式,在 Vue 中如何实现高阶组件呢?

在组件的render函数中,只需要返回一个vNode数据类型即可,如果在render函数中提前做一些处理,并返回this.$slots.default[0]对应的vnode,就可以实现高阶组件。

Vue 内置了一个高阶组件keep-alive,查看源码可以发现其实现原理,就是通过维护一个cache,并在render函数中根据key返回缓存的vnode,来实现组件的持久化。

当我们写组件的时候,通常我们都需要从父组件传递一系列的props到子组件,同时父组件监听子组件emit过来的一系列事件。举例子:

<template>
  <base-input :value="value" 
              label="密码" 
              placeholder="请填写密码" 
              @input="handleInput"
  			  @focus="handleFocus"/>
</template>
1
2
3
4
5
6
7
//子组件
<template>
  <label>
    <input :value="value" 
           :label="label" 
           :placeholder = "placeholder"
           @focus="$emit('focus', $event)"
           @input="$emit('input', $event.target.value)" />
  </label>
</template>
1
2
3
4
5
6
7
8
9
10

有下面几个优化点:

1.每一个从父组件传到子组件的props,我们都得在子组件的props中显式的声明才能使用。这样一来,我们的子组件每次都需要申明一大堆props, 而类似placeholer这种 dom 原生的property我们其实完全可以直接从父传到子,无需声明。方法如下:

<input :value="value" v-bind="$attrs" @input="$emit('input', $event.target.value)" />
1

$attrs包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind=”$attrs”传入内部组件——在创建更高层次的组件时非常有用。

2.注意到子组件的@focus=$emit('focus', $event)"其实什么都没做,只是把event传回给父组件而已,那其实和上面类似,我完全没必要显式地申明:

<input :value="value"
       v-bind="$attrs" 
       v-on="listeners" />
1
2
3
computed: {
  listeners() {
    return {
      ...this.$listeners,
      input: event =>
        this.$emit('input', event.target.value)
    }
  }
}
1
2
3
4
5
6
7
8
9

$listeners包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=”$listeners” 传入内部组件——在创建更高层次的组件时非常有用。

3.需要注意的是,由于我们input并不是BaseInput这个组件的根节点,而默认情况下父作用域的不被认作props 的特性绑定将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。所以我们需要设置inheritAttrs:false,这些默认行为将会被去掉, 以上两点的优化才能成功。

# 8. 递归组件

# 9. 函数组件

# 10. event-bus 事件总线

// global-event-bus.js
// 定义事件名称 和 触发器
export const EVENT_TODO_STH =  'EVENT_TODO_STH'

export default function useEventBus(eventName, callback) {
  Bus.$on(eventName, callback)
  this.$once('hook:beforeDestroy', () => {
    Bus.$off(eventName, callback)
  })
}
1
2
3
4
5
6
7
8
9
10
import useEventBus, { EVENT_TODO_STH } from '../global-event-bus'

// 使用总线
created() {
  useEventBus.call(this, EVENT_TODO_STH, this.deleteTodo)
}
1
2
3
4
5
6
最近更新时间: 11/27/2020, 5:42:30 PM