臭长警告!
尽管文章看起来很长,其实大部分都是结构性的代码。文章会很详细地为各个关键的知识点举例。相信很快就可以让我们回忆起每个关键的知识点及其用法。
一、
1. 插值语法
<template>
<div id="app">
{{ name }} <!-- 这里就是插值语法 -->
</div>
</template>
<script>
export default {
data () {
return {
name: '弼马温'
}
}
}
</script>
<style>
</style>
2. 指令
v-html、v-if、v-for、v-show、v-model
3. 动态属性
<template>
<div id="app">
{{ name }}
<p id="test">test</p>
<!-- 动态属性 -->
<p :id="dynamicId">test</p>
</div>
</template>
<script>
export default {
data () {
return {
name: '弼马温',
dynamicId: Math.random()
}
}
}
</script>
<style>
</style>
刷新一下页面,就可以看到第二个p标签的id发生了变化
4. 表达式
我们可以在 {{ }} 中写一些JS表达式,
什么是JS表达式?
可以赋值到一个变量上去的JS语句
<template>
<div id="app">
{{ monkeyName }}
<p id="test">test</p>
<p :id="dynamicId">test</p>
<!-- 表达式 -->
<p :id="dynamicId">{{isAMonkey ? monkeyName : pigName}}</p> <!-- {{}}中可以写一些JS表达式 -->
</div>
</template>
<script>
export default {
data () {
return {
monkeyName: '弼马温',
pigName: '猪悟能',
dynamicId: Math.random(),
isAMonkey: false
}
}
}
</script>
<style>
</style>
5. v-html
作用:以下方代码为例,作用就是用p1对应的标签替换掉div的子标签
<template>
<div id="app" v-html="p1">
{{ monkeyName }}
<p id="test">test</p>
<p :id="dynamicId">test</p>
<p :id="dynamicId">{{isAMonkey ? monkeyName : pigName}}</p>
</div>
</template>
<script>
export default {
data () {
return {
monkeyName: '弼马温',
pigName: '猪悟能',
dynamicId: Math.random(),
isAMonkey: false,
p1: '<p>呆子!</p>'
}
}
}
</script>
<style>
</style>
注意!v-html可能会造成XSS攻击,将p1转化为标签会有一个编译的过程,如果内部有恶意的js代码,可能就会被同时执行。
<template>
<div id="app" v-html="img1">
{{ monkeyName }}
<p id="test">test</p>
<p :id="dynamicId">test</p>
<p :id="dynamicId">{{isAMonkey ? monkeyName : pigName}}</p>
</div>
</template>
<script>
export default {
data () {
return {
monkeyName: '弼马温',
pigName: '猪悟能',
dynamicId: Math.random(),
isAMonkey: false,
p1: '<p>呆子!</p>',
img1: `<img src="123" onerror="alert('兄弟!你已经没了')"/>`
}
}
}
</script>
<style>
</style>
因此我们需要借助一些库将p1转化为一段纯净的html标签。
二、计算属性
<template>
<div id="app">
<p>{{number}}</p>
<p>number: {{computedNumber1}}</p>
<!-- 计算属性:数据的双向绑定 -->
<input v-model="computedNumber2">
</div>
</template>
<script>
export default {
data () {
return {
number: 5
}
},
computed: {
computedNumber1 () {
return this.number * 2
},
computedNumber2: {
<!-- 借助v-model绑定计算属性,实现数据的双向绑定,需要我们主动添加一个setter,
computedNumber1 中的return 本质上就相当于getter,getter只能让页面的数据跟随data的变化而变化
我们如果需要让data中的数据根据页面的变化而变化,就需要手动设定一个setter -->
get () {
return this.number * 2
},
set (value) {
this.number = value / 2
}
}
}
}
</script>
<style>
</style>
计算属性有一大优点,就是计算属性是否再次计算,取决于其绑定的data中的值是否发生变化,只有发生了变化,才会再次计算,
<template>
<div id="app">
<p>{{number}}</p>
<p>number: {{computedNumber1}}</p>
<input v-model="computedNumber2">
</div>
</template>
<script>
export default {
data () {
return {
number: 5,
number2: 5
}
},
computed: {
computedNumber1 () {
return this.number * 2
},
computedNumber2: {
get () {
console.log('???????')
return this.number * 2
},
set (value) {
console.log('!!!!!!!')
this.number = value / 2
}
}
}
}
</script>
<style>
</style>
我们修改了三次number2,但是两个计算属性绑定的都是number1,所以并不会导致computedNumber2被再次被执行。
??????是页面初始化时,执行的依次计算属性。
改变几次number试一下:
印证了上面的话,确实是没有问题的。
三、watch:检测数据的变化
<template>
<div id="app">
<input type="text" v-model="name">
<p>{{name}}</p>
<input type="text" v-model="info.hobby">
<p>{{info}}</p>
</div>
</template>
<script>
export default {
data () {
return {
name: '八戒',
info: {
hobby: '高老庄的小娘们'
}
}
},
watch: {
name(curValue, preValue){
console.log( 'name', curValue, preValue )
},
info(curValue, preValue){
console.log( 'name', curValue, preValue )
}
}
}
</script>
<style>
</style>
我们更改name的值,被watch检测到,触发name对应的函数,但是hobby的值发生更改,却无法被检测到。
原因很简单,采用的是值传递。而info是一个对象,存储的仅仅只是对象的地址。对象内的属性hobby发生改变,是不会影响对象所在的地址的。
监听属性值为基本数据类型的对象时,可以使用函数,但是,监听的如果是引用类型,就必须使用一个对象,我们调整一下代码:
<template>
<div id="app">
<input type="text" v-model="name">
<p>{{name}}</p>
<input type="text" v-model="info.hobby">
<p>{{info}}</p>
</div>
</template>
<script>
export default {
data () {
return {
name: '八戒',
info: {
hobby: '高老庄的小娘们'
}
}
},
watch: {
name(curValue, preValue){
console.log( 'name', curValue, preValue )
},
// 引用类型属性值的监听写法
info: {
handler: function(curValue, preValue){
console.log( 'info', curValue, preValue )
},
// 深度监听属性值
deep: true
}
}
}
</script>
<style>
</style>
任务完成!
四、动态绑定class和style
<template>
<ul class="list clearfix">
<li class="item" :class="{'active': isActive}">项目1</li>
<li class="item" :class="['active']">项目2</li>
<li class="item" :style="styleDate">项目3</li>
</ul>
</template>
五、v-show与v-if的区别
v-if不渲染未命中的元素,而v-show依旧存在于渲染树中,只不过display属性为none。
元素渲染后不会再发生改变,应该使用v-if,频繁发生改变的使用v-show
六、v-for遍历数组
<template>
<div>
<ul class="list clearfix">
<li v-for="(item, index) in arr" :key="item.id">
{{index+1}}-{{item.value}}
</li>
</ul>
<ul class="list clearfix">
<li v-for="(value, key, index) in obj" :key="key">
{{index}}: {{key}}: {{value}}
</li>
</ul>
</div>
</template>
<script>
export default {
data () {
return {
arr: [
{id: 1, value: '项目1'},
{id: 2, value: '项目2'},
{id: 3, value: '项目3'}
],
obj: {
name: '小马哥',
info: '好帅!'
}
}
}
}
</script>
注意:v-if与v-for不要联用,因为再Vue中,v-for的优先级要高于v-if。就可能出现v-for渲染了数据,但是v-if的值为false,又不允许数据被渲染。我们可以将v-if放到ul上,也可以放到 li 的内容的span上
七、事件
<template>
<div>
<button @click="incrementBy10(10, $event)">点击加10</button>
{{number}}
</div>
</template>
<script>
export default {
data () {
return {
number: 0
}
},
methods:{
incrementBy10(number, e){
this.number += number;
console.log( e )
}
}
}
</script>
八、父子附件通信
props & $emit
1. props:
<!-- 父:App.vue -->
<template>
<div id="app">
<MyInput />
<!-- 父组件向子组件传递数据 -->
<MyList :list="list" />
</div>
</template>
<script>
import MyInput from './components/Input';
import MyList from './components/List';
export default {
data () {
return {
list: [
{
id: 1,
title: '如何卷才能保持真正地高效卷?'
},
{
id: 2,
title: '我们应该停下成为卷王之王的脚步吗?'
},
{
id: 3,
title: '是什么让我们越来越卷?是求生欲?还是我乐意?'
},
{
id: 4,
title: '越卷真的可以越快乐?'
},
]
}
},
components: {
MyInput,
MyList
}
}
</script>
<style>
</style>
<!-- 子:Input.vue -->
<template>
<div>
<input type="text" v-model="title">
<button @click="add">添加</button>
</div>
</template>
<script>
export default {
data () {
return {
title: ''
}
},
methods:{
add(){
}
}
}
</script>
<style>
</style>
<!-- 子:List.vue -->
<template>
<div class="list">
<ul>
<!-- 同时将数据渲染到页面 -->
<li v-for="item in list" :key="item.id">
{{item.title}}
</li>
</ul>
</div>
</template>
<script>
export default {
// 子组件接收数据
props: {
list: Array
}
}
</script>
<style>
</style>
2. $emit
<!-- App.vue -->
<template>
<div id="app">
<!--订阅addItem消息(这种方式,只能订阅子组件发布的消息)-->
<MyInput @addItem="add"/>
<MyList :list="list" />
</div>
</template>
<!-- Item.vue -->
<template>
<div>
<input type="text" v-model="title">
<!-- 为按钮绑定事件 -->
<button @click="add">添加</button>
</div>
</template>
<script>
export default {
data () {
return {
title: ''
}
},
methods:{
add(){
// 在点击时,发布消息(这种发布方式只能由父组件监听)
this.$emit('addItem', this.title);
}
}
}
</script>
<style>
</style>
List.vue不变,然后在输入框中输入内容,点击添加进行测试:
成功!
九、兄弟组件通信:事件总线eventBus
eventBus其实就是一个Vue的实例
// eventBus.js
import Vue from 'vue';
const eventBus = new Vue();
export default eventBus;
我们借助Vue实例身上的$emit方法来讲数据广播出去,需要接收数据的地方,只需要$on来监听广播即可。
<!-- Input.vue -->
<template>
<div>
<input type="text" v-model="title">
<button @click="add">添加</button>
</div>
</template>
<script>
// 引入事件总线
import eventBus from '../event-bus'
export default {
data () {
return {
title: ''
}
},
methods:{
add(){
this.$emit('addItem', this.title);
// 通过事件总线,讲数据广播出去
eventBus.$emit('broadcastAResource', this.title);
}
}
}
</script>
<style>
</style>
<!-- List.vue -->
<template>
<div class="list">
<ul>
<li v-for="item in list" :key="item.id">
{{item.title}}
</li>
</ul>
</div>
</template>
<script>
import eventBus from '../event-bus';
export default {
props: {
list: Array
},
methods: {
// 处理对应的参数
handleAddTitle(title){
console.log( '拿到了广播的数据:', title );
}
},
mounted(){
// 监听事件总线的广播,获取同名广播内的数据,然后调用对应的函数,数据位于函数参数中
eventBus.$on('broadcastAResource', this.handleAddTitle);
}
}
</script>
<style>
</style>
测试一下:
就在刚刚,突然注意到了 之前在学习Vue的时候,忽略的一点:
为什么不直接用$emit去将数据发出去,然后通过@xxx去订阅对应数据呢?
原因很简单,回顾之前的例子,简单的$emit,使用的场景是父组件中引用了子组件,父组件可以获取$emit中对应的事件,所以@addItem就直接拿到了。但是现在是兄弟组件,他们是通过一个公共的父组件建立的联系,彼此之间并不能直接联系。因此,我们就不能单纯地采用$emit了。
于是,事件总线eventBus的效果就凸显出来了。
十、父子组件声明周期执行顺序
Vue实例创建完毕时,执行created;渲染完毕后,执行mounted;数据发生改变时,执行updated;销毁时,执行destroyed函数
单个组件中的生命周期,可以参考这篇文章:
移动端项目总结 - Vue 生命周期函数_拯救世界的光太郎-CSDN博客
上面这篇文章言简意赅,只分析重点,看完之后大致就能让你回忆起各个声明周期函数的特点了。
接下来,主要分析父子组件声明周期函数的执行顺序:
<!-- App.vue -->
<template>
<div id="app">
<MyInput @addItem="add"/>
<MyList :list="list" />
</div>
</template>
<script>
import MyInput from './components/Input';
import MyList from './components/List';
export default {
data () { ...
},
methods: { ...
},
created(){
console.log( 'parent: App created' );
},
mounted(){
console.log( 'parent: App mounted' );
},
beforeUpdate(){
console.log( 'parent: App before update' );
},
updated(){
console.log( 'parent: App updated' );
},
components: { ...
}
}
</script>
<style>
</style>
<!-- List.vue -->
<template>
<div class="list">
<ul>
<li v-for="item in list" :key="item.id">
{{item.title}}
</li>
</ul>
</div>
</template>
<script>
import eventBus from '../event-bus';
export default {
props: { ...
},
methods: { ...
},
created(){
console.log( 'child: List created' );
},
mounted(){
eventBus.$on('broadcastAResource', this.handleAddTitle);
console.log( 'child: List mounted' );
},
beforeUpdate(){
console.log( 'child: List before update' );
},
updated(){
console.log( 'child: List updated' );
}
}
</script>
<style>
</style>
看一下初次加载的打印结果:
先创建父组件实例,然后创建子组件实例,子组件先渲染,然后父组件渲染
在输入框中输入一个数据,然后添加进入,看一下执行结果:
为什么是父组件先执行before update呢?
原因很简单,数据存储在父组件。我们点击按钮,然后在Input组件中通过$emit将数据传递给父组件,父组件触发对应事件将数据添加进date中,父组件的数据发生变化,于是父组件的before update就先触发了,这个数据会被传递到List组件中,于是接下来,List也触发了before update。
为什么子组件的update先执行呢?
这是因为,我们将数据传递给子组件,然后子组件去渲染数据,子组件渲染完毕了,父组件调用子组件才算是渲染完毕。如果子组件没有渲染完成,那么父组件的update也就会一直等待着子组件。
十一、$nextTick
看一个打印异常的例子:
<template>
<div id="app">
<ul ref="ulRef">
<li v-for="item in list" :key="item.id">
{{item.title}}
</li>
</ul>
<button @click="add">添加</button>
</div>
</template>
<script>
export default {
data () {
return {
list: [
{
id: 1,
title: '如何卷才能保持真正地高效卷?'
},
{
id: 2,
title: '我们应该停下成为卷王之王的脚步吗?'
},
{
id: 3,
title: '是什么让我们越来越卷?是求生欲?还是我乐意?'
},
{
id: 4,
title: '越卷真的可以越快乐?'
},
]
}
},
methods: {
add(){
this.list.push({
id: Math.random(),
title: Math.random()
})
const ulElem = this.$refs.ulRef;
const length = ulElem.childNodes.length;
console.log( 'length: ', length );
}
}
}
</script>
<style>
</style>
点击添加按钮:
命名是五个数据,而且显示数据长度的代码,也在渲染追加 li 的下方,为什么length却是4呢?
这是因为,在Vue中,渲染是异步的。也就是说,我们写的打印长度的代码已经执行完了,这个时候,新的li其实还没有被追加到ul中去,所以求得的length还依旧是4。
怎么解决呢?
很简单,借助$nextTick。
methods: {
add(){
this.list.push({
id: Math.random(),
title: Math.random()
})
this.$nextTick(()=>{
const ulElem = this.$refs.ulRef;
const length = ulElem.childNodes.length;
console.log( 'length: ', length );
})
}
}
this.$nextTick接收一个箭头函数,这个箭头函数在页面最终渲染完毕(多次更新节点数据时,只在全部都更新完后才会执行)后执行,在这里处理数据,就可以保证数据是最新的了!
十二、插槽slot
基本使用:
1. 定义一个插槽
<!-- slotTest.vue -->
<template>
<div class="slot-test">
<!-- 相当于一个槽,我们可以将这个槽安装到任意位置 -->
<slot></slot>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
2. 安装插槽、将配件插入插槽
<!-- App.vue -->
<template>
<div id="app">
<!-- 相当于将槽安装到了电脑上,我们需要用这个槽时,就可以将对应的配件插件去 -->
<slotTest>
<!-- 向槽中插入对应的配件 -->
<p>人生最爱:小树林</p>
<!-- --------------- -->
</slotTest>
<!-- ---------------------------------------------------- -->
</div>
</template>
<script>
// 引入插槽
import slotTest from './slotTest.vue'
export default {
data () {
},
// 将插槽声明成组件,就可以使用
components: {
slotTest
}
}
</script>
<style>
</style>
再来看一个稍微复杂一丢丢的用法:
结构上是插槽套了一个插槽,不过也很容易理解,我们将插槽看作它的父级元素即可,来看一下
<!-- App.vue -->
<template>
<div id="app">
<list-slot>
<item-slot>娘子!</item-slot>
<item-slot>啊哈~</item-slot>
<item-slot>ku~liu~liu~liu~dei~hei~</item-slot>
</list-slot>
</div>
</template>
<script>
import listSlot from './components/listSlot.vue';
import itemSlot from './components/itemSlot.vue';
export default {
data () {
},
components: {
'list-slot': listSlot,
'item-slot': itemSlot
}
}
</script>
<style>
</style>
<!-- listSlot.vue -->
<template>
<ul class="list-slot">
<slot></slot>
</ul>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
<!-- itemSlot.vue -->
<template>
<li>
<slot></slot>
</li>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
3. 插槽默认值
这个很简单,只需要在<slot>默认值</slot>中间写上对应的默认值即可。
十三、插槽传参
date数据位于CurrentUser插槽中,但我们想在App.vue中使用这些数据,怎么办呢?
很简单,只需要在使用数据的地方,外面套一层template标签,然后给标签添加v-slot:default属性,即可拿到被引用的插槽传来的数据,看下面的例子
<!-- App.vue -->
<template>
<div id="app">
<current-user>
<!-- 在数据的外面,套一层template标签,然后添加一个属性即可获取到传递来的数据 -->
<template v-slot:default="{ user }">
{{user.name}}: {{user.skill}} biu~ biu~ biu~
</template>
</current-user>
</div>
</template>
<script>
import CurrentUser from './components/CurrentUser.vue';
export default{
components: {
CurrentUser
}
}
</script>
<style>
</style>
<!-- CurrentUser.vue -->
<template>
<h1>
<slot :user="user">
{{ user.name }}
</slot>
</h1>
</template>
<script>
export default {
data() {
return {
user: {
name: '动感超人',
skill: '动感光波!'
}
}
}
}
</script>
<style scoped>
</style>
一定要注意,"v-slot:defalult" v与default之间,不要习惯性地加一个空格,这里整体是一个属性,被空格分隔开就无法识别了,然后就会报下面的错误:
十四、具名插槽
具名插槽,就是我们可以定义多个插槽。在使用插槽时,通过v-slot指定具体插槽的名字,将内容放至对应的插槽中,而没有指定名字的部分,都被放置默认插槽中
其次,页面显示的顺序,取决于我们在插槽组件中,插槽定义的顺序,而非使用插槽的顺序,我们一起来看一下:
<!-- App.vue -->
<template>
<div id="app">
<layout>
<p>文本1</p>
<p>文本2</p>
<template v-slot:footer>
<h1>footer</h1>
</template>
<template v-slot:header>
<h1>header</h1>
</template>
<p>文本3</p>
</layout>
</div>
</template>
<script>
import Layout from './components/Layout.vue';
export default{
components: {
Layout
}
}
</script>
<style>
</style>
<!-- Layout.vue -->
<template>
<div id="app">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
十五、动态组件
<!-- App.vue -->
<template>
<div id="app">
<div v-for="item in productData" :key="item.id">
<!-- 可以看到,:is的值,应该与我们注册组件时,对组件命的名相同 -->
<component :is="`My${item.type}`"></component>
</div>
</div>
</template>
<script>
import Image from './components/Image.vue';
import Text from './components/Text.vue';
import Video from './components/Video.vue';
export default{
// 模拟从后台获取的数据
data() {
return {
productData: [
{
id: 1,
type: 'Text'
},
{
id: 2,
type: 'Image'
},
{
id: 3,
type: 'Video'
}
]
}
},
components: {
// 其实这里可以不去重命名,这样在使用时,就不需要在:is中去拼接My了
MyImage: Image,
MyText: Text,
MyVideo: Video
}
}
</script>
<style>
</style>
<!-- Video、Text、Image -->
<template>
<div id="video">
<h1>
视频
</h1>
</div>
</template>
<script>
export default {
// name: 'Video'
}
</script>
<style scoped>
</style>
页面中渲染的数据,就取决于后台返回的数据。可以根据后台返回的数据,任意顺序、任意数量地使用组件。
十六、异步组件
当我们的组件中,引入着一些非常大的组件时,又不确定用户是否为激活这些组件,若直接将这些组件加载下来,会造成很大的资源浪费,因此我们就可以选择异步加载。只有当用户激活这些组件了,才去加载,不激活就不加载,可以在一定程度上节省资源的开销。
<!-- 异步引入方式 -->
<template>
<div id="app">
<test v-if="show"></test>
<button @click="show = true">显示Test组件</button>
</div>
</template>
<script>
import Test from './components/Test.vue'
export default{
data() {
return {
show: false
}
},
components: {
// Test: () => import('./components/Test')
Test
}
}
</script>
<style>
</style>
<!-- Test.vue -->
<template>
<div id="app">
Test组件
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
先来看一下同步import引入,由components注册组件的结果是什么样的:
可以看到,虽然我们没有点击按钮,但是Test组件其实已经被加载出来了,只是还没有渲染到页面,我们如果在是这个时候点击按钮,是不可能再去发送请求的,因为Test资源已经被请求过了。
对比一下异步引入Test组件的结果:
可以看到,在我们还没有点击按钮时,Test组件并没有被加载到本地。
点击按钮:
可以看到,成功加载至本地。
这就是同步与异步的差别,当一些资源,很有可能不会被用户浏览,或者说被用户浏览的概率非常非常小,就可以选择用异步的方式。减少页面初次加载时发送的请求数量,来降低请求时间,提升页面响应速度,给用带来更流畅的使用体验。
十七、用keep-alive实现组件缓存
ABC分别对应三个组件,点击不同的按钮,可以控制触发不同的组件
<!-- App.vue -->
<template>
<div id="app">
<button @click="state = 'A'">A</button>
<button @click="state = 'B'">B</button>
<button @click="state = 'C'">C</button>
<comp-a v-if="state === 'A'"></comp-a>
<comp-b v-if="state === 'B'"></comp-b>
<comp-c v-if="state === 'C'"></comp-c>
</div>
</template>
<script>
import CompA from './components/CompA.vue'
import CompB from './components/CompB.vue'
import CompC from './components/CompC.vue'
export default{
data() {
return {
state: 'A'
}
},
components: {
CompA,
CompB,
CompC
}
}
</script>
<style>
</style>
<!-- CampA.vue、CampB.vue、CampC.vue -->
<template>
<div id="comp-a">
组件A
</div>
</template>
<script>
export default {
mounted(){
console.log( '组件A渲染' );
},
destroyed(){
console.log( '组件A销毁' );
}
}
</script>
<style scoped>
</style>
"组件A渲染" 触发于页面的初次渲染
然后我们依次点击B、C、A按钮,可以看到:
如果每个组件的代码量都非常地大,组件不断地渲染、销毁,会十分消耗性能。
这个时候,我么就可以借助keep-alive对Dom结构进行缓存,使用方式很简单,只需要将需要缓存的组件放到keep-alive中即可:
<!-- App.vue -->
<template>
<div id="app">
<button @click="state = 'A'">A</button>
<button @click="state = 'B'">B</button>
<button @click="state = 'C'">C</button>
<keep-alive>
<comp-a v-if="state === 'A'"></comp-a>
<comp-b v-if="state === 'B'"></comp-b>
<comp-c v-if="state === 'C'"></comp-c>
</keep-alive>
</div>
</template>
这个时候我们再去点击三个按钮,就会发现,仅仅只会去执行重新渲染的操作,而不再去销毁、创建。
即使我们再次点击A按钮,也不会再次去执行组件A渲染,因为这些都可以从缓存中获取
易混知识点对比:v-show于keep-alive
<!-- App.vue -->
<template>
<div id="app">
<button @click="state = 'A'">A</button>
<button @click="state = 'B'">B</button>
<button @click="state = 'C'">C</button>
<comp-a v-show="state === 'A'"></comp-a>
<comp-b v-show="state === 'B'"></comp-b>
<comp-c v-show="state === 'C'"></comp-c>
</div>
</template>
我们发现,页面一加载,组件ABC就全部被加载出来了。这显然是不符合我们的需求的,我们一开始的目的,就是在寻找针对较大组件的解决方案。
一下子就把全部的大组件都加载出来,这得消耗多少性能,给用户带来的体验,恐怕就只有:这软件太卡了,启动地这么慢!
十八、使用mixin抽离公共逻辑
我们书写代码时,难免会遇到很多公共的属性、方法、生命周期函数,如果我们在每个组件都写入这些代码,第一代码冗余,组件间的数据维护十分困难
我们可以定义一个mixin文件,来专门存放这些公共的数据,这样就很大程度地降低了开发难度
<!-- App.vue -->
<template>
<div id="app">
<comp-a></comp-a>
<hr>
<comp-b></comp-b>
</div>
</template>
<script>
import CompA from './components/CompA.vue';
import CompB from './components/CompB.vue';
export default{
components: {
CompA,
CompB
}
}
</script>
<style>
</style>
<!-- CompA.vue、CompB.vue -->
<template>
<div id="comp-a">
<p>{{ aData }}</p>
<p>{{ commonData }}</p>
<button @click="aMethod">aMethod</button>
<button @click="commonMethod">commonMethod</button>
</div>
</template>
<script>
export default {
data(){
return {
aData: '组件A的数据',
commonData: '公共的数据'
}
},
methods: {
aMethod() {
console.log( '组件A的方法' );
},
commonMethod() {
console.log( '公共的方法' );
}
},
mounted() {
console.log( '组件A mounted' );
console.log( 'common mounted' );
}
}
</script>
<style scoped>
</style>
可以看到,其实组件A、B之间有很多重复的公共代码,虽然这样写起来十分复杂,但是只要功底够硬,运行起来还是没有问题的:
不过,对代码的维护,可能就是个十分困难的问题。
既然有这么多公共的代码,我们为什么不将这些代码维护到一起呢?
来,我们一起看一下如果借助mixin,将公共的代码给维护起来:
先创建一个mixin.js文件
// mixin.js
export default{
data(){
return {
commonData: '公共的数据'
}
},
methods: {
commonMethod() {
console.log( '公共的方法' );
}
},
mounted() {
console.log( 'common mounted' );
}
}
去掉组件A中公共的数据,然后将mixin引入进去
<template>
<div id="comp-a">
<p>{{ aData }}</p>
<p>{{ commonData }}</p>
<button @click="aMethod">aMethod</button>
<button @click="commonMethod">commonMethod</button>
</div>
</template>
<script>
import mixin from './mixin';
export default {
mixins: [mixin],
data(){
return {
aData: '组件A的数据',
}
},
methods: {
aMethod() {
console.log( '组件A的方法' );
}
},
mounted() {
console.log( '组件A mounted' );
}
}
</script>
<style scoped>
</style>
测试,一切正常,这样对于公共数据的维护,减轻了极大的负担。
十九、Vue3与Vue2声明周期的区别
1. vue2中的beforeDestroy和destroyed,在Vue3中变成了beforeUmount、unmounted。
2. Vue3提供的Composition API写法
<!-- App.vue -->
<template>
<div id="app">
{{ message }}
</div>
</template>
<script>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue';
export default{
// Vue3
setup() { // setup 相当于 beforeCreate + created 两个生命周期函数
onBeforeMount(()=>{ })
onMounted(()=>{ })
onBeforeUpdate(()=>{ })
onUpdated(()=>{ })
onBeforeUnmount(()=>{ })
onUnmounted(()=>{ })
},
// Vue2
beforeCreate(){ },
created(){ },
beforeMount(){ },
mounted(){ },
beforeUpdate(){ },
updated(){ },
onBeforeUnmount(){ },
unmounted(){ }
}
</script>
<style>
</style>
二十、Composition API之 代码优化
// UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
data () {
return {
repositories: [], // 1
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories () {
// 使用 `this.user` 获取用户仓库
}, // 1
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
该组件可能拥有三个任务:1. 渲染一个仓库列表 2. 支持对仓库数据的过滤 3. 以及筛选
我们会在这其中写很多属性方法,以提供对上述功能的支持。
可以看到,每个任务的不同不同操作,被分割在了不同的选项中,于是,代码看起来就会是这种感觉:
这个时候,对于代码的维护,就会变得十分困难,因为同一个功能,方法被写的七零八散,寻找对应的代码,就十分地麻烦
这时,Composition API就可以发挥它的作用了
它地目的就会对代码按功能进行分割,一个组件包含了三个功能,那么,每个功能都可以对应一个setup
与用户列表有关的功能:
// UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
// 在我们的组件内
setup (props) {
let repositories = []
const getUserRepositories = async () => {
repositories = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories // 返回的函数与方法的行为相同
}
}
可以看到,存放仓库数据的代码都在一个setup中,以及根据不同的用户请求不同仓库的方法
// useUserRepositories.js
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'
export default function useUserRepositories(user) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
同时,对搜索功能也进行拆分:
// userRepositoryNameSearch.js
import { ref, computed } from 'vue'
export default function useRepositoryNameSearch(repositories) {
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(repository => {
return repository.name.includes(searchQuery.value)
})
})
return {
searchQuery,
repositoriesMatchingSearchQuery
}
}
过滤也是如此,
然后在文件中将每个不同的功能都引入进去
import useUserRepositories from './composables/userRepositoryName';
import useRepositoryNameSearch from './composables/useRepositoryNameSearch';
import useRepositoryFilters from './composables/useRepositoryFilters';
export default{
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props){
const {user} = toRefs(props);
// 请求用户仓库
const {
repositories,
getUserRepositories
} = useUserRepositories(user);
// 搜索
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories);
// 过滤
const {
filters,
updateFilters,
filteredRepositories
} = useRepositoryFilters(repositoriesMatchingSearchQuery);
return {
// 因为我们并不关心未经过滤的仓库
// 我们可以在`repositories`名称下暴露过滤后的结果
repositories: filteredRepositories,
getUserRepositories,
searchQuery,
filters,
updateFilters
}
}
}