本文整理了常用的 vue 实战技巧及要点。对于 vue 初级开发者来说可能有些难度,但干货很足。
一.vue
1.vue 生命周期
vue生命周期大致分为:创建前后,挂载前后,更新前后,销毁前后四个阶段。
- beforeCreate:在 beforeCreate 生命周期执行时,data 和 methods 中的数据还未初始化,所以此时不能使用 data 中的数据和 methods 中的方法
- created:data 和 methods 初始化完毕,此时可以使用 methods 中的方法和 data 中的数据
- beforeMount:template 模版已经编译好,但还未挂载到页面,此时页面还是上一个状态
- mounted:此时 Vue 实例初始化完成了,DOM 挂载完毕,可以直接操作 dom 或者使用第三方 dom 库
- beforeUpdate:此时 data 已更新,但还未同步页面
- updated:data 和页面都已经更新完成
- beforeDestory:Vue 实例进入销毁阶段,但所有的 data, methods,指令,过滤器等都处于可用状态
- destroyed: 此时组件已经被销毁,data,methods 等都不可用
我们一般在 created 中发送 http 请求获取后端数据,在 mounted 中操作 dom。
另外 3 个生命周期方法:
- activated:被 keep-alive 缓存的组件激活时调用。
- deactivated:被 keep-alive 缓存的组件停用时调用。
- errorCaptured:当捕获一个来自子孙组件的错误时被调用。
使用 keep-alive 时,第一次进入页面的生命周期:beforeCreate/created => beforeMount/mounted => activated;再次进入时只执行 activated。
2.$on, $once 的使用
$on:监听当前实例上的自定义事件。
$once:监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除
示例:全局事件的绑定与解绑
普通版:
mounted () {
window.addEventListener('scroll', this.handleScroll)
},
beforeDestroy () {
window.removeEventListener('scroll', this.handleScroll)
}
升级版:
mounted () {
window.addEventListener('scroll', this.handleScroll)
// 通过 hook 监听组件销毁钩子函数,并取消监听事件。
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('scroll', this.handleScroll)
})
},
3.指令与修饰符
指令:
- v-text
- v-html
- v-show
- v-if
- v-else-if
- v-else
- v-for
- v-on,缩写:@,用于绑定事件。绑定多个事件还可以这样写:
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
- v-bind,缩写::,用于绑定属性
- v-model,用于表单绑定
- v-slot,缩写:#,插槽
- v-pre,跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。例子:
<span v-pre>{
{
this will not be compiled }}</span>
- v-cloak,这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。(用来避免页面加载时出现闪烁的问题)
[v-cloak] {
display: none;
}
<div v-cloak>
{
{ message }}
</div>
- v-once,只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
常用修饰符:
(1)事件修饰符(与 v-on 使用)
- .stop:调用 event.stopPropagation(),阻止事件冒泡。
- .prevent:调用 event.preventDefault(),阻止默认行为。
- .self:只当事件是从侦听器绑定的元素本身触发时才触发回调。
- .{keyCode | keyAlias}:只当事件是从特定键触发时才触发回调。
- .native:监听组件根元素的原生事件。
- .once:只触发一次回调。
(2)表单修饰符(与 v-model 使用)
- .lazy:在输入框输入完内容,光标离开时才更新视图。
- .number:输入字符串转为有效的数字。
- .trim:输入首尾空格过滤。
(3)系统修饰符
- .ctrl
- .alt
- .shift
- .meta:在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。
修饰符可以多个使用,比如 @click.prevent.self,修饰符的位置代表执行顺序。
系统修饰符的使用,如 @click.ctrl=“ctrlClick”,只有按住 ctrl 键再点击才会触发。当然,我们也可以在该元素上同时绑定 @click 事件。
4.computed,watch 的使用与比较
(1)computed 计算属性
计算属性的结果会被缓存,除非依赖的响应式 property 变化才会重新计算。
计算属性可以定义 get 和 set
var vm = new Vue({
data: {
a: 1 },
computed: {
// 读取和设置
aPlus: {
get: function () {
return this.a + 1
},
set: function (v) {
this.a = v - 1
}
}
}
})
vm.aPlus // => 2
vm.aPlus = 3
vm.a // => 2
(2)watch 监听
watch 除了基础用法外,还有 立即触发监听,深度监听:
var vm = new Vue({
data: {
c: {
a: 1
},
d: 4,
e: {
f: {
g: 5
}
}
},
watch: {
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: function (val, oldVal) {
/* ... */ },
deep: true
},
// 该回调将会在侦听开始之后被立即调用
d: {
handler: 'someMethod',
immediate: true
},
// 只监听对象某一个属性
'e.f': function (val, oldVal) {
/* ... */ }
}
})
(3)computed 与 watch 的比较
- 计算属性支持缓存,监听不缓存;
- 计算属性是由一个或多个依赖计算出来的,所以适合一对一或多对一的关系;监听是一个属性变化从而执行一个或多个操作,所以适合一对一或一对多的关系;
- 计算属性不支持异步操作,监听支持;若想要在计算属性中使用异步,可以考虑使用 vue-async-computed。
5.组件间的通信方式
- 父子组件通信,通过属性与 $emit 事件触发的方式
- 通过事件总线(bus),即发布订阅的方式
- Vuex
- Vue.observable
Vuex 建议我们只在开发中大型项目时才使用它,这里我们可尝试用其他方式做状态管理。
使用 Vue.observable
(1)创建 store
import Vue from 'vue'
// 通过 Vue.observable 创建一个可响应的对象
export const store = Vue.observable({
userInfo: {
}
})
// 定义 mutations, 修改属性
export const mutations = {
setUserInfo(userInfo) {
store.userInfo = userInfo
}
}
(2)组件中使用
<template>
<div>
{
{ userInfo.name }}
</div>
</template>
<script>
import {
store, mutations } from '../store'
export default {
computed: {
userInfo() {
return store.userInfo
}
},
created() {
mutations.setUserInfo({
name: '小明'
})
}
}
</script>
6.全局组件,局部组件,动态组件,异步组件,递归组件
(1)全局组件
通过 Vue.component 注册一个全局组件。如,在 main.js 中:
import MyPopover from '@/components/popover'
Vue.component('my-popover', MyPopover)
(2)局部组件
在一个组件或页面中引入另一个组件,则被引入组件就是局部组件,其只能在当前范围内使用。
(3)动态组件
有时我们需要在组件间做动态切换,这时就会用到动态组件。
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
currentTabComponent 可以包括:
- 已注册组件的名字,或
- 一个组件的选项对象
(4)异步组件
在做路由懒加载时我们会使用异步组件,以实现按需加载,优化单页应用的首屏时间。
(5)递归组件
当我们要在组件中调用自身时会使用递归组件。
<template>
<div>
<div
class="item"
v-for="(item, index) of list"
:key="index"
>
<div class="item-title border-bottom">
<span class="item-title-icon"></span>
{
{item.title}}
</div>
<div v-if="item.children" class="item-children">
<!-- 调用自身 -->
<detail-list :list="item.children"></detail-list>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DetailList',
props: {
list: Array
}
}
</script>
7.单文件组件中 name 值的作用
<script>
export default {
name: 'Home'
}
</script>
- 作用一:给 <keep-alive> 设置 include 和 exclude 的值,这个值就是 name(当然,如果 name 选项不可用,则匹配它的局部注册名称);
- 作用二:递归组件的调用;
- 作用三:在使用 Vue.js devtools 调试工具时,用来显示组件的名字(当然,如果 name 选项不可用,则匹配它的局部注册名称)。
8.v-show 与 v-if 的比较
- v-show 有更高的初始渲染开销,v-if 有更高的切换开销。v-if 在切换时会对元素进行创建和销毁,v-show 只会在初始化时加载一次。
- v-if 是惰性的,只有条件为真时才会进行渲染;v-show 不管条件真假,都会进行渲染,由 css 样式 display 进行显示隐藏。
- 所以,v-show 适合频繁切换的元素,v-if 适合切换频率低的元素。
9.key 的作用
key 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
当 key 值发生改变时,元素总是会被替换而不是被修改,因此,以下情景下可能会使用:
- 完整地触发组件的生命周期钩子
- 触发过渡
10.双向绑定原理
vue 数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。通过 Object.defineProperty() 来劫持各个属性的 setter 和 getter,当数据变化时会通知视图更新。下面,我们通过一个例子来简单实现其效果。
<div id="harry"></div>
<input id="trigger" type="text">
<script>
let harry = document.getElementById('harry')
let trigger = document.getElementById('trigger')
let key = 'name' // 属性键名
let store = {
} // 辅助 get 取值
let obj = {
// 对象
name: ''
}
Object.defineProperty(obj, key, {
set (value) {
harry.innerText = value // 修改视图
store[key] = value
},
get () {
return store[key]
}
})
trigger.addEventListener('keyup', function () {
obj[key] = this.value
console.log(obj[key])
})
</script>
效果图:
11.默认插槽,具名插槽,作用域插槽
(1)默认插槽
<div id="app">
<slot-component>
<span>hello</span>
</slot-component>
</div>
<script>
var SlotComponent = {
template: `
<div>
<slot></slot>
</div>
`
}
new Vue({
el: '#app',
components: {
SlotComponent
}
})
</script>
渲染出的元素如下:
(2)具名插槽
顾名思义,就是给插槽加上名字。
<div id="app2">
<slot-component2>
<template v-slot:header>
<div>header</div>
</template>
<div>footer</div>
</slot-component2>
</div>
<script>
// 具名插槽
var SlotComponent2 = {
template: `
<div>
<slot name="header"></slot>
<div>Other Content</div>
<slot></slot>
</div>
`
}
new Vue({
el: '#app2',
components: {
SlotComponent2
}
})
</script>
这里,我们需要在一个<template> 元素上使用 v-slot 指令,以指明其名称。同时,任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。
上述示例的渲染结果如下:
(3)作用域插槽
正常情况下,父级是无法直接访问插槽中的数据,如要实现这个效果,就需要使用作用域插槽。
<div id="app3">
<slot-component3>
<template v-slot:default="scope">
{
{ scope.user.lastName }}
</template>
</slot-component3>
</div>
<script>
// 作用域插槽
var SlotComponent3 = {
data () {
return {
user: {
firstName: '1',
lastName: '2'
}
}
},
template: `
<div>
<slot :user="user">{
{ user.firstName }}</slot>
</div>
`
}
new Vue({
el: '#app3',
components: {
SlotComponent3
}
})
</script>
可以看到,在组件中原本是显示默认值 firstName,但在父作用域中,我们改为了 lastName。
12.混入(mixin)
我们在提炼组件间可复用功能时,往往会想到封装一个方法。而 mixin 相对而言会更加灵活,我们在使用 mixin 时需要掌握其合并策略。
<script>
var myMixin1 = {
data () {
return {
msg: 'hi'
}
},
created () {
console.log('msg mixin1', this.msg) // hello
}
}
var myMinin2 = {
created () {
console.log('msg mixin2', this.msg) // hello
},
methods: {
sayName () {
console.log('mixin name')
}
}
}
new Vue({
el: '#app',
mixins: [myMixin1, myMinin2],
data: {
msg: 'hello'
},
created () {
console.log('msg', this.msg) // hello
this.sayName() // component name
},
methods: {
sayName () {
console.log('component name')
}
}
})
</script>
最终打印的结果为:
// msg mixin1 hello
// msg mixin2 hello
// msg hello
// component name
由上,mixin 遵循以下规则:
- 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先;
- 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用;
- 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
13.函数式组件
对于一个简单的数据展示组件,它不用管理任何状态,也不用监听任何传递给它的状态,也不用生命周期方法。这时,我们可以考虑使用函数式组件。
<div id="app">
<my-component :text="'Hello World!'"></my-component>
</div>
<script>
var MyComponent = {
// 声明函数式组件
functional: true,
props: {
text: {
type: String,
default: ''
}
},
// context 包含 props, slots 等字段
render: function (createElement, context) {
// console.log('content', context)
return createElement('div', context.props.text)
}
}
new Vue({
el: '#app',
components: {
MyComponent
}
})
</script>
函数式组件特点:
- 无状态、无实例
- 因为函数式组件只是函数,所以渲染开销也低很多
基于模板的函数式组件
如果不习惯用 JS 或 JSX 的方式写 html 模板,可以使用这种方式。
<template functional>
</template>
14.自定义指令(Vue.directive)
举个聚焦输入框的例子:
<div id="app">
<input type="text" v-focus >
</div>
<script>
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
new Vue({
el: '#app'
})
</script>
钩子函数:
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
关于钩子函数的参数相关介绍,见官网:钩子函数参数
15.$attrs 与 $listeners
- $attrs : 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。
- $listeners : 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。
如下例子,我们封装了一个表单的保存按钮。
父作用域:
<FormSaveButton :is-editForm="isEditForm" :disabled="loading" @click="submitForm('form')" />
子组件:
<template>
<!-- 表单的保存按钮 -->
<el-button
:class="isEditForm ? 'warning-btn-text': ''"
style="float: right; padding: 3px 0"
type="text"
v-bind="$attrs"
v-on="$listeners"
>{
{$t('buttonText.save')}}
</el-button>
</template>
<script>
export default {
props: {
isEditForm: Boolean
}
}
</script>
这里我们通过 v-bind="$attrs" 将父作用域中的 :disabled=“loading” 绑定在了按钮上,通过 v-on="$listeners" 将点击事件绑定在了按钮上。注意:这里 $attrs 不包含 isEditForm 属性,因为其已经在 props 中进行了定义。
这样写的好处在于减少了 props, $emit 操作。
16. .sync 修饰符
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。这时我们可以考虑使用 .sync 。
<div id="app">
<my-component :title="title" @update:title="title = $event"></my-component>
<!-- 简写版 -->
<!-- <my-component :title.sync="title"></my-component> -->
</div>
<script>
var MyComponent = {
props: {
title: {
type: String,
default: ''
}
},
template: `
<div>{
{ title }}</div>
`,
created () {
// 修改 title
this.$emit('update:title', '更新标题')
}
}
new Vue({
el: '#app',
components: {
MyComponent
},
data: {
title: '标题内容',
}
})
</script>
除了使用 .sync 实现双向绑定外,v-model 也能实现双向绑定,不过两种之间存在差异:
- 一个组件上可多个使用 .sync,而 v-model 目前只能使用一个。
- v-model 默认触发 input 事件,常用于表单元素的双向绑定。
17.使用 Object.freeze 冻结对象或数组
vue 默认会对 data 中的数据做 getter 和 setter 转换,以支持响应式。
如果你有很大的数组或对象,并确信不会改变,这时可以考虑使用 Object.freeze 对其冻结。这样做能一定程度上提升性能。
例如:
// 冻结 sourceArr 数组, 使 vue 不对其做 setter getter 转换
this.myArr = Object.freeze(sourceArr)
18.watch 中使用防抖
使用场景:搜索框搜索,当用户输入内容后自动更新数据,这时,我们一般要加个防抖。
一般的防抖方法
function debounce (func, wait) {
let timeout = null
return function() {
const context = this
const args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
常见的使用防抖方式
methods: {
_addEvent () {
window.addEventListener('resize', debounce(this.doSomething, 100))
},
}
我们给 resize 事件这样使用防抖是没问题的,但如果在监听中这样使用防抖
watch: {
searchVal: debounce(this._initData, 500)
},
这时 this._initData 就会报错。那又如何改进呢?
修改防抖方法
/**
* 防抖函数
* @param {String} funcName 执行的方法名
* @param {Number} wait 等待的时间
* @return {Function} 执行函数
*/
function debounce (funcName, wait) {
let timeout = null
let _this = this
return function() {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
// 监听防抖时用内层 this, 此时外层 this 为 undefined
_this = _this || this
_this[funcName]()
}, wait)
}
}
可以看到,这里将方法改为了函数名传递,然后再通过 this 进行访问。
在监听中使用
watch: {
searchVal: debounce('_initData', 500)
},
由于修改了防抖方法,所以 resize 事件添加防抖的方式也要改变
methods: {
_addEvent () {
window.addEventListener('resize', debounce.call(this, '_setCollapse', 100))
},
}
当然,若你有更好的方式欢迎在评论区提出。
19.v-model 的基础上再次封装
有如下需求,需要将下面的代码封装成全局组件:
<el-input
v-model="searchVal"
size="mini"
:placeholder="$t('hintText.searchName')"
class="vk-search-input"
/>
这个需求的难点在于 v-model,在进行封装前,我们需要知道 v-model 是语法糖,具体如下:
<input v-model="val">
等同于
<input
v-bind:value="val"
v-on:input="val = $event.target.value"
>
如果我们需要将代码如下模样,该如何实现呢?
<table-search-input v-model="searchVal" />
实现如下:
<template>
<!-- 表格搜索输入框 -->
<el-input
v-model="val"
size="mini"
:placeholder="$t('hintText.searchName')"
class="vk-search-input"
/>
</template>
<script>
export default {
model: {
prop: 'searchVal',
event: 'input'
},
props: {
searchVal: {
type: String,
default: ''
}
},
data () {
return {
val: this.searchVal
}
},
watch: {
val (val) {
this.$emit('input', val)
}
}
}
</script>
这里我们使用了 model 配置,其作用是——允许一个自定义组件在使用 v-model 时定制 prop 和 event。详情可见官网:API-model。
二.vue-router
1.一些专业术语
- 动态路由 (
/user/:id
) - 嵌套路由 (
children: []
) - 编程式导航 (push、replace、go)
- 命名路由、命名视图
- 重定向 (redirect) 和别名 (alias)
- 路由组件传参 (props 解耦, 布尔模式、对象模式、函数模式)
- H5 History 模式 (hash、history)
- 导航守卫 (前置导航守卫 beforeEach、全局解析守卫 beforeResolve、全局后置钩子 afterEach、路由独享的守卫 beforeEnter、组件内的守卫 beforeRouteEnter beforeRouteUpdate beforeRouteLeave、完整的导航解析流程)
- 路由元 (meta)
- 过度效果 (transition)
- 数据获取
- 滚动行为 (scrollBehavior)
- 路由懒加载
- 导航故障
以上这些术语其实就是官网侧边栏目录,通过看这些词汇,我们脑海中应浮现出对应内容,如果没有,可能就是不太熟悉。
2.this.$route 与 this.$router 的区别
$route 是当前路由,$router 全局路由。
打印效果如下:
3.动态路由参数变化的注意点
比如有动态路由 /user/:id
。当参数发生变化时,例如从 /user/0
导航到 /user/1
,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
对路由参数变化做出响应的方式:
方式一:监听 $route
watch: {
$route(to, from) {
// 对路由变化作出响应...
}
}
方式二:使用 beforeRouteUpdate 导航守卫
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// 对路由变化作出响应...
// 记得执行 next()
}
}
4.编程式导航常用方法
router.push
导航到不同的 URL,会向 history 栈添加一个新的记录;router.replace
它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录;router.go
方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步。
三.vuex
1.流程图
图中,整个虚线部分就是Vuex,我们可以把它看成一个公共仓库 store。store 中有 Actions(行为)、Mutations(变动)和 State(状态)。整个的逻辑是组件通过 Dispatch 调用 Actions 中的方法,Actions 通过 Commit 调用 Mutations 中的方法,Mutatisons 改变 State 中的值。
注意:mutation 必须是同步函数,action 可以执行异步操作,action 也可省略。
2.解决刷新页面数据被重置为初始状态的问题
对于这个问题,我们一般通过 Vuex 与缓存结合使用来解决。
读取数据时
let defaultCity = ''
if (localStorage.getItem('city')) {
defaultCity= localStorage.getItem('city')
}
const state = {
city: defaultCity
}
export default state
改变数据时
const mutations = {
changeCity (state, val) {
localStorage.setItem('city', val)
state.city= val
}
}
export default mutations
四.vue3
vue3 正式版已发布,所以我们应对其有所了解。
1.vue2 有哪些不足?
- vue2.x 对数组对象的深层监听无法实现;
- vue2.x 在模板编译过程中会涉及到许多不必要的CPU工作;
- 随着功能的增长,复杂组件的代码变得难以维护;
- vue2.x 是采用 Facebook 的 Flow 做类型检查,但在某些情况下推断有问题,且对 typescript 支持不太友好。
2.vue3 有哪些改动?
- Object.defineProperty => Proxy
- 使用 TS 重构
- 重构了虚拟 DOM
- OptionApi => Composition API
当然,以上写得并不全面具体,详细见官网。