每天写重复代码、当 CV 仔,效率上不去。上一篇《绝对干货~!学会这些Vue小技巧,可以早点下班和女神约会了》整理过一批 Vue 小技巧;这里再补一组实战写法:hookEvent 监听生命周期、Vue.observable 小项目状态、Vue.extend 全局 Loading、自定义指令、watch 立即/深度监听,以及函数式组件。
原文:https://juejin.im/post/5eef7799f265da02cd3b82fe。文内示例按原知识点保留,便于对照落地。
一、hookEvent:监听组件生命周期
1. 内部监听
图表需求常从 echarts 示例改起:mounted 里 init 并监听 resize,beforeDestroy 再移除。两端代码隔几百行,code review 时可读性差。
<template>
<div class="echarts"></div>
</template>
<script>
export default {
mounted() {
this.chart = echarts.init(this.$el)
// 请求数据,赋值数据 等等一系列操作...
// 监听窗口发生变化,resize组件
window.addEventListener('resize', this.$_handleResizeChart)
},
updated() {
// 干了一堆活
},
created() {
// 干了一堆活
},
beforeDestroy() {
// 组件销毁时,销毁监听事件
window.removeEventListener('resize', this.$_handleResizeChart)
},
methods: {
$_handleResizeChart() {
this.chart.resize()
},
// 其他一堆方法
}
}
</script>
把两个钩子换位置仍是两处生命周期。Vue 2 其实可以用 hook 把销毁逻辑写在注册处:
大佬:这样写不是很好,应该将监听`resize`事件与销毁`resize`事件放到一起,现在两段代码分开而且相隔几百行代码,可读性比较差
我:那我把两个生命周期钩子函数位置换一下,放到一起?
大佬: `hook`听过没?
我:`Vue3.0`才有啊,咋,咱要升级`Vue`?
export default {
mounted() {
this.chart = echarts.init(this.$el)
// 请求数据,赋值数据 等等一系列操作...
// 监听窗口发生变化,resize组件
window.addEventListener('resize', this.$_handleResizeChart)
// 通过hook监听组件销毁钩子函数,并取消监听事件
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('resize', this.$_handleResizeChart)
})
},
updated() {},
created() {},
methods: {
$_handleResizeChart() {
// this.chart.resize()
}
}
}
组件内可用 $on / $once 监听全部生命周期,例如 this.$on(‘hook:updated’, () => {})。
2. 外部监听
第三方组件没有 change 时,可在模板用 @hook:updated 监听它的 updated。所有钩子都支持 @hook:钩子名。
<template>
<!--通过@hook:updated监听组件的updated生命钩子函数-->
<!--组件的所有生命周期钩子都可以通过@hook:钩子函数名 来监听触发-->
<custom-select @hook:updated="$_handleSelectUpdated" />
</template>
<script>
import CustomSelect from '../components/custom-select'
export default {
components: {
CustomSelect
},
methods: {
$_handleSelectUpdated() {
console.log('custom-select组件的updated钩子函数被触发')
}
}
}
</script>
二、小项目用 Vue.observable 代替 Vuex
多组件共享状态通常上 Vuex。官网也说:应用够简单时 Vuex 会显得繁琐。可用 Vue 2.6 的 Vue.observable 手写一份迷你 store。
创建 store
import Vue from 'vue'
// 通过Vue.observable创建一个可响应的对象
export const store = Vue.observable({
userInfo: {},
roleIds: []
})
// 定义 mutations, 修改属性
export const mutations = {
setUserInfo(userInfo) {
store.userInfo = userInfo
},
setRoleIds(roleIds) {
store.roleIds = roleIds
}
}
在组件中引用
<template>
<div>
{{ userInfo.name }}
</div>
</template>
<script>
import { store, mutations } from '../store'
export default {
computed: {
userInfo() {
return store.userInfo
}
},
created() {
mutations.setUserInfo({
name: '子君'
})
}
}
</script>
三、Vue.extend:开发全局组件
Vue.extend 是全局 API,业务里少用;做 Loading / Notify / Message 这类全局组件时正好用上。element-ui 的 loading 可以 this.$loading() 再 close()。
// 显示loading
const loading = this.$loading()
// 关闭loading
loading.close()
连调两次 loading 却只出现一个,关掉第一个另一个也没了——这是单例:
const loading = this.$loading()
const loading1 = this.$loading()
setTimeout(() => {
loading.close()
}, 1000 * 3)
下面用 Vue.extend + 单例实现同样的 loading。
开发 loading 组件
<template>
<transition name="custom-loading-fade">
<!--loading蒙版-->
<div v-show="visible" class="custom-loading-mask">
<!--loading中间的图标-->
<div class="custom-loading-spinner">
<i class="custom-spinner-icon"></i>
<!--loading上面显示的文字-->
<p class="custom-loading-text">{{ text }}</p>
</div>
</div>
</transition>
</template>
<script>
export default {
props: {
// 是否显示loading
visible: {
type: Boolean,
default: false
},
// loading上面的显示文字
text: {
type: String,
default: ''
}
}
}
</script>
直接当局部组件用时是这样,但还达不到「JS 调用 + 全屏遮罩」:
<template>
<div class="component-code">
<!--其他一堆代码-->
<custom-loading :visible="visible" text="加载中" />
</div>
</template>
<script>
export default {
data() {
return {
visible: false
}
}
}
</script>
-
-
通过 Vue.extend 转成全局组件
1. 把组件的 props 改成 data:
export default {
data() {
return {
text: '',
visible: false
}
}
}
2. 用 Vue.extend 包装:
// loading/index.js
import Vue from 'vue'
import LoadingComponent from './loading.vue'
// 通过Vue.extend将组件包装成一个子类
const LoadingConstructor = Vue.extend(LoadingComponent)
let loading = undefined
LoadingConstructor.prototype.close = function() {
// 如果loading 有引用,则去掉引用
if (loading) {
loading = undefined
}
// 先将组件隐藏
this.visible = false
// 延迟300毫秒,等待loading关闭动画执行完之后销毁组件
setTimeout(() => {
// 移除挂载的dom元素
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
}
// 调用组件的$destroy方法进行组件销毁
this.$destroy()
}, 300)
}
const Loading = (options = {}) => {
// 如果组件已渲染,则返回即可
if (loading) {
return loading
}
// 要挂载的元素
const parent = document.body
// 组件属性
const opts = {
text: '',
...options
}
// 通过构造函数初始化组件 相当于 new Vue()
const instance = new LoadingConstructor({
el: document.createElement('div'),
data: opts
})
// 将loading元素挂在到parent上面
parent.appendChild(instance.$el)
// 显示loading
Vue.nextTick(() => {
instance.visible = true
})
// 将组件实例赋值给loading
loading = instance
return instance
}
export default Loading
3. 页面里使用:
import Loading from './loading/index.js'
export default {
created() {
const loading = Loading({ text: '正在加载。。。' })
// 三秒钟后关闭
setTimeout(() => {
loading.close()
}, 3000)
}
}
要像 element-ui 一样挂到 Vue.prototype,通过 this.$loading 调用:
挂到 Vue.prototype
Vue.prototype.$loading = Loading
// 在export之前将Loading方法进行绑定
export default Loading
// 在组件内使用
this.$loading()
四、自定义指令,从底层解决问题
loading 做完后,同事又要:能挂到某个元素上(不只全屏),以及用指令在指定元素上挂 loading。
开发 v-loading 指令
import Vue from 'vue'
import LoadingComponent from './loading'
// 使用 Vue.extend构造组件子类
const LoadingContructor = Vue.extend(LoadingComponent)
// 定义一个名为loading的指令
Vue.directive('loading', {
/**
* 只调用一次,在指令第一次绑定到元素时调用,可以在这里做一些初始化的设置
* @param {*} el 指令要绑定的元素
* @param {*} binding 指令传入的信息,包括 {name:'指令名称', value: '指令绑定的值',arg: '指令参数 v-bind:text 对应 text'}
*/
bind(el, binding) {
const instance = new LoadingContructor({
el: document.createElement('div'),
data: {}
})
el.appendChild(instance.$el)
el.instance = instance
Vue.nextTick(() => {
el.instance.visible = binding.value
})
},
/**
* 所在组件的 VNode 更新时调用
* @param {*} el
* @param {*} binding
*/
update(el, binding) {
// 通过对比值的变化判断loading是否显示
if (binding.oldValue !== binding.value) {
el.instance.visible = binding.value
}
},
/**
* 只调用一次,在 指令与元素解绑时调用
* @param {*} el
*/
unbind(el) {
const mask = el.instance.$el
if (mask.parentNode) {
mask.parentNode.removeChild(mask)
}
el.instance.$destroy()
el.instance = undefined
}
})
在元素上使用
<template>
<div v-loading="visible"></div>
</template>
<script>
export default {
data() {
return {
visible: false
}
},
created() {
this.visible = true
fetch().then(() => {
this.visible = false
})
}
}
</script>
哪些场景适合自定义指令
-
-
-
-
-
五、watch:立即触发与深度监听
基础用法
列表页希望搜索框一改就查,除了听 change,也可以 watch 关键字:
<template>
<!--此处示例使用了element-ui-->
<div>
<div>
<span>搜索</span>
<input v-model="searchValue" />
</div>
<!--列表,代码省略-->
</div>
</template>
<script>
export default {
data() {
return {
searchValue: ''
}
},
watch: {
// 在值发生变化之后,重新加载数据
searchValue(newValue, oldValue) {
// 判断搜索
if (newValue !== oldValue) {
this.$_loadData()
}
}
},
methods: {
$_loadData() {
// 重新加载数据,此处需要通过函数防抖
}
}
}
</script>
立即触发
值变化会加载,但首屏还要在 created / mounted 再调一次。给 watch 配 immediate: true 即可,初次 newValue 为空串、oldValue 为 undefined。
// 改造watch
export default {
watch: {
// 在值发生变化之后,重新加载数据
searchValue: {
// 通过handler来监听属性变化, 初次调用 newValue为""空字符串, oldValue为 undefined
handler(newValue, oldValue) {
if (newValue !== oldValue) {
this.$_loadData()
}
},
// 配置立即执行属性
immediate: true
}
}
}
深度监听
表单任意一项改了就要标「已修改」。逐个属性 watch 太烦,用 deep: true。注意对象引用相同,newValue 和 oldValue 会一直相等。
export default {
data() {
return {
formData: {
name: '',
sex: '',
age: 0,
deptId: ''
}
}
},
watch: {
// 在值发生变化之后,重新加载数据
formData: {
// 需要注意,因为对象引用的原因, newValue和oldValue的值一直相等
handler(newValue, oldValue) {
// 在这里标记页面编辑状态
},
// 通过指定deep属性为true, watch会监听对象里面每一个值的变化
deep: true
}
}
}
六、函数式组件
函数式组件:无内部状态、无生命周期、无 this(不必实例化)。纯展示的详情页、列表项,只渲染外部数据时可以考虑。
示例
export default {
// 通过配置functional属性指定组件为函数式组件
functional: true,
// 组件接收的外部属性
props: {
avatar: {
type: String
}
},
/**
* 渲染函数
* @param {*} h
* @param {*} context 函数式组件没有this, props, slots等都在context上面挂着
*/
render(h, context) {
const { props } = context
if (props.avatar) {
return <img src={props.avatar}></img>
}
return <img src="default-avatar.png"></img>
}
}
上例是头像组件:有传入就显示,否则默认图。render 是 JSX 写法。
为什么用
-
不实例化、无状态、无生命周期,渲染通常快于普通组件
-
和普通组件的区别
-
-
没有
this,props / slots 在 render 第二个参数 context 上
-
-
不能
$emit,只能 context.listeners.click 调外部事件
-
-
props 可不显式声明,未声明属性会隐式当 prop;普通组件未声明的进 $attrs,并挂到根元素(可用 inheritAttrs 禁止)
不用 JSX 行不行
Vue 2.5 之后可用模板声明函数式组件,在 template 上加 functional:
<!--在template 上面添加 functional属性-->
<template functional>
<img :src="avatar ? avatar : 'default-avatar.png'" />
</template>
<!--根据上一节第六条,可以省略声明props-->
| 技巧 |
适用 |
hook:beforeDestroy |
同一处注册与注销副作用 |
@hook:updated |
外部听第三方组件生命周期 |
Vue.observable |
小项目共享状态,不必上 Vuex |
Vue.extend + 单例 |
全局 Loading / Message |
v-loading |
指令把 Loading 挂到指定元素 |
immediate / deep |
首屏加载、表单脏检查 |
| functional |
无状态展示组件 |
一句话总结:Vue 2 里用 hookEvent 把副作用收在一处,小项目用 observable 管状态,全局浮层用 extend + 单例,指令解决声明式 DOM,watch 配 immediate/deep,纯展示再考虑函数式组件。