VUE使用技巧(vue2.x)
vue使用技巧
data 数据冻结
vue
中data
的数据默认会进行双向数据绑定,若将大量和渲染无关的数据直接放置在data
中,将会浪费双向数据绑定时所消耗的性能,将这些和渲染无关的数据进行抽离并配合Object.freeze
进行处理。
table
中columns
数据可单独提取到一个外部js
文件作为配置文件,也可以在当前.vue
文件中定义一个常量定义columns
数据,因为无论如何都是固定且不会修改的数据,应该使用Object.freeze
进行包裹,既可以提高性能还可以将固定的数据抽离,一些下拉框前端固定的数据也建议此操作。
Object.freeze()
方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。
freeze()
返回和传入的参数相同的对象作为参数传递的对象与返回的对象都被冻结 所以不必保存返回的对象(因为两个对象全等)
const obj = { a: 1, b: 2 }
const obj2 = Object.freeze(obj)
obj === obj2 // => true
需要注意的是 Object.freeze() 冻结的是值,这时仍然可以将变量的引用替换掉,还有确保数据不会变才可以使用这个语法,如果要对数据进行修改和交互,就不适合使用冻结了。
export default {
data() {
return {
// 冻结数据,避免vue进行响应式数据转换
columns: Objet.freeze([{ title: "姓名", key: "name" }, { title: "性别", key: "gender" }])
}
}
}
debounce 使用
例如远程搜索时需要通过接口动态的获取数据,若是每次用户输入都接口请求,是浪费带宽和性能的。
当一个按钮多次点击时会导致多次触发事件,可以结合场景是否立即执行immediate
。
<template>
<select :remote-method="remoteMethod">
<option v-for="item in temoteList" :value="item.value" :key="item.id">{{item.label}} </option>
</select>
</template>
import {debounce} from 'lodash-es' methods:{ remoteMethod:debounce(function (query) { // to do ... // this 的指向没有问题 },
200), }
路由参数解耦
一般在组件内使用路由参数,都会写如下代码:
export default {
computed: {
id() {
return this.$route.params.id
}
}
}
在组件中使用$route
会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的URL
上使用,限制了其灵活性。
如果一个组件既可以是路由组件又可以是普通组件,那该如何实现呢? 有人会说我做个判断就可以了啊
export default {
props: {
userId: {
type: String,
default: ''
}
}
computed: {
id() {
return this.userId || this.$route.params.id
}
}
}
但是这种方式不够优雅,那还有更优雅的处理方式吗?
正确的做法是通过路由props
进行参数解耦
const router = new VueRouter({
routes: [
{
path: "/user/:id",
component: User,
props: true
}
]
})
将路由的props
属性设置为true
,就可以在组件内通过props
接收$route.prams
参数,在模板中就可以直接使用id
了,不用关系这个id
是从外部传递过来的还是通过路由参数传递过来的。
export default {
props: {
id: {
type: String,
default: ""
}
}
}
另外你还可以通过函数模式来传递props
const router = new VueRouter({
routes: [
{
path: "/user/:id",
component: User,
props: route => ({
id: route.query.id
})
}
]
})
函数式组件
由于函数式组件只是函数,因此渲染性能开销也低很多,适合依赖外部数据变化而变化的组件。
无状态,没有响应式 data
无实例,没有 this
上下文无生命周期方法,没有 钩子函数
不会出现在 Vue devtools 的组件树中
<template functional>
<button class="btn btn-primary" v-bind="data.attrs" v-on="listeners"><slot /></button>
</template>
watch 高阶使用
立刻执行
watch
是监听属性改变才触发,有时候我们系统在组件创建时也触发。
一般的想法是在
created
生命周期函数里调用一次,以后watch
监听变化时触发。
export default {
data() {
return {
name: "Yori"
}
},
watch: {
name: {
hander: "sayName",
immediate: true // 立刻执行
}
},
created() {
// this.sayName()
},
methods: {
sayName() {
console.log(this.name)
}
}
}
深度监听
watch
监听对象时,属性内对象的属性改变无法监听到,这时我们就需要设置深度监听
模式。
export default {
data() {
return {
user: {
name: {
firstName: "张",
lastName: "三"
}
}
}
},
watch: {
name: {
handler: function() {},
deep: true
}
}
}
触发执行多个方法
watch
运行以数组的形式传递的多个方法,方法会一一调用
可以是字符串函数名、匿名函数和对象形式。
export default {
watch: {
name: [
"sayName",
function(oldVal, newVal) {},
{
handler: function() {},
immediate: true
}
]
}
}
监听多个属性
watch
本身不支持多个属性同时监听,我们可以通过计算属性的形式把多个属性合并成一个对象,供watch
监听使用
export default {
data() {
return {
msg1: "apple",
msg2: "banana"
}
},
computed: {
msgObj() {
const { msg1, msg2 } = this
return {
msg1,
msg2
}
}
},
watch: {
msgObj: {
handler(newVal, oldVal) {
if (newVal.msg1 != oldVal.msg1) {
console.log("msg1 is change")
}
if (newVal.msg2 != oldVal.msg2) {
console.log("msg2 is change")
}
},
deep: true
}
}
}
组件生命周期钩子
父子组件的加载顺序是先加载子组件,再加载父组件,但是父组件是无法知道子组件什么时候加载完成的,我们一般的做法是使用$emit
进行事件触发,通知父组件。
子组件,在生命周期方法里触发事件
export default {
name: "ComponentSub",
mounted() {
this.$emit("mounted")
}
}
父组件,监听事件,接收子组件的事件
<template>
<component-sub @mounted="onSubMounted" />
</template>
<script>
export default {
methods: {
onSubMounted() {
//
}
}
}
</script>
但是这种方式不够灵活,如果子组件是调用的三方框架组件,有没有更优雅的方式监听组件的生命周期呢。
通过@hook
可以监听组件的生命周期方法,组件无需修改,如created
,beforeDestroy
等
<template>
<component-a @hook:created="onReady" />
</template>
<script>
export default {
methods: {
onReady() {
// ...
}
}
}
</script>
可编程的事件监听器
我们在写组件时时经常能看到如下代码:
export default {
data() {
return {
timer: null
}
},
methods: {
start() {
this.timer = setInterval(() => {
console.log(Date.now())
}, 1000)
}
},
mounted() {
this.start()
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
}
}
我们使用timer
来缓存定时器的引用以便后续使用,只为了在生命周期方法beforeDestroy
中释放资源使用。
多了一个 timer
实例属性设置和销毁代码分散在各处
通过可编程的事件监听器,把创建实例和销毁实例放在一起,使代码更加简洁
export default {
methods: {
start() {
const timer = setInterval(() => {
console.log(Date.now())
}, 1000)
this.$once("hook:beforeDestroy", () => {
clearInterval(timer)
})
}
}
}
手动刷新组件
是否遇到这种场景,一个组件包含了多个表单和信息展示,出于业务需求,我们需要清空组件里表单的信息还原到刚进来的初始状态 在表单组件少的情况下,我们通过设置初始值的方法来还原组件状态,但是太繁琐,有没有更简单的方式呢
更新 key
强制渲染组件
通过更新组件的key
来强制重新渲染组件
<template>
<component-a :key="key" />
</template>
<script>
export default {
data() {
return {
key: 0
}
},
methods: {
forceRerender() {
this.key++
}
}
}
</script>
通过更新组件的key
属性,强制使组件不复用缓存,生成一个新的组件,达到还原组件到初始状态的目的
v-if
强制渲染
v-if
指令,该指令仅在组件为 true 时才渲染。 如果为 false,则该组件在 DOM 中不存在。
<template>
<component-a v-if="renderComponent" />
</template>
<script>
export default {
forceRerender() {
// 从DOM删除组件
this.renderComponent = false
this.$nextTick().then(() => {
// 在DOM中添加该组件
this.renderComponent = true
})
}
}
</script>
$forceUpdate()
迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
vue
属性本身是响应式的,但是有时候属性改变,页面会出现不刷新情况
如果我们不用$set()
方法来更新数据,比如我们改变数组的长度arr.length = 1
,页面也要求同步更新,就需要手动调用$forceUpdate
强制渲染
<template>
<ul>
<li v-for="(item) in list" :key="item">{{ item }}</li>
</ul>
</template>
<script>
export default {
data() {
return {
list: ["apple", "orange", "banana"]
}
},
methods: {
resetHandler() {
this.list.length = 1
this.$forceUpdate()
}
}
}
</script>
使用Vue.extend()
写命令式 api
对应一些组件,使用命令式编程的方式更优雅调用组件,如$message
,$confirm
等弹窗自己使用的比较多,而不是在组件中切换组件的方式来实现效果,下面是简易版的命令式组件的实现。
components/Comfirm/index.js
import Vue from 'vue'
import Confirm from './Confirm.vue'
const ConfirmModal = {
instance: null,
show (title = '温馨提示', message = '') {
if (!this.instance) {
const ConfirmConstructor = Vue.extend(Confirm)
this.instance = new ConfirmConstructor({
propsData: {
title,
message
}
})
} else {
this.instance.title = title
this.message = message
}
// $mount("#my") 挂载到指定选择器
// $mount() 不传入选择器,生成文档之外的元素,类似‘document.creatElement()’在内存中生成DOM,
this.instance.$mount()
// this.instance.$el 获取对应的DOM元素
// 把内存中的DOM挂载到body里
document.body.appendChild(this.instance.$el)
Vue.nextTick().then(() => {
this.instance.show()
})
},
hide () {
if (this.instance) {
// 等待动画执行完
this.instance.hide().then(() => {
// 删除DOM元素
document.body.removeChild(this.instance.$el)
})
}
}
}
export default {
install () {
Vue.prototype.$confirm = ConfirmModal
}
}
export {
Confirm
}
src/components/Confirm/Confirm.vue
<template>
<transition>
<div class="q-confirm" v-if="visible">
<div class="q-confirm-overlay" @click="close"></div>
<div class="q-confirm-content"><h1>Confirm</h1></div>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
visible: false
}
},
created() {
console.log("Confirm created")
},
mounted() {
console.log("Confirm mounted")
},
methods: {
show() {
this.visible = true
},
close() {
this.visible = false
},
hide() {
return new Promise((resolve, reject) => {
this.visible = false
setTimeout(() => resolve(), 200)
})
}
}
}
</script>
<style scoped>
.q-confirm-content {
width: 300px;
height: 300px;
position: absolute;
top: 50%;
left: 50%;
background: white;
border: 1px solid #dddddd;
transform: translate(-50%, -50%);
z-index: 20;
}
.q-confirm-overlay {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.2);
z-index: 10;
}
</style>
巧用slot-scope
数据和 UI 分离组件
slot-scope
语法V2.5
添加,V2.6
废弃,V2.6+
请使用v-slot
如何写数据和 UI 分离组件,以vue-promised这个库为例。 Promised 组件并不关注你的视图展示成什么样,它只是帮你管理异步流程,并且通过你传入的 slot-scope,在合适的时机把数据回抛给你,并且帮你去展示你传入的视图。
封装前写法
<template>
<div>
<p v-if="error">Error: {{ error.message }}</p>
<p v-else-if="isLoading && isDelayElapsed">Loading...</p>
<ul v-else-if="!isLoading">
<li v-for="user in data">{{ user.name }}</li>
</ul>
</div>
</template>
<script>
export default {
data: () => ({
isLoading: false,
error: null,
data: null,
isDelayElapsed: false
}),
methods: {
fetchUsers() {
this.error = null
this.isLoading = true
this.isDelayElapsed = false
getUsers()
.then(users => {
this.data = users
})
.catch(error => {
this.error = error
})
.finally(() => {
this.isLoading = false
})
setTimeout(() => {
this.isDelayElapsed = true
}, 200)
}
},
created() {
this.fetchUsers()
}
}
</script>
封装后写法
<template>
<Promised :promise="usersPromise">
<!-- Use the "pending" slot to display a loading message -->
<template v-slot:pending>
<p>Loading...</p>
</template>
<!-- The default scoped slot will be used as the result -->
<template v-slot="data">
<ul>
<li v-for="user in data">{{ user.name }}</li>
</ul>
</template>
<!-- The "rejected" scoped slot will be used if there is an error -->
<template v-slot:rejected="error">
<p>Error: {{ error.message }}</p>
</template>
</Promised>
</template>
<script>
export default {
data: () => ({ usersPromise: null }),
created() {
this.usersPromise = this.getUsers()
}
}
</script>
样式穿透
在开发中修改第三方组件样式是很常见,但由于 scoped
属性的样式隔离,可能需要去除 scoped
或是另起一个 style
。这些做法都会带来副作用(组件样式污染、不够优雅),样式穿透在 css 预处理器中使用才生效。
less
使用/deep/
<style scoped lang="less">
.content /deep/ .el-button {
height: 60px;
}
</style>
scss
使用::v-deep
<style scoped lang="scss">
.content ::v-deep .el-button {
height: 60px;
}
</style>
stylus
使用>>>
<style scoped ang="stylus">
外层 >>> .custon-components {
height: 60px;
}
</style>