# Vue 开发技巧
# 1. 化繁为简的 Watch
watch: {
myProperty() {
this.doSomething();
}
},
created() {
this.doSomething();
},
methods: {
doSomething() {
console.log('doing something...');
}
}
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里面声明了
}
}
}
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: value 和 events: 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>
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" />
使用 v-model,可以很方便地在子组件中同步父组件的数据。在 2.2 之后的版本中,可以定制 v-model 指令的 prop 和 event 名称,参考 model 配置项
export default {
model: {
prop: 'value',
event: 'input'
}
// ...
}
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'])
}
}
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)
})
2
3
4
5
6
7
8
9
10
11
# 4. 利用hook监听子组件的生命周期
仅能在 vue2.x 版本里可以使用,在 3.x 里不识别该方法,出现报错
<!-- parent component -->
<template>
<child @hook:created="listenCreated" />
<child @hook:mounted="listenMounted" />
</template>
2
3
4
5
# 5. CSS Module
在.vue文件中使用module,CSS Modules 为每一个局部类赋予带 hash 的全局唯一的类名,这样组件样式间就不会相互影响了。
# 5.1 使用 Boolean 值传入对象
<el-input :class="{ [$style.readonly]: isUpdate }" />
# 5.2 传入单个
<el-input :class="$style.isUpdate" />
# 5.3 传入多个
# 5.3.1 使用数组
<el-input :class="[isUpdate && [$style.readonly], $style.test]" />
# 5.3.2 使用对象
<el-input :class="{ [$style.readonly]: isUpdate, [$style.test]: true }"/>
# 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
2
3
4
5
6
7
8
9
10
11
# 6. 组件内利用hook钩子回调
# 6.1 常规方法的缺陷:
- vue实例中需要额外声明一个定时器变量实例timer
- 创建的定时器代码和销毁定时器的代码没有放在一起,容易遗漏
//
export default{
data(){
timer:null
},
mounted(){
this.timer = setInterval(()=>{
//some code
},1000)
},
beforeDestory(){
clearInterval(this.timer)
this.timer = null
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6.2 利用hook钩子:
- 该钩子配合
$once函数 - 优先级低于选项参数
beforeDestroy()
//
export default{
mounted(){
this._launch()
},
methods: {
_launch(){
let timer = setInterval(()=>{
//some code
},1000)
// 同样的回调逻辑情况下,相对于选项参数 beforeDestroy(), 下面的处理方式较后执行
this.$once('hook:beforeDestroy',()=>{
clearInterval(timer)
timer = null
})
}
}
}
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>
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>
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)" />
$attrs包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind=”$attrs”传入内部组件——在创建更高层次的组件时非常有用。
2.注意到子组件的@focus=$emit('focus', $event)"其实什么都没做,只是把event传回给父组件而已,那其实和上面类似,我完全没必要显式地申明:
<input :value="value"
v-bind="$attrs"
v-on="listeners" />
2
3
computed: {
listeners() {
return {
...this.$listeners,
input: event =>
this.$emit('input', event.target.value)
}
}
}
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)
})
}
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)
}
2
3
4
5
6