vue的基本使用和高级特性,周边插件vuex和vue-router
- 一、vue的使用
- 1、vue-cli
- 2、基本使用
- (1)模板(插值,指令)
- (2)computed和watch
- (3)class和style
- (4)条件
- (5)循环(列表)渲染
- (6)事件
- (7)表单
- 3、组件
- (1)props和$emit(适合父子组件间的通信)
- (2)组件间通信 - 自定义事件
- (3)组件生命周期
- 二、vue的高级特性
- 1、自定义 v-model
- 2、$nextTick
- (1)知识点
- (2)例子展示🌰
- 3、slot插槽
- (1)插槽的作用
- (2)三种插槽类型
- 1)普通slot插槽
- 2)作用域插槽slot-scope
- 3)具名插槽
- 4、动态、异步组件
- (1)动态组件
- 1)举个例子🌰
- 2)动态组件的用法和应用场景
- 3)演示
- (2)异步组件
- 5、keep-alive
- (1)定义和应用场景
- (2)举例🌰
- 6、mixin
- (1)mixin是什么
- (2)mixin的问题
- (3)举个栗子🌰
- 三、vue的周边插件:vuex和vue-router
- 1、vuex
- (1)vuex基本概念
- (2)用于vue组件中的API
- (3)图例
- 2、vue-router
- (1)路由模式
- (2)路由配置
- 四、结束语
以下文章将讲解对 vue
的基本使用以及各种高级特性还有周边插件 vuex
和 vue-router
,融合大量案例🌰和动图🕹️进行展示。可以把它当成是 vue
的入门宝库,有不懂的语法知识点时或许在这里可以寻找到你的答案并且通过例子运用起来。
废话不多说,下面来开始探索 vue
的奥秘吧🙆
一、vue的使用
1、vue-cli
vue
项目是基于 vue-cli
脚手架搭建的项目。当我们要创建一个项目时,首先要先全局安装 vue-cli
脚手架,命令行为:
npm i -g @vue/cli
在搭建完成项目以后,我们需要来了解 src
目录下各个文件夹和文件的用法。
├── assets 放置静态资源
├── components 放组件
├── router 定义路由的相关配置
├── views 视图
├── views 视图
├── app.vue 应用主组件
├── main.js 入口文件
2、基本使用
(1)模板(插值,指令)
1)插值、表达式
<template><div><p>文本插值 {{message}}</p><p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p></div>
</template><script>
export default {data() {return {message: 'I am Monday.',flag: true}}
}
</script>
//浏览器显示结果
// 文本插值 I am Monday.
// JS 表达式 yes
2)动态属性
<template><div><p :id="dynamicId">动态属性:{{dynamicId}}</p></div>
</template><script>
export default {data() {return {dynamicId: `id-${Date.now()}`}}
}
</script>//浏览器显示结果
//动态属性:id-1622339576875
3)v-html指令
<template><div><p v-html="rawHtml"><span>有 xss 风险</span><span>【注意】使用 v-html 之后,将会覆盖子元素</span></p></div>
</template><script>
export default {data() {return {rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜体</i>',}}
}
</script>//浏览器显示结果
//指令 - 原始 html 加粗 斜体
值得注意的是, v-html
指令会有 xss
风险,且会覆盖子组件,要谨慎使用!!
(2)computed和watch
1)computed
computed
有缓存, data
不变则不会重新计算。如下代码所示:
<template><div><p>num {{num}}</p><p>double1 {{double1}}</p>//用v-model一定要有get和set,否则会报错<input v-model="double2"/></div>
</template><script>
export default {data() {return {num: 20}},computed: {double1() {return this.num * 2},double2: {// 获取值get() {return this.num * 2},// 设置值set(val) {this.num = val/2}}}
}
</script>
此时浏览器的打印结果如图所示。
大家可以看到,当 num
的值如果一直是 20
的值时,那么 double1
和 double2
的 get()
方法是不会重新计算的,它会被缓存下来,达到提高运算性能的效果。
2)watch
watch
监听基本数据类型时,可正常拿到oldVal
和val
的值。watch
如果监听引用数据类型时,需要进行深度监听,且拿不到oldVal
。因为指针相同,监听时,指针已经指向了新的val
。
如下代码所示:
<template><div><input v-model="name"/><input v-model="info.city"/></div>
</template><script>
export default {data() {return {name: 'Monday',info: {city: 'FuZhou'}}},watch: {name(oldVal, val) {// eslint-disable-next-lineconsole.log('watch name', oldVal, val) // 值类型,即基本数据类型,可正常拿到 oldVal 和 val},info: {handler(oldVal, val) {// eslint-disable-next-lineconsole.log('watch info', oldVal, val) // 引用数据类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val},deep: true // 深度监听}}
}
</script>
(3)class和style
-
使用动态属性,即
v-bind
绑定; -
使用驼峰式写法。
如下代码所示:
<template><div><p :class="{ black: isBlack, yellow: isYellow }">使用 class</p><p :class="[black, yellow]">使用 class (数组)</p><p :style="styleData">使用 style</p></div>
</template><script>
export default {data() {return {isBlack: true,isYellow: true,black: 'black',yellow: 'yellow',styleData: {fontSize: '40px', // 转换为驼峰式color: 'red',backgroundColor: '#ccc' // 转换为驼峰式}}}
}
</script><style scoped>.black {background-color: #999;}.yellow {color: yellow;}
</style>
此时浏览器的显示效果如下。
(4)条件
v-if
、v-else
的用法:可使用变量,也可以使用 === 表达式。v-if
和v-show
的区别?v-if
会根据条件对元素进行渲染的控制,此处的控制渲染指的是将元素添加到dom
中或移除dom
,所以会存在dom的增删。假设页面初始化时,条件判断结果为false
,则不会将该元素添加到dom
当中。v-show
与v-if
不一样的是,不管条件是true
还是false
,都会将对应的元素添加到dom
中,在条件为false
时将元素的css
属性display
设置为none
,所以我们只是肉眼看到元素从页面中消失了,但是它还存在于dom
当中。
v-if
和v-show
的使用场景?v-if
适用于条件变化频率不高的时候,这样不会频繁的去对DOM
进行增删操作;v-show
适用于需要做频繁切换的时候,比如说A和B两个元素,需要一会显示A
,一会显示B
,这样就算比较频繁。如果去操作v-if
,让DOM
疯狂的增添和销毁是会非常耗费性能的,所以这个时候就应该用v-show
,display
属性原本就已经存放在DOM
当中,只是对display
属性进行修改修做即可。
具体使用方式如下代码所示:
<template><div><p v-if="type === 'a'">A</p><p v-else-if="type === 'b'">B</p><p v-else>other</p><p v-show="type === 'a'">A by v-show</p><p v-show="type === 'b'">B by v-show</p></div>
</template><script>
export default {data() {return {type: 'a'}}
}
</script>
(5)循环(列表)渲染
-
vue
中如何遍历对象? —— 使用v-for
。 -
v-for
和v-if
不能一起使用。 -
key
的重要性。在对数据进行v-for
遍历时,需要加上key
值来保证数据的唯一性。同时,key
不能乱写,一般不要使用random
或者index
,而是绑定一个与该绑定数据相关的唯一值来确定。如果用random
或者index
时,在对数据进行删除或增添操作时,有可能会出现数据错乱等问题出现。 -
key
的作用。在对节点进行diff
的过程中,判断两个节点是否为相同节点的一个很重要的条件就是key
值是否相等,如果key
值相等,则说明两个节点是相同节点,所以会尽可能的复用原有的DOM
节点,减少不必要的DOM
操作,提升系统性能。
下面用一段代码演示 v-for
遍历数组和遍历对象时的效果。
<template><div><p>遍历数组</p><ul><li v-for="(item, index) in listArr" :key="item.id">{{index}} - {{item.id}} - {{item.title}}</li></ul><p>遍历对象</p><ul ><li v-for="(val, key, index) in listObj" :key="key">{{index}} - {{key}} - {{val.title}}</li></ul></div>
</template><script>
export default {data() {return {listArr: [{ id: 'a', title: '今天周一' }, // 在数据结构中,最好有 id ,目的是为了方便使用 key{ id: 'b', title: '今天周二' },{ id: 'c', title: '今天周三' }],listObj: {a: { title: '今天周一' },b: { title: '今天周二' },c: { title: '今天周三' },}}}
}
</script>
此时浏览器的显示效果如下所示:
(6)事件
1)event参数,自定义参数
下面通过绑定一个两个按钮事件,来观察 vue
中的事件,参数时怎么进行传递的,事件又会被绑定到哪里去?
<template><div><!-- <p>{{num}}</p> --><!-- event参数 --><button @click="increment1">+1</button><!-- 自定义参数 --> <button @click="increment2(2, $event)">+2</button><div>递增值:{{num}}</div></div>
</template><script>
export default {data() {return {num: 0}},methods: {increment1(event) {console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象console.log(event.target) //vue中的event,放在什么元素下,就会被挂载在什么元素下console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样this.num++// 1. event 是原生的// 2. 事件被挂载到当前元素// 和 DOM 事件一样},increment2(val, event) {console.log(event.target)this.num = this.num + val}}
}
</script>
大家可以看到,上面的 increment1
方法,没有传递参数,则它可以直接使用 event
来触发当前对象;而 increment2
方法中,通过传递了一个 2
的参数,我们可以通过 $event
的方式,把 2
传递进来,之后同样用 event
触发原生对象。
同时,当触发到当前元素时,事件会被挂载到当前元素,和 DOM
事件一样。
此时浏览器的打印效果如下:
2)自定义事件
上面的演示中, vue
事件是直接挂载到 vue
上面进行监听的。而下面的代码中,我们可以看到,如果我们需要自定义一个事件,而不进行直接挂载,则可以通过 addEventListener
来进行注册。
但值得注意的是,用 vue
绑定的事件,组件销毁时会自动进行解绑。但是呢,如果是自己定义的事件,需要自己再进行手动销毁!!这一点需要特别注意,不然写代码过程很容易因为没有销毁热导致后面引发的一系列 bug
出现。
<template><div></div>
</template><script>
export default {data() {return {num: 0}},methods: {loadHandler() {// do some thing}},mounted() {window.addEventListener('load', this.loadHandler)},beforeDestroy() {//【注意】用 vue 绑定的事件,组建销毁时会自动被解绑// 但如果是,自己绑定的事件,需要自己销毁!!!window.removeEventListener('load', this.loadHandler)}
}
</script>
3)事件修饰符
对于 vue
来说,经常用到的修饰符有下面6种事件修饰符和3种按键修饰符。
<!-- 事件修饰符 --><!-- 阻止单击事件继续传播 -->
<a v-on:stop="doThis"></a><!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form><!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a><!-- 只有修饰符 -->
<form v-on:submit.prevent></form><!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div><!-- 只当在event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
<!-- 按键修饰符 --><!-- 即使Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button><!-- 有且只有ctrl被按下时候才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button><!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click,exact="onClick">A</button>
(7)表单
在vue项目中,主要使用 v-model
指令在表单 input
、 textarea
、 checkbox
、 radio
、 select
等元素上创建双向数据绑定。
对于 v-model
来说,常见的修饰符有 trim
、 lazy
和 number
。 trim
表示去除输入内容前后的空格, lazy
表示需要按回车键数据才会进行创建, number
表示把用户输入的值转换为 number
类型,如果先输入字符,则整串数据都会被当成字符串 string
类型看待,如果先输入数字,后输入字母,则后面的字母无法输入到输入框当中。
接下来我们用代码演示一遍常见的表单。
<template><div><p>输入框: {{name}} {{age}}</p><!-- .trim去除前后空格 --><input type="text" v-model.trim="name"/><!-- .lazy需要按回车键数据才会起作用 --><input type="text" v-model.lazy="name"/><!-- .number表示把用户输入的值转为number类型 --><input type="text" v-model.number="age"/><hr><p>多行文本: {{intro}}</p><textarea v-model="intro"></textarea><!-- 注意,<textarea>{{intro}}</textarea> 是不允许的!!! --><hr><p>复选框 {{checked}}</p><input type="checkbox" v-model="checked"/><hr><p>多个复选框 {{checkedNames}}</p><input type="checkbox" id="jack" value="Jack" v-model="checkedNames"><label for="jack">Jack</label><input type="checkbox" id="john" value="John" v-model="checkedNames"><label for="john">John</label><input type="checkbox" id="mike" value="Mike" v-model="checkedNames"><label for="mike">Mike</label><hr><p>单选 {{gender}}</p><input type="radio" id="male" value="male" v-model="gender"/><label for="male">男</label><input type="radio" id="female" value="female" v-model="gender"/><label for="female">女</label><hr><p>下拉列表选择 {{selected}}</p><select v-model="selected"><option disabled value="">请选择</option><option>A</option><option>B</option><option>C</option></select><hr><p>下拉列表选择(多选) {{selectedList}}</p><select v-model="selectedList" multiple><option disabled value="">请选择</option><option>A</option><option>B</option><option>C</option></select></div>
</template><script>
export default {data() {return {name: 'Monday',age: 18,intro: '自我介绍',checked: true,checkedNames: [],gender: 'female',selected: '',selectedList: []}}
}
</script>
此时浏览器的显示效果如下:
3、组件
(1)props和$emit(适合父子组件间的通信)
传递形式 | 通信方式 |
---|---|
$emit | 子组件向父组件传递数据 |
props | 父组件的数据需要通过props把数据传给子组件,props的取值可以是数组也可以是对象 |
(2)组件间通信 - 自定义事件
除了父子组件通信外,一般还有兄弟间的组件通信。对于兄弟间的组件通信来说, 兄弟1
可以通过 event.$on
来绑定自定义事件,绑定完成之后,需要再 beforeDestroy
的生命周期里面用 event.$off
及时销毁自定义事件, 防止内存泄漏发生。之后 兄弟2
通过 event.$emit
来调用 兄弟1
的自定义事件。
下面我们结合父子组件通信和兄弟组件通信的知识点,来实现一个添加和删除元素的功能。具体代码如下:
父组件index.vue:
<template><div><!-- 当前使用是父组件,背后的组件是子组件,即input.vue和List.vue是子组件 --><Input @add="addHandler"/><List :list="list" @delete="deleteHandler"/></div>
</template><script>
import Input from './Input'
import List from './List'export default {components: {Input,List},data() {return {list: [{id: 'id-1',title: '标题1'},{id: 'id-2',title: '标题2'}]}},methods: {addHandler(title) {this.list.push({id: `id-${Date.now()}`,title})},deleteHandler(id) {// 过滤出不等于当前删除内容的元素this.list = this.list.filter(item => item.id !== id)}}
}
</script>
子组件List.vue:
<template><div><ul><li v-for="item in list" :key="item.id">{{item.title}}<button @click="deleteItem(item.id)">删除</button></li></ul></div>
</template><script>
import event from './event'export default {// props: ['list']props: {// prop 类型和默认值list: {type: Array,default() {return []}}},data() {return {}},methods: {deleteItem(id) {this.$emit('delete', id)},addTitleHandler(title) {console.log('on add title', title)}},created() {},mounted() {// 绑定自定义事件//event是通过实例化一个vue对象,来调用vue中本身具有的自定义事件能力event.$on('onAddTitle', this.addTitleHandler)}beforeDestroy() {// 及时销毁,否则可能造成内存泄露event.$off('onAddTitle', this.addTitleHandler)}
}
</script>
子组件Input.vue:
<template><div><input type="text" v-model="title"/><button @click="addTitle">add</button></div>
</template><script>
import event from './event'export default {data() {return {title: ''}},methods: {addTitle() {// 调用父组件的事件this.$emit('add', this.title)// 调用自定义事件,调用List.vue中自定义的事件event.$emit('onAddTitle', this.title)this.title = ''}}
}
</script>
event.vue:
import Vue from 'vue'export default new Vue()
此时浏览器的显示效果如下:
(3)组件生命周期
1)组件生命周期(单个组件)
一般来说,组件生命周期的执行顺序为:挂载阶段 → 更新阶段 → 销毁阶段。下面给出常用组件生命周期的解析。
生命周期钩子 | 介绍 |
---|---|
beforeCreate | 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。 |
created | 页面还没有渲染,但是vue的实例已经初始化结束。 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用。 |
mounted | 页面已经渲染完毕。 |
beforeUpdate | 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。 |
updated | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。 |
activated | keep-alive 组件激活时调用。 |
deactivated | keep-alive 组件停用时调用。 |
beforeDestroy | 实例销毁之前调用。在这一步,实例仍然完全可用。常用场景有: 自定义事件的绑定要解除、setTimeout等定时任务需要销毁、自己绑定的window或者document事件需要销毁。 |
destroyed | Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 |
2)组件生命周期(父子组件)
加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
父组件更新过程
父beforeUpdate->父updated
销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
二、vue的高级特性
vue的高级特性主要有以下6种:
- 自定义
v-model
- $nextTick
- slot
- 动态、异步组件
- keep-alive
- mixin
下面对这6种高级特性进行一一讲解。
1、自定义 v-model
在文章的前半部分我们有讲到了 v-model
在表单中的应用,那么接下来我们将动手来实现一个 v-model
。
第一步,我们先定义一个子组件,名字叫 CustomVModel.vue
,具体代码如下:
<template><!-- $emit是子组件往父组件传递数据 --><input type="text":value="text1"@input="$emit('change1', $event.target.value)"><!--1. 上面的 input 使用了 :value 来绑定数据,而不是使用 v-model2. 上面的 change1 和 model.event 要对应起来3. 上面的 text1 与下面props的 text1 属性对应起来-->
</template><script>
export default {model: {prop: 'text1', // 对应下面 props 的 text1event: 'change1'},props: {text1: String,default() {return ''}}
}
</script>
第二步,我们在父组件中使用上面的这个子组件:
<template><div><p>vue 高级特性</p><hr><!-- 自定义 v-model --><p>{{name}}</p><CustomVModel v-model="name"/></div>
</template><script>
import CustomVModel from './CustomVModel'export default {components: {CustomVModel},data() {return {name: 'Monday'}}
}
</script>
通过上面的代码我们可以发现,通过绑定 value
属性和 input
事件这两个语法糖,最终实现数据的双向绑定。
此时我们看下浏览器的显示效果。
通过上图我们自己发现,结果跟实际的 v-model
结果是一样的。至此,我们就实现了自定义的 v-model
,以此来操作数据的双向绑定。
2、$nextTick
(1)知识点
先来了解三个知识点:
Vue
是异步渲染;- 当
data
发生改变之后,DOM
不会立刻进行渲染; $nextTick
会在DOM
渲染之后被触发,以获取最新DOM
节点。
(2)例子展示🌰
假设我们现在要实现一个功能,当我们点击按钮时,打印出列表的项数。这个时候我们大多人可能会这么操作。
<template><div id="app"><!-- ref的设置时为了方便后续可以用来:取节点的DOM元素 --><ul ref="ul1"><li v-for="(item, index) in list" :key="index">{{item}}</li></ul><button @click="addItem">添加一项</button></div>
</template><script>
export default {name: 'app',data() {return {list: ['a', 'b', 'c']}},methods: {addItem() {this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)// 获取 DOM 元素const ulElem = this.$refs.ul1console.log( ulElem.childNodes.length )}}
}
</script>
此时浏览器的显示效果如下。
细心的小伙伴已经发现,浏览器并没有按照我们所想的打印。当页面上的列表显示 6项
内容时,此时控制台只打印 3项
;当显示 9项
时,此时控制台直接只打印 6项
。
那这究竟时为什么呢?
其实,当我们点击的那一刻, data
发生变化,但是 DOM
并不会立刻进行渲染。所以等到我们点击完成的时候,获取的元素还是原来触发的内容,而不会增添上新的内容。
那我们所期望的是,当点击之后立刻触发 DOM
渲染并拿到最新的值。这个时候就需要用到 nextTick
。具体代码如下:
<script>
export default {name: 'app',data() {return {list: ['a', 'b', 'c']}},methods: {addItem() {this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)// 1. 异步渲染,$nextTick 待 DOM 渲染完再回调,// 即NextTick函数会在多次data修改完并且全部DOM渲染完再触发,仅在最后触发一次// 2. 页面渲染时会将 data 的修改做整合this.$nextTick(() => {// 获取 DOM 元素const ulElem = this.$refs.ul1console.log( ulElem.childNodes.length )})}}
}
</script>
我们通过给获取DOM元素的代码外面再嵌套一层 $nextTick
函数,来达到我们想要的效果。在此过程中,当我们点击结束后,data的值发生变化,此时 $nextTick
会等待DOM全部渲染完成之后再进行回调。
最终浏览器的打印效果如下:
3、slot插槽
(1)插槽的作用
让用户可以拓展组件,去更好地复用组件和对其做定制化处理。
(2)三种插槽类型
1)普通slot插槽
slot的基本使用方法是,父组件往子组件中插入一段内容。
我们来演示一下。
假设我们现在有一个子组件,名字叫SlotDemo.vue,它的代码如下。
<template><a :href="url"><slot></slot></a>
</template><script>
export default {props: ['url'], data() {return {}}
}
</script>
我们可能希望这个 <a>
标签内绝大多数情况下都渲染文本 Sunday
,但是有时候却希望渲染文本不是 Sunday
,而是其他内容,那该怎么实现呢?
我们就可以将 Sunday
作为后备内容,这个时候把它放在 <slot>
标签内:
<a :href="url"><slot>Sunday</slot></a>
现在,我定义一个父级组件,名字叫 index.vue
,并且在父级组件中引用上面的 SlotDemo
组件,具体代码如下:
<template><div><p>vue 高级特性</p><hr><SlotDemo :url="website.url"></SlotDemo></div>
</template><script>
import SlotDemo from './SlotDemo'export default {components: {SlotDemo},data() {return {name: 'Monday',website: {url: 'https://blog.csdn.net/weixin_44803753',title: 'Monday',subTitle: '穿梭于前端开发的学习永动机'}}}
}
</script>
这个时候后备内容 Sunday
就会被渲染出来。此时浏览器显示效果如下:
那么如果想要把这个后备内容渲染成我们想要的内容,而不是 Sunday
呢,这个时候我们应该这么做:
<SlotDemo :url="website.url">{{website.title}}
</SlotDemo>
我们可以通过在父组件 index.vue
中引用的子组件 SlotDemo.vue
里面,插入我们想要的内容。这个时候子组件 SlotDemo.vue
的后备内容 Sunday
就会渲染成我们提供的新内容,以此来达到我们想要的目的。
此时浏览器的打印效果如下:
2)作用域插槽slot-scope
vue官方的说法给到的解释是:
- 父组件模板的所有东西都会在父级作用域内编译;
- 子组件模板的所有东西都会在子级作用域内编译。
这样看起来好像有点难懂,接下来我们用一个例子来了解作用域插槽想要解决的问题究竟是什么!
首先,我们先定义一个子组件,名字叫 ScopedSlotDemo.vue
,具体代码如下所示:
<template><a><slot>{{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 --></slot></a>
</template><script>
export default {props: ['url'],data() {return {website: {url: 'http://tinymce.ax-z.cn/',title: 'tinymce',subTitle: '一款易用、且功能强大的富文本编辑器'}}}
}
</script>
通过以上代码可以分析,这个时候我们想要把 slot
里面想要呈现的内容绑定下方的data数据,于是通过 {{website.subTitle}}
引用。
接下来我们定义一个父组件,名字叫 index.vue
,父组件代码如下:
<template><div><p>vue 高级特性</p><hr><ScopedSlotDemo>{{website.title}}</ScopedSlotDemo></div>
</template>
<script>
import ScopedSlotDemo from './ScopedSlotDemo'export default {components: {ScopedSlotDemo},data() {return {name: 'Monday'}}
}
</script>
我们希望父组件不显示子组件的后备内容 subTitle
,而是显示子组件的另外一个 data
属性 ➡ title
,于是我们按照默认绑定的操作,直接在父组件进行 {{website.title}}
的绑定。此时我们运行一下:
阿欧!这是什么东西呢?为什么会报错呢?
这就谈到了我们开头说的定义了。在vue当中,父组件模板的所有东西只会在父级作用域内编译;子组件模板的所有东西只会在子级作用域内编译。
所以,我们在父组件当中引用子组件的内容,当然是要报错的。这就相当于,一个父亲称呼他的儿子为爸爸一样,怎么可能嘛对吧!
在vue中,父级组件只能访问父级的数据,而子级组件也只能访问子级的数据,不能跃级访问。
因此,有了这个问题的出现,我们引用了作用域插槽来解决这个问题。怎么做呢?来看下面内容。
我们需要在父级作用域 index.vue
中为我们要绑定的元素设置一个 v-slot
,这个 v-slot
用来定义我们提供的插槽 prop
的名字。
index.vue:
<ScopedSlotDemo> <template v-slot="slotProps"> {{slotProps.slotData.title}} </template>
</ScopedSlotDemo>
那么父级定义完只是第一步,接下来我们要让子级 ScopedSlotDemo.vue
的插槽内容在父级当中可用,于是我们需要在子级的插槽中绑定一个自定义事件,并且指向我们需要的数据,如下代码所示:
ScopedSlotDemo.vue:
<slot v-bind:slotData="website"> {{website.subTitle}}
</slot>
从上面我们可以看到,我们通过绑定一个自定义事件 v-bind:slotData
,并把它指向我们想要的数据 website
。指向之后,我们在父级的插槽当中,通过 slopProps.slotData
,就可以访问到自己子级 website
的数据,这样,就达到最终我们想要的效果。
最终浏览器访问结果:
最后贴上完整的父子级代码,大家可以根据以上讲解进行演示。
子级 ScopedSlotDemo.vue:
<template><a><slot>{{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 --></slot></a>
</template><script>
export default {props: ['url'],data() {return {website: {url: 'http://tinymce.ax-z.cn/',title: 'tinymce',subTitle: '一款易用、且功能强大的富文本编辑器'}}}
}
</script>
父级 index.vue:
<template><div><p>vue 高级特性</p><hr><ScopedSlotDemo><template v-slot="slotProps">{{slotProps.slotData.title}}</template></ScopedSlotDemo></div>
</template><script>
import ScopedSlotDemo from './ScopedSlotDemo'export default {components: {ScopedSlotDemo},data() {return {name: 'Monday',website: {url: 'https://blog.csdn.net/weixin_44803753',title: 'Monday',subTitle: '穿梭于前端开发的学习永动机'}}}
}
</script>
3)具名插槽
具名插槽的定义: 将父组件中的内容插入指定的子组件位置中。
接下来来看一下具名插槽是怎么使用的?
有时候我们一个组件里需要多个插槽。比如,现在我们有一个子组件,名字叫 named.vue
,具体代码如下:
<template><div><header><!-- 我们希望把标题放这里 --></header><main><!-- 我们希望把主体内容放这里 --></main><footer><!-- 我们希望把页脚放这里 --></footer></div>
</template>
大家可以看到,上面的代码中,我们希望在 header
, main
和 footer
当中放入对应模块的内容进去,这个时候就需要使用多个插槽来解决。
那怎么处理呢? 对于这样的情况, <slot>
元素有一个特殊的属性: name
,这个属性可以用来定义额外的插槽。我们通过使用 name
属性来对子组件 named.vue
的插槽加上属性名。具体做法如下:
<template><div><header><slot name="header"></slot></header><main><!-- 如果一个<slot>不带name属性的话,那么它的name默认为default --><slot></slot></main><footer><slot name="footer"></slot></footer></div>
</template>
值得注意的是,如果一个 <slot>
不带 name
属性的话,那么它的 name
默认为 default
。
子级组件插入完插槽之后,接下来我们来定义一个父组件 index.vue
。在向子级组件的具名插槽提供内容的时候,我们可以在父级组件的 <template>
元素上使用 v-slot
指令,并以参数的形式传递具名插槽的名称。
index.vue:
<template><div><template v-slot:header><h1>这里是页面标题</h1></template><p>页面主题内容第一页</p><p>页面主题内容第二页</p><template v-slot:footer><p>这里是页脚</p></template></div>
</template>
现在 <template>
元素中的所有内容都将会被传入到子组件相应的插槽当中,且任何没有被包裹在带有 v-slot
的 <template>
中的内容都会被视为默认插槽的内容。
如果你希望更加明确一点的话,那就把主体内容包裹上一个插槽,并设置 name="default"
。
<template v-slot:default><p>页面主题内容第一页</p><p>页面主题内容第二页</p>
</template>
这样看起来或许会更优雅一点。
最终打印结果如下:
这里是页面标题页面主题内容第一页
页面主题内容第二页这里是页脚
讲完具名插槽,我们来对它的使用方法进行一个总结归纳。
具名插槽的使用语法:
- 子组件定义
slot
时,在标签上加上name='xxx'
属性,不写name
则默认name=default
。 - 父组件将想插入的部分最外部的
div
上加上v-slot="xxx"
属性,xxx
对应name的值。 v-slot:
可以简写为#
,如v-slot:header
相当于#header
。
都说一个萝卜一个坑,具名插槽通过将父组件中的内容一个一个对应地插入到指定的子组件位置当中,把坑填上了,萝卜也就长出来了。
4、动态、异步组件
(1)动态组件
1)举个例子🌰
比如我们在做某个新闻网站的文章详情页,那这个时候文章详情页显示的内容是非常多样化的。有可能是文本、视频和图片类型,也有可能是文本和图片类型,这个时候我么就没有办法确定这个组件想要的是什么类型的内容,于是我们据需要用到动态组件。
2)动态组件的用法和应用场景
- 用法:
:is = "component-name"
用法; - 应用场景: 需要根据 数据 进行动态渲染的场景,即组件类型不确定的场景。
3)演示
演示1:
我们定义一个子组件,名字叫 Dynamic.vue
,具体代码如下:
<template><div><p>动态组件子组件</p></div>
</template><script>export default {data() {reuturn{}}
}
</script>
接下来定义一个父组件,名字叫 index.vue
。我们希望地引入子组件 Dynamic.vue
。因此,我们可以这样做:
<template><div><p>vue 高级特性</p><hr><!-- 动态组件 --><component :is="DynamicName"/></div>
</template><script>
import Dynamic from './Dynamic'
export default {components: {Dynamic},data() {return {DynamicName: "Dynamic"}}
}
</script>
这个时候我们可以发现,通过在 data
中绑定组件的名字,之后在视图上用 is="ComponentName"
的形式进行引用,达到对组件进行动态绑定的效果。
演示2:
假设我们要给某一个页面上的一组数据进行动态绑定,这个时候我们可以这么做:
<template><div><!-- 动态组件 --><div v-for="(val, key) in newsData" :key = "key"><component :is = "val.type"/></div></div>
</template><script>export default {data() {return {newsData:{1:{type:'text'},2:{type:'text'},3:{type:'image'}}}}
}
</script>
(2)异步组件
平常我们在开发中,通常用的是 import()函数
来引入一个组件,但是import虽然好用,可是大家有没有想过,如果当我们要引入一个比较大的组件时,也用 import
去引入的话,它会进行同步加载操作,这样子对性能优化和用户体验来说是非常不友好的。
因此,我们要引入异步组件来解决 import
同步加载的问题。
我们来举个例子:
<template><div><!-- 异步组件 --><FormDemo v-if="showFormDemo"/><button @click="showFormDemo = true">show form demo</button></div>
</template><script>
export default {components: {FormDemo: () => import('./FormDemo')},data() {return {showFormDemo: false}}
}
</script>
通过上面的代码,我们可以了解到,通过控制 showFormDemo
的布尔值,来决定 FormDemo
组件在什么时候引入。因此,在button点击之前, FormDemo
的判断条件 showFormDemo
为 false
,这个时候组件不会被加载。只有当 button
点击之后, FormDemo
的判断条件 showFormDemo
变为true,这个时候才按需将组件的内容加载出来。
通过以上分析,大家应该可以了解到, import
是同步加载,不管页面需不需要这个组件,都会在页面加载时就加载出来。
而如果将组件直接注册在 components
中,通过布尔值数据来控制组件是否加载,而不会从一开始就加载,这样的方式来加载组件,从性能上会好非常多。
因此,我们对异步组件做出以下总结:
- 不适合用
import()
函数; - 按需加载,异步方式加载大组件;
- 异步加载,即什么时候用什么时候加载,什么时候不用,就永远不会加载。
5、keep-alive
(1)定义和应用场景
keep-alive
,从字面意思来讲就是,保持它存活着,保持它不死亡。
我们先抛出 keep-alive
的定义和应用场景:
keep-alive
是一个缓存组件;- 应用于频繁切换的场景,用
keep-alive
就不需要重复渲染; keep-alive
是vue
中常见的性能优化方法。
(2)举例🌰
了解完定义和应用场景,我们来开始举例子看看这个缓存组件,到底是何方神圣,让人们在开发时都对之有所热爱。
首先,我们先来定义一个父组件,名字叫 keepAlive.vue
,具体代码如下:
keepAlive.vue:
<template><div><button @click="changeState('A')">button A</button><button @click="changeState('B')">button B</button><button @click="changeState('C')">button C</button><KeepAliveStageA v-if="state === 'A'"/> <KeepAliveStageB v-if="state === 'B'"/><KeepAliveStageC v-if="state === 'C'"/></div>
</template><script>
import KeepAliveStageA from './KeepAliveStateA'
import KeepAliveStageB from './KeepAliveStateB'
import KeepAliveStageC from './KeepAliveStateC'export default {components: {KeepAliveStageA,KeepAliveStageB,KeepAliveStageC},data() {return {state: 'A'}},methods: {changeState(state) {this.state = state}}
}
</script>
我们再来定义三个子组件,名字分别为 keepAliveStateA.vue
、 keepAliveStateB.vue
、 keepAliveStateC.vue
。 具体代码如下:
keepAliveStateA.vue:
<template><p>state A</p>
</template><script>
export default {mounted() {console.log('A mounted')},destroyed() {console.log('A destroyed')}
}
</script>
keepAliveStateB.vue:
<template><p>state B</p>
</template><script>
export default {mounted() {console.log('B mounted')},destroyed() {console.log('B destroyed')}
}
</script>
keepAliveStateC.vue:
<template><p>state C</p>
</template><script>
export default {mounted() {console.log('C mounted')},destroyed() {console.log('C destroyed')}
}
</script>
此时我们来看下浏览器的运行效果:
通过以上动图可以发现,我们初始化时是状态 A
,之后当我们每次点击按钮时,编译器都会对这个组件进行销毁,再创建我们点击的新组件。这样反复的创建销毁,创建销毁,对用户的体验似乎是不太好的。因为用户每点击一个东西就得重新下载,如果遇到代码量很多的页面呢?那用户可能会从此把这个网站拉入黑名单了。
因此,我们可以使用vue中的 keep-alive
,来解决这个问题。我们想要达到的目的很简单,就是把用户每次点击后的内容给缓存下来,之后用户每次点击时就可以直接从缓存中拉取资源直接显示,而无需重新加载。
那么,怎么做呢?
很简单,我们只需要在父组件 keepAlive.vue
中引入的三个子组件外面,再包上一个缓存组件 keep-alive
, 这样,就能达到我们想要的目的了。
<keep-alive> <KeepAliveStageA v-if="state === 'A'"/><KeepAliveStageB v-if="state === 'B'"/><KeepAliveStageC v-if="state === 'C'"/>
</keep-alive>
我们来看下此时浏览器的打印效果:
我们可以发现,有keep-alive时,浏览器直接把子组件的内容缓存下来了,当我们再次点击它时,就不会再进行重复的创建和销毁,直接从缓存中拉取资源,并加载到我们的页面中来。
6、mixin
(1)mixin是什么
我们先来了解下 mixin
的定义:
- 多个组件有相同的逻辑,抽离出来,当作
mixin
来使用。 mixin
并不是完美的解决方案,会存在一些小问题的出现。Vue 3
提出的Composition API
旨在解决这些问题。这里不对Vue 3
的Composition API
进行细讲,将在后面的文章中做介绍。
(2)mixin的问题
mixin虽说可以用来抽离相同逻辑的组件,但是总会存在各种各样的问题。比如:
- 变量来源不明确,不利于阅读。
- 多个
mixin
可能会造成命名冲突。 mixin
和组件可能出现多对多的关系,复杂度较高。
(3)举个栗子🌰
我们先来定义一个组件,名字叫 MixinDemo.vue
,具体代码如下:
<template><div><p>{{name}}: {{intro}}: {{city}}</p><button @click="showName">显示姓名</button></div>
</template><script>
import myMixin from './mixin'export default {mixins: [myMixin], // 可以添加多个,会自动合并起来data() {return {name: 'Monday',intro: '热爱前端的学习永动机'}},methods: {},mounted() {console.log('component mounted', this.name)}
}
</script>
再定义一个 js
文件,名字叫 mixin.js
,具体代码如下:
export default {data() {return {city: '广东'}},methods: {showName() {console.log(this.name)}},mounted() {console.log('mixin mounted', this.name)}
}
此时浏览器的演示效果如下:
我们来解析下上面这波操作。假设此时 js
文件的代码是多个组件中抽离出来的比较相似的代码部分,那我们我们对它进行抽离后并注册到 MixinDemo.vue
当中,用 mixins: [myMixin]
的方式进行引入。此时,我们在 MixinDemo.vue
组件中就可以访问到 mixin.js
里面的内容,这样就完成了一波逻辑复用。
mixin看着是挺好用,但是它会存在各种各样的问题发生。这就谈到我们第二点中所说的。
我们来对问题进行一一剖析。
第一个,变量来源不明确,不利于阅读。 比如我们的第一块代码,里面有一个按钮绑定了showName的方法,此时showName的方法放在了mixin里面,而没有放在该页面中。假如我们定义了多个mixin,那我们得对每个mixin一个一个地去找showName这个方法,因为我们也不知道showName来自于哪里,只能这样干。那这样子的话得有多麻烦,非常的影响开发效率。
第二个,多个 mixin 可能会造成命名冲突。 比如说当出现多个 mixin
时,里面的 data
绑定的属性一模一样,那这个时候就有可能出现覆盖的情况,也很不利于我们找错。
第三个, mixin 和组件可能出现多对多的关系,复杂度较高。 我们在开发过程中,有可能会遇到一个组件引入多个 mixin
,也有可能多个组件引入一个 mixin
,这样就是一个多对多的关系。这有一天如果某个组件想要改个属性值,但是它只能动 mixin
。这一动,其他组件就都被影响到了,很容易导致了剪不断理还乱的尴尬局面。
所以, mixin
有它的好处所在,也有它的问题所在。而 vue3
中的 composition API
就旨在解决这个问题。这里先给大家抛个概念, composition API
将在后面的文章中进行讲解。
三、vue的周边插件:vuex和vue-router
1、vuex
(1)vuex基本概念
- state:单一状态树,用一个对象来包含全部的应用层级状态;
- getters:
vuex
允许我们在store
中定义getter
,就像计算属性一样,getter
的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生改变时,才会被重新计算。 - action:异步逻辑封装;
- mutation:更改状态的唯一方法,并且这个过程是同步的。
(2)用于vue组件中的API
- dispatch
- commit
- mapState
- mapGetters
- mapActions
- mapMutations
(3)图例
对于 vuex
来说,先理解上面这几个概念,后面还会有文章讲解关于 vuex
的实际应用。
2、vue-router
(1)路由模式
1)hash 模式(默认)
hash模式,如 http://abc.com/#/user/10
。早期的前端路由就是基于 location.hash
来实现的。其实原理很简单, location.hash 的值就是 url
中 #
后面的内容。比如下面这个网站,它的 location.hash
就是 #abc
。
https://www.baidu.com#abc
2)H5 history模式
history
模式,如 http://abc.com/user/20
;与 hash
模式不同的是,这种模式需要 server
端支持,因此无特殊需求可选择前者。
HTML5
提供了 History API
来实现 URL
的变化。其中最主要的 API
有以下两个。 history.pushState()
和 history.replaceState()
。这两个 API
可以在不刷新的情况下,操作浏览器的历史记录。唯一不用的是,前者是新增一个记录,后者是直接替换当前的历史记录。如下所示:
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
同时,对于 history
来说,如果刷新时,服务器没有响应的资源,会报错 404
。
(2)路由配置
1)动态路由
动态路径参数,以 冒号 :
开头进行路由配置。如下所示:
const User = {//获取参数如 3 5template: '<div>User {{$route.params.id}}</div>'
}const router = new VueRouter({routes:[//动态路径参数 以冒号开头。能命中`/user/3` `/user/5` 等格式的路由{path: 'user/:id', component: User}]
})
2)懒加载
懒加载即对应一个 import()
函数,然后去导入一个组件。跟我们前面主题二的第4小点讲过的异步组件类似,当我们去访问一级二级等各种级别的的路由时,就会异步的去把对应的组件加载出来。不会出现一下子全部同时加载的问题,而是该加载时再加载,可以理解为我需要你的时候你再出现就好了。示例代码如下所示:
export default new VueRouter({routes:[{path:'/',component: () => import('./../compoents/Navigator')},{path:'/detail',component: () => import('./../components/Detail')}]
})
四、结束语
以上文章总结了 vue
的基本使用以及 vue
的高级特性,还有概括了 vue
的周边插件 vuex
和 vue-router
的一些常见知识,融合大量案例进行讲解。
前端在做 vue
的项目中,总是脱离不开以上文章所涉及到的内容,唯一的区别在于用的多和用的少的问题。
希望通过以上文章的讲解,小伙伴们能有所收获🥂
- 关注公众号 星期一研究室 ,不定期分享学习干货,学习路上不迷路~
- 如果这篇文章对你有用,记得点个赞加个关注再走哦~