Vue2
1. 生命周期流程
- 通过new Vue()创建Vue实例
- beforeCreate,到这一步已初始化了事件和生命周期
- created,到这一步已初始化了依赖注入和响应式数据
- beforeMount,到这一步已把模板渲染成了虚拟dom
- mounted,到这一步已根据虚拟dom生成了实际dom元素
- beforeUpdate,响应式数据改变时,触发这一步
- updated,到这一步,虚拟dom已重新渲染和装载
- beforeDestroy,使用vm.$destroy()触发
- destroyed,到这一步,组件、监察者、事件均已卸载
- activated,keep-alive的生命周期,当被其包裹的组件激活时触发
- deactivated,keep-alive的生命周期,当被其包裹的组件关闭激活时触发
2. v-if 和 v-show
- v-if如果为false,则不会渲染节点,反之会重新渲染节点
- v-show如果为false,已渲染节点依然存在,但会通过display: none;隐藏显示
如果是需要频繁操作显示隐藏的节点,建议使用v-show,这样不会频繁渲染导致过大的性能损耗
3. 内置指令
- v-once,使用它的元素或组件只渲染一次,其本身和子级都不会随着数据改变而重新渲染,是非响应式的静态内容
- v-bind,用于绑定元素上的属性,让属性可以随响应式改变,比如v-bind:class,一般都简写为:class
- v-on,用于监听dom事件,比如v-on:click,一般简写为@click
- v-html,相当于innerHTML,需要谨慎使用,有遭受XSS攻击的风险
- v-text,相当于innerText
- v-model,是元素或组件value属性的语法糖
- v-for,用于循环dom结构,优先级比v-if高,尽量不与v-if一起使用,不然v-for中的每个循环都会执行一次v-if,增大了浏览器性能开销;需要加上:key的绑定,vnode缺少比对的唯一标识,也会增大比对开销
- v-if/v-else/v-else-if,配合template使用,相当于render()中的三元表达式
- v-show,上面已有说明
- v-pre,跳过这个元素及其子元素的编译过程
- v-cloak,解决vue加载慢导致屏幕闪动的问题,也就是避免先出现vue源代码,然后再出现渲染内容的情况
4. computed计算属性
computed可以设置get和set,一般用来定义根据其他属性计算来的值,computed的值有缓存,只有依赖的属性值改变时,才会重新计算并返回结果
<template>
<div>
<input type="text" v-model="msg" />
<p>{{text}}</p>
</div>
</tempalte>
export default {
name: 'test',
data() {
return {
msg: '掘金'
}
},
computed: {
text: {
get() {
return '欢迎来到' + this.msg;
}
}
}
}
5. watch监听属性
watch可以监听属性的变化,并通过回调函数执行一些逻辑处理
<template>
<div>
<input type="text" v-model="msg" />
<input type="text" v-model="form.name" />
</div>
</template>
export default {
name: 'test',
data() {
return {
msg: ''
},
form: {
name: ''
}
},
watch: {
msg(newVal, oldVal) {
console.log('old value is ' + oldVal);
},
form: {
//监听引用类型数据的内部数据变化,默认为false
deep: true,
//刚刚初始化组件时,就执行属性监听回调,默认为false
immediate: true,
handler(newVal, oldVal) {
console.log('name is changed');
}
},
//监听路由变化
'$route'(to, from) {}
}
}
如果是使用vm.$watch绑定的监听,那么注销组件时还需要注销watch绑定
const unWatch = vm.$watch('msg', (newVal, oldVal) => {
console.log('old value is ' + oldVal);
});
unWatch(); //手动注销
6. mixin混入
mixin一般用来定义公用方法或逻辑代码,方便其他组件统一调用。
如果mixin中定义的属性或函数,与当前组件重名,则会以当前组件的属性或函数为准。
//mixins.js
export default {
data() {
return {
msg: '掘金'
}
},
methods: {
getMsg() {
return this.msg;
}
}
}
//组件
import globalMixins from 'mixins.js';
export default {
name: 'test',
/*
* 使用了mixins后,globalMixins中的data/computed/methods都会合并到当前组件实例下
* mixins是一个数组,也就是说,可以用来混入多个vue实例
*/
mixins: [globalMixins],
data() {
return {
txt: 'hello world'
}
},
created() {
this.printMsg();
},
methods: {
printMsg() {
//可以使用由globalMixins混入进来的getMsg()
const msg = this.getMsg();
console.log(msg);
}
}
}
还有一个全局方法Vue.mixin,因为vue是一个基于Vue构造函数的单实例框架,内部所有的组件都是Vue实例的子实例,因此通过全局的Vue.mixin方法,可以直接影响Vue实例,从而影响其内部关联的所有组件实例,所以需要谨慎使用,可以在创建中间件时用到。
/*
* 通过全局的Vue.mixin,可以影响所有组件的生命周期
* 下面这个例子会导致所有组件的created生命周期,都会打印出'全局混入的created'
*/
Vue.mixin({
created() {
console.log('全局混入的created');
}
});
7. nextTick
vm.$nextTick的回调是在下一次dom更新渲染之后执行的,因为vue的dom更新是异步的,因此需要借助这个回调方法获取重新渲染后的dom结构。
nextTick函数在源码中,会通过判断是否支持下列原生方法函数来依次进行实现:
先判断是否支持setImmediate(),支持就使用setImmediate()来执行 ->
否则判断是否支持MessageChannel(),支持就使用new MessageChannel()来执行 ->
否则使用setTimeout()来执行
上面是宏任务(macro task)的判断执行流程,如果是微任务(micro task),就会先判断是否支持原生Promise,
支持就用new Promise()来执行,否则就把宏任务的判断流程走一遍。
<template>
<div>
<p ref="domMsg">{{msg}}</p>
<button @click="changeMsg()">change msg</button>
</div>
</template>
export default {
name: 'test',
data() {
return {
msg: 'hello world'
}
},
methods: {
changeMsg() {
this.msg = '掘金';
//此时打印的还是hello world,因为dom更新是异步的
console.log(this.$refs.domMsg.innerHTML);
//nextTick中打印的才是新渲染的dom内容
this.$nextTick(() => {
let domMsg = this.$refs.domMsg.innerHTML;
console.log(domMsg);
});
}
}
}
8. keep-alive
keep-alive是vue内置的组件,可以缓存被其包裹住的组件,组件切换时不会对当前组件进行卸载,因此无需变动内容的组件可以通过它降低性能损耗。
可用参数有三个:
- include,表示需要缓存的组件,以逗号分隔的名称列表对应的是组件的name
- exclude,表示不需要被缓存的组件,也就是列表以外的组件都会被缓存
- max,表示最大缓存数,不常用
keep-alive运用LRU算法,会将最近访问的组件插入到访问列表尾部,越在列表前面的组件,表示越久没有被访问过,当列表数量超过max属性值时,会自动销毁列表最前面的组件。
keep-alive是缓存当前页面虚拟dom中的节点,所以对iframe中的内容不起作用,因为iframe中的内容不在当前页面的虚拟dom中。
//app.js
<template>
<div>
<keep-alive include="globalMenu,globalHeader">
<router-view></router-view>
</keep-alive>
<keep-alive exclude="test">
<router-view></router-view>
</keep-alive>
</div>
</template>
//globalMenu.js
export default {
name: 'globalMenu',
data() {
return {}
},
mounted() {
console.log('组件已装载');
},
/*
* 组件第一次生成时,activated是发生在mounted之后的;
* 以后再切换回该组件,会直接触发activated,不会再触发之前的生命周期
*/
activated() {
console.log('组件被激活了');
},
deactivated() {
console.log('组件关闭了激活');
}
}
9. Vue.set
使用方式:Vue.set(Array或Object, 数组下标或对象key, 新的值);
因为vue是在初始化的时候,利用Object.defineProperty的访问器属性一次性在data数据上绑定的响应式,因此引用类型的数据,如数组和对象,在下面两种情况下修改数据,会导致数据不是响应式的:
- 在实例创建之后,在响应式对象上添加新的属性
- 直接通过数组下标来修改数组的值
这两种情况下,就需要用到Vue.set去修改数组或对象,保证修改后的值是响应式的:
<template>
<div>
<div v-for="(item,idx) in arr" :key="idx">{{item}}</div>
<div>{{obj.text}}</div>
<button @click="addArrVal()">修改数组</button>
<button @click="addObjVal()">修改对象</button>
</div>
</template>
export default {
name: 'test',
data() {
return {
arr: [11,22,33],
obj: {
name: '对象'
}
}
},
methods: {
addArrVal() {
//让数组变成[66,22,33]
//this.arr[0] = 66;这样赋值的66,是非响应式的,无法触发页面渲染
Vue.set(this.arr, 0, 66);
},
addObjVal() {
//this.obj.text = '掘金';这样赋的值,是非响应式的,页面上不会渲染出来
Vue.set(this.obj, 'text', '掘金');
}
}
}
Vue.set的执行原理:初始化实例时,vue给数组和对象数据都增加了__ob__属性,代表Observer实例。当给对象新增不存在的属性时,首先会把新的属性进行响应式跟踪,然后触发ob.dep.notify(),通知dep把收集的watcher进行更新。当改变数组值时,因为vue重写了数组的push/splice/unshift,增加了响应式转换逻辑,因此会调用数组的splice进行数值替换或添加,将修改的数组值转换成响应式的。
10. Vue.extend
Vue.extend使用基础Vue构造器,创建一个子类,参数是包含组件options的对象。
一般的开发场景其实是用不到Vue.extend的,但是如下使用场景可能会需要使用:
- 想从接口动态渲染组件时
- 想要实现一个类似于window.alert()的组件,要求像js一样可全局调用它
//test.js
import Vue from 'vue';
const testComponent = Vue.extend({
template: '<div>{{msg}}</div>',
props: {
msg: String
}
});
//局部注册,这里给组件传参,要使用porpsData
new testComponent({
porpsData: {
msg: '掘金'
}
}).$mount('#app');
//全局注册
Vue.component('testComponent', testComponent);
//在其他组件中调用
export default {
components: {
testComponent
}
}
11. Vue.directive自定义指令
Vue.directive可以用来自定义类似于v-bind的标签指令
自定义指令有bind/inserted/update/componentUpdate/unbind五个钩子函数
每个钩子函数有el/binding/vnode/oldVnode四个参数。el表示所绑定的元素,可直接用来操作dom。binding表示指令所对应的对象。vnode表示Vue生成的虚拟节点。oldVnode表示上一个虚拟节点,只在update和componentUpdate可用。
<template>
<div>
<input type="text" v-model="msg" v-focus />
</div>
</template>
import Vue from 'vue';
Vue.directive('focus', {
//只调用一次,指令第一次绑定到元素时调用
bind(el, binding, vnode) {
console.log('first bind');
},
//被绑定元素插入父节点时调用
inserted(el, binding, vnode) {
el.focus();
},
//所在组件的vnode更新时调用
update(el, binding, vnode, oldVnode) {
console.log('component is update');
},
//指令所在组件的vnode及其子级vnode全部更新后调用
componentUpdate(el, binding, vnode, oldVnode) {
console.log('component all vnode is updated');
},
//只调用一次,指令与元素解绑时调用
unbind(el, binding, vnode) {
console.log('directive is unbind');
}
});
12. Vue修饰符
事件修饰符
比如@click.stop=""
- .stop,阻止事件冒泡
- .prevent,阻止标签默认行为
- .capture,使用事件捕获模式,也就是先在触发元素自身上处理事件,然后再向内部元素逐级触发
- .self,只有当event.target是当前元素自身时,触发处理函数
- .once,事件只触发一次
- .passive,告诉浏览器不阻止事件的默认行为
- .native,使用原生方式给当前元素绑定事件,而不是vue内部定义的$on
v-model修饰符
比如v-model.lazy=""
- .lazy,使v-model的值在触发change后才执行数据响应
- .number,自动将输入的值转化为数值类型
- .trim,自动过滤用户输入的首位空格
键盘事件修饰符
使动作事件可以捕获按键类型,比如@keyup.enter=""
- .enter
- .tab
- .delete,捕获删除和退格键
- .esc
- .space
- .up
- .down
- .left
- .right
系统修饰键
比如@click.ctrl="",相当于鼠标点击 + Ctrl
比如@keyup.alt.67="",相当于Alt + C
- .ctrl
- .alt
- .shift
- .meta,对应windows键盘上的视窗键
鼠标按钮修饰符
比如@click.left="",@click.middle=""
- .left
- .right
- .middle
13. slot插槽
插槽分为下面几种:
- 匿名插槽或者叫默认插槽,一个组件中只能有一个匿名插槽
//父组件
<template>
<div class="father>
<h2>这是父组件</h2>
<child>
<div class="child-info">
<p>子组件的插槽内容</p>
</div>
</child>
</div>
</template>
//子组件
<template>
<div class="child">
<h3>这是子组件</h3>
<slot></slot>
</div>
</template>
vue会把上面例子中子组件的slot标签替换成父组件中被child标签包裹的内容,也就是说,子组件内容会变成下面的样子:
<template>
<div class="child">
<h3>这是子组件</h3>
<div class="child-info">
<p>子组件的插槽内容</p>
</div>
</div>
</template>
- 具名插槽,一个组件中可以有多个具名插槽
//父组件
<template>
<div class="father">
<h2>这是父组件</h2>
<child>
<div class="name1" slot="name1">
<p>这是name1的内容</p>
</div>
<div class="name2" slot="name2">
<p>这是name2的内容</p>
<p>hello world</p>
</div>
<div class="default">
<p>这是匿名插槽的内容</p>
</div>
</child>
</div>
</template>
//子组件
<template>
<div class="child">
<h3>这是子组件</h3>
<slot name="name1"></slot>
<slot name="name2"></slot>
<slot></slot>
</div>
</template>
vue会把上面例子中的子组件内容通过slot标签替换成下面的样子:
<template>
<div class="child">
<h3>这是子组件</h3>
<div class="name1">
<p>这是name1的内容</p>
</div>
<div class="name2">
<p>这是name2的内容</p>
<p>hello world</p>
</div>
<div class="default">
<p>这是匿名插槽的内容</p>
</div>
</div>
</template>
- 作用域插槽,也就是绑定了数据的插槽
在下面例子中,slot-scope对应的名称取名叫slotProps,这是一个包含插槽所有prop的对象集合,这个名字是可以随便起的,只是一个对象的代称,类似于形参的概念
//父组件
<template>
<div class="father">
<h2>这是父组件</h2>
<child>
<template slot-scope="slotProps">
<ul>
<li v-for="(item,idx) in slotProps.list" :key="idx">{{item.name}}</li>
</ul>
</template>
</child>
<child>
<template slot-scope="slotProps">
<p>{{slotProps.txt}}</p>
</template>
</child>
</div>
</template>
//子组件
<template>
<div class="child">
<h3>这是子组件</h3>
<slot :list="list" :txt="txt"></slot>
</div>
</template>
export default {
name: 'childComponent',
data() {
return {
list: [
{ name: '列表1' },
{ name: '列表2' }
],
txt: '这是子组件文字'
}
}
}
通过slot标签替换后,最终父组件的dom结构会变成下面的样子:
<template>
<div class="father">
<h2>这是父组件</h2>
<div class="child">
<h3>这是子组件</h3>
<ul>
<li>列表1</li>
<li>列表2</li>
</ul>
</div>
<div class="child">
<h3>这是子组件</h3>
<p>这是子组件文字</p>
</div>
</div>
</template>
上面说的关于slot的用法是vue2.6.0之前的,从2.6.0开始,具名插槽和作用域插槽的写法有了一点儿变化
具名插槽新写法:
//父组件
<template>
<div class="father">
<h2>这是父组件</h2>
<child>
<!--具名插槽可以缩写成<template #name1>-->
<template v-slot:name1>
<div class="name1">
<p>这是name1的内容</p>
</div>
</template>
<template v-slot:name2>
<div class="name2">
<p>这是name2的内容</p>
<p>hello world</p>
</div>
</template>
<div class="default">
<p>这是匿名插槽的内容</p>
</div>
</child>
</div>
</template>
//子组件
<template>
<div class="child">
<h3>这是子组件</h3>
<slot name="name1"></slot>
<slot name="name2"></slot>
<slot></slot>
</div>
</template>
作用域插槽新写法:
//父组件
<template>
<div class="father">
<h2>这是父组件</h2>
<child>
<!--具名作用域插槽可以缩写成<template #name1="{slotProps}">-->
<template v-solt:name1="slotProps">
<ul>
<li v-for="(item,idx) in slotProps.list" :key="idx">{{item.name}}</li>
</ul>
</template>
</child>
<child>
<!--匿名作用域插槽可以缩写成<template #default="{slotProps}">-->
<template v-solt:default="slotProps">
<p>{{slotProps.txt}}</p>
</template>
</child>
</div>
</template>
//子组件
<template>
<div class="child">
<h3>这是子组件</h3>
<slot name="name1" :list="list"></slot>
<slot :txt="txt"></slot>
</div>
</template>
export default {
name: 'childComponent',
data() {
return {
list: [
{ name: '具名插槽列表1' },
{ name: '具名插槽列表2' }
],
txt: '这是匿名作用域插槽文字'
}
}
}
14. vue组件通信方式
父组件向子组件通信
//父组件
<template>
<div>
<child :msg=""></child>
</div>
</template>
export default {
name: 'ComponentFather',
data() {
return {
msg: '掘金'
}
}
}
//子组件
<template>
<div>
<p>{{msg}}</p>
</div>
</template>
export default {
name: 'ComponentChild',
props: {
msg: {
type: String,
required: true
}
}
}
子组件向父组件通信
//父组件
<template>
<div>
<child @msgChange="msgUpdate"></child>
<p>{{msg}}</p>
</div>
</template>
export default {
name: 'ComponentFather',
data() {
return {
msg: '掘金'
}
},
methods: {
msgUpdate(msg) {
this.msg = msg;
}
}
}
//子组件
<template>
<div>
<button @click="changeMsg"></button>
</div>
</template>
export default {
name: 'ComponentChild',
methods: {
changeMsg() {
this.$emit('msgChange', 'hello world');
}
}
}
使用EventBus方式实现父子、兄弟、跨级通信
//main.js
//在Vue原型上挂一个Vue实例,使得全局可调用
Vue.prototype.bus = new Vue();
//组件A
<template>
<div>
<button @click="sendMsg('掘金')"></button>
</div>
</template>
export default {
name: 'ComponentA',
methods: {
sendMsg(msg) {
this.bus.$emit('eventMsg', msg);
}
}
}
//组件B
<template>
<div>
<p>{{msg}}</p>
</div>
</template>
export default {
name: 'ComponentB',
data() {
return {
msg: ''
}
},
mounted() {
this.bus.$on('eventMsg', (msg) => {
this.msg = msg;
});
}
}
使用listeners
//组件A
<template>
<div>
<p>{{msg}}</p>
<component-b :txt="txt" :msg="msg" @emitMsg="changeMsg"></component-b>
</div>
</template>
import ComponentB from './ComponentB';
export default {
name: 'ComponentA',
components: {
ComponentB
},
data() {
return {
msg: '哈哈哈',
txt: '啦啦啦'
}
},
methods: {
changeMsg(msg) {
this.msg = msg;
}
}
}
//组件B
<template>
<div>
<p>{{txt}}</p>
<component-c v-bind="$attrs" v-on="$listeners"></component-c>
</div>
</template>
import ComponentC from './ComponentC';
export default {
name: 'ComponentB',
components: {
ComponentC
},
//可以关闭自动挂载到组件根元素上的没有在props声明的属性
inheritAttrs: false,
props: {
txt: {
type: String
}
}
}
//组件C
<template>
<div>
<p>{{msg}}</p>
<button @click="sendMsg"></button>
</div>
</template>
export default {
name: 'ComponentC',
props: {
msg: {
type: String
}
},
methods: {
sendMsg() {
this.$emit('emitMsg', '掘金');
}
}
}
使用provide/inject实现祖先元素与子孙元素的通信
//祖先组件
export default {
provide: {
msg: '掘金'
}
}
//子孙组件
export default {
inject: ['msg'],
mounted() {
console.log(this.msg);
}
}
父组件使用v-on:hook监听子组件的生命周期
//父组件
<template>
<div>
<child @hook:mounted="listenerChild"></child>
</div>
</template>
export default {
name: 'Father',
methods: {
listenerChild() {
console.log('父组件监听子组件');
}
}
}
//子组件
export default {
name: 'Child',
mounted() {
console.log('触发子组件mounted');
}
}
使用Vue.observable(),模拟一个简易的状态管理
//store.js
import Vue from 'vue';
export const state = Vue.observable({
count: 0
});
export const mutations = {
SET_COUNT(payload) {
if(payload > 0) state.count = payload;
}
}
//组件
import { state, mutations } from '../store.js';
<template>
<div>
<div @click="setCount">{{count}}</div>
</div>
</template>
export default {
name: 'test',
computed() {
count() {
return state.count;
}
},
methods: {
setCount() {
mutations.SET_COUNT(state.count++);
}
}
}
使用vuex通信
15. 自定义v-model
Vue.component('custom-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
:checked="checked"
@change="$emit('change', $event.target.checked)"
/>
`
});
16. vue style scoped
在组件中的style标签上加scoped属性,会让style标签中的样式只对当前组件元素生效,因为会在每一层dom节点上生成一个data-v-hash属性,作为当前组件的style的唯一标识
<template>
<div>
<p>test</p>
</div>
</template>
//这个加了scoped属性的style,只会对当前组件元素有效
<style scoped>
.p{
color: red;
}
</style>
17. 自定义组件
建议阅读这篇:手把手教你封装 Vue 组件,并使用 npm 发布
常规的vue自定义组件方式:
// customComponent.vue
<template>
<div class="custom-component">{{ text }}</div>
</template>
<script>
export default {
name: 'custom-component',
props: {
text: {
type: String,
default: '自定义组件内容'
}
},
computed: {},
methods: {}
};
</script>
// customComponent.js(导出自定义组件的js)
import CustomComponent from './customComponent.vue';
export default {
install(Vue) {
Vue.component(CustomComponent.name, CustomComponent);
}
};
// 引用自定义组件的代码,一般是在入口文件中
import Vue from 'vue';
import CustomComponent from '@/packages/customComponent.js';
Vue.use(CustomComponent);
一个调用其他组件的自定义组件:
import { MessageBox } from 'elementui';
export const Confirm = function (content, options = {}) {
return MessageBox({
title: options.title || '提示',
message: content,
confirmButtonText: options.okText,
cancelButtonText: options.cancelText,
showCancelButton: true,
showClose: true,
stopPropagation: true
});
};
// 通过将组件挂载在Vue的原型上,方便全局所有组件通过this.$confirm(content, options)来调用
export default {
install(Vue) {
Vue.prototype.$confirm = Confirm;
}
};
18. vue-router
这是vue专用的路由插件,通过hash、history、abstract三种模式,方便用户管理页面路由跳转
插件hash模式主要依赖于原生的hashchange监听事件来实现
插件history模式主要依赖于原生的pushState/replaceState来实现的,popstate监听事件在history执行前进、后退或跳转时会触发,可以在时间的event对象上找到event.state,这是pushState/replaceState改变url时通过第一个参数传的一个对象。
插件abstract模式不依赖bom和dom事件,因此可以脱离浏览器环境执行,比如在node中执行
<template>
<div>
<router-view />
</div>
</template>
import Vue from 'vue';
import VueRouter, { RouteConfig } from 'vue-router';
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/',
name: 'Layout',
component: () => import('页面地址'),
meta: {
//此属性配合vue内置的keep-alive组件使用,为true表示需要被缓存,false表示不需要缓存
keepAlive: true
}
}
]
});
export default router;
特别说明一下,abstract模式,可以实现不改变浏览器路由地址的页面内嵌,示例如下:
//某个组件内
<template>
<div>
<el-drawer>
<router-view />
</el-drawer>
</div>
</template>
import { routes } from '../router/index';
import VueRouter from 'vue-router';
export default {
name: 'ComponentText',
//在组件中创建一个独立的vue-router实例,不影响总路由的配置,只控制当前组件包裹的<router-view>的路由
router: new VueRouter({
mode: 'abstract',
base: '/',
routes
})
}
全局守卫:
import VueRouter from 'vue-router';
const router = new VueRouter();
//从beforeRouteLeave离开后,会调用到这个钩子函数
router.beforeEach((to, form, next) => {})
router.beforeResolve((to, from, next) => {});
//导航跳转完了会走到这个钩子函数
router.afterEach((to, from) => {});
路由守卫:
import VueRouter from 'vue-router';
const router = VueRouter({
mode: 'hash',
routes: [
{
name: '',
path: '',
component: () => import('页面地址')
beforeEnter: (to, from, next) => {
}
}
]
});
组件守卫:
export default {
name: 'ComponentText',
data() {
return {
msg: '掘金'
}
},
beforeRouteEnter(to, from, next) {
//beforeRouteEnter这一步,组件还没创建好实例,因此访问不到this,需要在next的回调中调取组件实例
next(vm => {
console.log(vm.msg);
})
},
//访问过的组件被再次激活时,调用到此钩子函数
beforeRouteUpdate(to, from, next) {
console.log(this.msg);
},
//离开组件时,会先调用到这个钩子函数
beforeRouteLeave(to, from, next) {
console.log(this.msg);
}
}
因为vue-router会对组件缓存用于激活复用,因此下面的情况,id可能会失效:
const router = new VueRouter({
routes: [
{
name: 'detail',
path: '/detail/:id',
component: Detail
}
]
})
可以使用下面两种方式解决:
//通过watch监听路由,获取参数进行请求
export default {
watch: {
'$route'() {
this.getData(this.$route.params.id);
}
}
}
//或者通过设置key,防止vue-router进行组件复用
//不推荐这一种,相当于把vue-router复用功能废掉了
<template>
<div>
<router-view :key="$route.fullPath" />
</div>
</template>
19. vuex
vuex是一个专为vue提供的状态管理工具,在全局用一个state存放数据,利用其封装的mutation方法来修改state,以达到更新数据状态的目的。虽然可以直接通过mutation修改state,但vuex不建议直接操作mutation,因为mutation必须是同步的,而action可以异步操作,所以推荐使用action来操作mutation,然后再由mutation修改state。
vuex本身没做数据持久化,所以刷新页面,state中的数据状态就会消失,因此可以使用插件vuex-persist,将state数据存储到cookie或localStorage中来完成数据持久化。
具体使用可查看官网例子,官网写的很简洁明白:vuex官网
20. 注意事项
-
某些情况下,通过v-if删除了一个dom节点,但是dom节点包裹的子节点代码片段并没有被删除,仍然在浏览器中占用着内存,最终导致内存溢出问题。需要通过在destoryed生命周期中销毁不再需要的dom片段、事件、监察者等。或者通过keep-alive的deactivated生命周期,销毁不再需要的dom片段、事件、监察者等。
-
vue秉承单向数据流原则,父级组件传给子级组件的props,不应该在子组件直接修改,假如有修改需求,应该通过computed计算属性,做一个根据props改变的计算数据。如果在子组件直接修改了props的数据,那么基本类型数据会报错,引用类型数据不会报错,但不会对父组件产生响应式影响,毕竟子组件会根据父组件改变,如果子组件可以直接影响父组件的响应式数据,那么响应式结构就会发生混乱,因此子组件不能修改父组件的props。
21. vue2开发优化
- 路由懒加载,在vue-router中使用import方式引入组件,然后在webpack中配置chunkFilename进行文件打包切割
- 注意v-if和v-show的使用场景
- v-for不要跟v-if一起使用,且需要加有唯一性的key
- 合理的使用computed和watch
- 图片懒加载,可以使用插件vue-lazyload实现,这个插件有个使用的坑,需要注意:
<template>
<div>
<ul>
<!--这里的循环中的img必须要加key,不然分页显示时图片还是显示第一页的图片-->
<li v-for="(item,idx) in imgs" :key="item.id">
<img v-lazy="item.src" :key="'img'-item.id" />
</li>
</ul>
</div>
</template>
- 适当采用keep-alive
- 响应式的数据对象,层级尽量不要太深,因为vue绑定对象响应式,是递归绑定的,对性能损耗较大
- 运用SSR或预渲染,预渲染可以使用插件prerender-spa-plugin,对指定路由页面生成静态的html,防止首屏加载过慢的问题
- 尽量利用tree shaking技术
- 如果不得不使用v-html属性,可以使用sanitize-html插件,对html进行消毒,尽量保证前端html片段的安全性
- webpack配置中,使用terser-webpack-plugin做多线程压缩,使用cache-loader做编译文件的缓存,使用thread-loader做loader多线程执行
npm install sanitize-html
import sanitizeHTML from 'sanitize-html';
Vue.prototype.$sanitize = sanitizeHTML;
v-html="$sanitize(html)"
说到这里,顺便提一下,webpack5新出的模块联邦功能,很神奇,可以做到让一个项目调用另一个项目封装的组件,但这有一个问题,就是需要不同的项目使用相同的技术栈,不然react项目的组件拿到vue项目中也用不了。
Vue3
1. 生命周期
vue3的生命周期钩子是通过在setUp()中调用的,setUp()是围绕beforeCreate和created执行的,因此不需要给这两个生命周期专门提供钩子函数
- onBeforeMount
- onMounted
- onBeforeUpdate
- onUpdated
- onBeforeUnmount
- onUnmounted
- onErrorCapture
- onRenderTracked
- onRenderTriggered
- onActivated
- onDeactivated
2. 用法
vue3语法有三种用法:
- 一种是与vue2基本一致的写法,应该是为了方便vue2的项目迁移而做的。
export default {
name: 'TestOne',
props: {
msg: {
type: String,
default: ''
}
},
data() {
return {
testList: [
{name: '列表一'},
{name: '列表二'},
{name: '列表三'},
]
}
},
computed: {
},
created() {
},
mounted() {
},
mothods: {
}
};
- 一种写法是js代码都写在setup函数中,然后通过vue3提供的各种钩子处理数据和逻辑。
//vue3的template中包裹的第一层节点,就不需要非得是个div了
<template>
<ul>
<li v-for="(item, idx) in list" :key="item.id"></li>
</ul>
<p>{{msg}}</p>
</template>
import {
defineComponent,
onBeforeMount,
onMounted,
reactive,
toRefs
} from 'vue';
type PageState = {
list: any
}
export default defineComponent({
name: 'TestTwo',
setup(props) {
const { msg } = toRefs(props);
let state = reactive<PageState>({
list: []
});
const getList = () => {
state.list = [
{id: '1',name: '列表一'},
{id: '2',name: '列表二'},
{id: '3',name: '列表三'},
];
};
onBeforeMount(() => {
getList();
});
onMounted(() => {
});
return {
msg,
...toRefs(state)
}
}
});
- 一种是直接在script标签带上属性setup,表示内部执行的都是setup运行的代码。
<script lang="ts" setup>
import { defineProps, computed } from 'vue';
import { useStore } from 'vuex';
import { key } from '@src/store';
type Props = {
msg: string
}
defineProps<Props>();
const store = useStore(key);
const count = computed(() => {
return store.state.count;
});
const inCrement = () => {
store.commit('increment');
};
</script>
axios笔记
1. 常规options设置
//会返回一个promise
let request = axios({
method: 'GET',
url: '',
params: {},
body: {},
responseType: 'json',
baseURL: '/api',
timeout: 30 * 1000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
});
更多配置项需要参考官方文档:axios中文文档
2. 拦截器
- 拦截器使用
/*
* 请求拦截器和响应拦截器都可以定义多个,它们会按照顺序执行
* request是先定义的后执行,response是先定义先执行,这跟它们往任务队列中的插入方式有关
*/
//请求拦截器
axios.intercepters.request.use(
config => {
},
error => {
}
);
axios.intercepters.request.use(
config => {
},
error => {
}
);
//响应拦截器
axios.intercepters.response.use(
res => {
},
error => {
}
);
- 拦截器实现原理
//此代码只是简单示意,并不是axios真实的内部代码实现
//定义一个拦截器任务队列
let queue = [];
//每个拦截器任务都是一个数组键值对
let task = [fulfilled, rejected];
//request拦截器,通过unshift插入到队列前面,因此越晚插入的越先执行
this.interceptors.request.forEach(interceptor => {
let task = [interceptor.fulfilled, interceptor.rejected];
queue.unshift(task);
});
//response拦截器,通过push插入到队列后面,因此越先插入的越先执行
this.interceptors.response.forEach(interceptor => {
let task = [interceptor.fulfilled, interceptor.rejected];
queue.push(task);
});
//最终形成的队列类似于下面的样子
[requestInterceptor, requestInterceptor, promise, responseInterceptor, responseInterceptor]