[Study]Vue
文章目录
- 一、基础语法
- 1.1 概述
- 1.2 入门案例
- 1.3 el 与 data
- 1.4 MVVM
- 1.5 数据代理
- 1.6 事件处理
- 1.7 计算属性
- 1.8 监视属性
- 1.9 绑定样式
- 1.10 常用指令
- 1.11 自定义指令
- 1.12 列表数据处理
- 1.13 表单数据收集
- 1.14 过滤器
- 1.15 :key 作用与原理
- 1.16 数据监测原理
- 1.17 生命周期
- 二、组件化
- 2.1 概述
- 2.2 组件的定义和使用
- 2.3 VueComponent
- 三、脚手架
- 3.1 开发环境配置
- 3.2 render 函数
- 3.3 ref 引用
- 3.4 props 配置(组件通信)
- 3.5 mixin 混入
- 3.6 plugin 插件
- 3.7 localStorage
- 3.8 组件自定义事件(组件通信)
- 3.9 全局事件总线(组件通信)
- 3.10 消息订阅与发布(组件通信)
- 3.11 $nextTick
- 3.12 动画与过渡
- 3.13 TodoList 案例
- 3.14 配置代理
- 3.15 GitHub 案例
- 3.16 slot 插槽(组件通信)
- 四、vuex 插件
- 4.1 概述(组件通信)
- 4.2 搭建 Vuex 环境
- 4.3 求和案例
- 4.4 vuex 中的四种 map
- 4.5 vuex 模块化
- 五、vue-router 插件
- 5.1 概述
- 5.2 路由基本使用
- 5.3 嵌套与命名路由
- 5.4 路由参数
- 5.5 编程式路由导航
- 5.6 缓存路由组件
- 5.7 路由生命周期钩子
- 5.8 路由守卫
- 5.9 history 与 hash
- 六、Vue3
一、基础语法
1.1 概述
- Vue:一套用于构建用户界面的渐进式 JavaScript 框架。何为渐进式?是指 Vue 可以自底向上逐层地应用,也即按需引入 Vue 的各种组件
- Vue 的发展历程:2013(0.6.0)、2014(0.8.0)、2015(1.0.0)、2016(2.0.0)、2020(3.0.0)
- Vue 的特点:
- 采用组件化模式,提高代码复用率,易于维护
- 声明式编码,无需直接操作 DOM,提高开发效率
- 使用虚拟 DOM + 优秀的
Diff
算法,尽量复用 DOM 节点
1.2 入门案例
- 容器与 Vue 实例的关系:容器与 Vue 实例是一对一的关系,真实开发环境中只有一个 Vue 实例,并且会配合着组件一起使用
- 插值语法
{{}}
:用于解析标签体内容,{{xxx}} 插值语法中 xxx 要写 js 表达式,且 xxx 可以自动读取到data
中的所有属性,一旦 data 中的数据发生改变,那么页面中用到该数据的地方都会自动解析并更新 - 指令语法:用于解析标签属性,如
v-bind
等 - 数据绑定:
- 单向数据绑定(
v-bind
):v-bind:
可简写为:
- 双向数据绑定(
v-model
):只能用于表单类元素,v-model:value
可以简写为v-model
,因为 v-model 默认收集的就是表单元素的 value 值
- 单向数据绑定(
<!DOCTYPE html>
<html lang="zh-cn">
<head><meta charset="UTF-8"><title>Vue 入门案例</title><script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root"><!-- 插值语法 --><h1>Hello, {{ name.toUpperCase() }}</h1><!-- 指令语法 --><a v-bind:href="url">百度一下</a><hr/><!-- 数据绑定 -->单向数据绑定(data -> 页面):<input type="text" v-bind:value="name"/><br/>双向数据绑定(data <-> 页面):<input type="text" v-model:value="name">
</div><script type="text/javascript">// 阻止 vue 在启动时生成生产提示Vue.config.productionTip = falsenew Vue({// el 即 element 指明当前 vue 实例为哪个容器服务el: '#root',// data 用于存储数据,供 el 指定的容器使用data: {name: 'Spring-_-Bear',url: ''}})
</script>
</body>
</html>
1.3 el 与 data
-
el 的两种写法:
// el 的第一种写法 new Vue({el: '#root' })// el 的第二种写法 const vm = new Vue({}) vm.$mount('#root')
-
data 的两种写法:
-
对象式:
data: {name: 'Spring-_-Bear' }
-
函数式:由 Vue 管理的函数不能写为箭头函数,否则 this 指向存在问题
// 函数式完整写法 data: function () {return {name: 'Spring-_-Bear'} } // 函数式简写写法 data() {return {name: 'Spring-_-Bear'} }
-
1.4 MVVM
-
MVVM:
- M(Model):模型,对应 data 数据
- V(View):视图,对应模板代码
- VM(ViewModel):视图模型,对应 Vue 实例对象
-
vm:data 中所有的属性最终都出现在了视图模型 vm 身上,vm 身上所有的属性以及 Vue 原型上的所有属性在模板中都可以直接使用
<div id="root"><h1>{{ name }}</h1><!-- vm 的 _data 属性,对应数据 data --><h1>{{ _data }}</h1><!-- Vue 的原型属性 $delete --><h1>{{ $delete }}</h1> </div><script type="text/javascript">Vue.config.productionTip = falseconst vm = new Vue({el: '#root',data: {name: 'Spring-_-Bear'}})console.log(vm) </script>
1.5 数据代理
-
Object.defineProperty
即为实现数据代理的基本原理:<script type="text/javascript">let number = 18;let person = {name: 'Spring-_-Bear',sex: '男'}Object.defineProperty(person, 'age', {// value: 18, // 设置 person 对象的 age 属性值为 18// enumerable: true, // 控制属性是否可以被遍历,默认 false// writable: true, // 控制属性是否可以被修改,默认 false// configurable: true, // 控制属性是否可以被删除,默认 false// 当读取 person.age 时,get 函数自动调用,且返回值就是 person.age 的值get: function () {return number;},// 当修改 person.age 时,set 函数自动调用,且会收到具体修改的值set(val) {number = val;}}) </script>
-
数据代理:通过一个对象代理对另一个对象的属性操作
<script type="text/javascript">let obj1 = {age: 18};let obj2 = {};// obj2 代理 obj1 的属性操作Object.defineProperty(obj2, 'age', {get() {return obj1.age;},set(val) {obj1.age = val;}}) </script>
-
Vue 数据代理的基本原理:通过 vm 对象来代理 data 对象中属性的操作,如此可以更加方便地操作 data 中的数据
- 复制 data:将 data 对象中的属性复制到
vm._data
对象中 - _data 到 vm:通过
Object.defineProperty()
把vm._data
对象中所有属性添加到 vm 上 - getter/setter:为每一个添加到 vm 上的属性都指定一个
getter/setter
,在getter/setter
方法内部操作 data 对应的属性
- 复制 data:将 data 对象中的属性复制到
1.6 事件处理
-
事件绑定:使用
v-on:
或@
简写形式绑定对应的事件:<div id="root"><!-- v-on:click:不传递参数 --><button v-on:click="clickMe">点我呀(不传参)</button><!-- @click:传递参数 --><button @click="sayHello($event, 'Hello World')">说你好(传递参数)</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',/** 受 Vue 管理的函数不要写成箭头函数,否则 this 对象为 Window 而非 vm*/methods: {clickMe(event) {// 事件对象即 button 按钮元素console.log(event.target.innerText)},sayHello(event, content) {// this -> vmconsole.log(this)alert(content)}}}) </script>
-
事件修饰符:
-
prevent:阻止事件的默认行为
<a @click.prevent="sayHello" href="">走,百度两下</a>
-
stop:阻止事件冒泡
<div @click="sayHello"><!-- 先阻止事件冒泡,后阻止事件默认行为 --><button @click.stop.prevent="sayHello">点我呀</button> </div>
-
once:事件只触发一次
<button @click.once="sayHello">点击只生效一次哦</button>
-
capture:使用事件的捕获行为
<div @click.capture="sayHello('div1')" style="width: 100px; height: 100px; background: red">div1<!-- 点击 div2 时总是先输出 div1 再输出 div2 --><div @click="sayHello('div2')" style="width: 50px; height: 50px; background: skyblue">div2</div> </div>
-
self:只有
event.target
是当前操作的元素才触发事件<div @click.self="sayHello" style="height: 500px; width: 500px; background: red"><!-- 点击 button 时 div 的 click 事件不会响应 --><button @click="sayHello">点我呀</button> </div>
-
passive:事件的默认行为立即执行,无需等待事件回调执行完毕
<div id="root"><!-- 滚动条滑动事件 --><ul @scroll="msg" style="height: 200px; width: 200px; background: red; overflow: auto"><li style="height: 100px">1</li><li style="height: 100px">2</li><li style="height: 100px">3</li><li style="height: 100px">4</li><li style="height: 100px">5</li></ul><!-- 鼠标滚轮滚动事件,加上 passive 事件修饰符后立即滑动滑动条,无需等待 msg 回调执行完成 --><ul @wheel.passive="msg" style="height: 200px; width: 200px; background: red; overflow: auto"><li style="height: 100px">1</li><li style="height: 100px">2</li><li style="height: 100px">3</li><li style="height: 100px">4</li><li style="height: 100px">5</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',methods: {msg() {for (let i = 0; i < 100000; i++) {console.log('bear')}}}}) </script>
-
-
键盘事件:
-
Vue 中提供的按键别名,可直接用于绑定按键事件:
enter、delete、esc、space、tab、up、down、left、right
<div id="root">enter:<input type="text" @keyup.enter="show"/><br/><!-- Tab 键的功能是从当前元素失焦,较为特殊,需绑定 @keydown 按下事件 -->tab:<input type="text" @keydown.tab="show" placeholder="绑定 @keydown 事件"> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',methods: {show(e) {console.log(e.key, e.keyCode)}}}) </script>
-
Vue 未提供别名的按键,可以使用按键原始名称绑定,但多英文时注意要转为英文小写加短横线连接方式如
caps-lock
大小写切换提示:<input type="text" @keyup.caps-lock="show"/>
-
系统修饰键用法特殊:
ctrl、alt、shift、meta
(即 win 键)-
配置
@keydown
使用时:正常触发事件 -
配合
@keyup
使用时:按下修饰键的同时按下其它键,释放其它键时事件才被触发Ctrl + Y 释放 Y 时触发事件:<input type="text" @keyup.ctrl="show">
-
-
1.7 计算属性
-
姓名案例使用函数方式实现:
<div id="root">姓:<input type="text" v-model="firstName"><br>名:<input type="text" v-model="lastName"><br>全名:<span>{{ fullName() }}</span> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {firstName: '张',lastName: '三'},methods: {fullName() {return this.firstName + '-' + this.lastName}}}) </script>
-
姓名案例使用计算属性实现:
computed
计算属性与methods
方法实现相比有读取缓存机制,效率更高、调试方便<div id="root">姓:<input type="text" v-model="firstName"><br>名:<input type="text" v-model="lastName"><br>全名:<span>{{ fullName }}</span> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {firstName: '张',lastName: '三'},computed: {fullName: {/** get() 调用时机:* 1. 初次读取 fullName 属性时* 2. fullName 所依赖的数据发生变化时*/get() {return this.firstName + '-' + this.lastName;},/** set(val) 调用时机:fullName 被修改时*/set(val) {let arr = val.split('-');this.firstName = arr[0];this.lastName = arr[1];}}}}) </script>
-
姓名案例计算属性的简写形式:
computed: {// 当计算属性只读不改时可使用以下简写方式fullName() {return this.firstName + '-' + this.lastName;} }
1.8 监视属性
-
监视属性的第一种配置方式
watch
:<div id="root"><h1>今日天气:{{ weather }}</h1><button @click="isHot = !isHot">变天咯</button> </div><script type="text/javascript">Vue.config.productionTip = falseconst vm = new Vue({el: '#root',data: {isHot: true},computed: {weather() {return this.isHot ? '炎热' : '凉爽';}},watch: {// 监视数据isHot: {// immediate: true 初始化完成立即监视一次 isHot 的变化immediate: true,handler(newValue, oldValue) {console.log(newValue, oldValue)}},// 监视计算属性weather: {handler(newValue, oldValue) {console.log(newValue, oldValue)}}}}) </script>
-
监视属性的第二种配置方式
vm.$watch()
:// 第二种监视方式 vm.$watch('isHot', {handler(newValue, oldValue) {console.log(newValue, oldValue)} })
-
监视属性的简写形式:当监视的属性不需要其它配置项时可简写 handler 函数
watch: {isHot(newValue, oldValue) {console.log(newValue, oldValue)} }
-
监视属性的深度监视配置:
<div id="root"><h1>a = {{ numbers.a }}</h1><button @click="numbers.a++">a++</button><h1>d = {{ numbers.b.c.d }}</h1><button @click="numbers.b.c.d++">d++</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {numbers: {a: 1,b: {c: {d: 1}}}},watch: {// 监视多级数据结构中某个属性的变化'numbers.a': {handler(newVal, oldVal) {console.log('a: ' + oldVal + ' -> ' + newVal)}},// 监视多级数据结构中所有属性的变化numbers: {// 开启深度监视deep: true,handler() {console.log('numbers 发生了变化')}}}}) </script>
-
监视属性对比计算属性:computed 能完成的功能 watch 都可以完成,watch 能完成的功能 computed 不一定能完成,如 watch 可以进行异步操作等
<div id="root">姓:<input type="text" v-model="firstName"><br>名:<input type="text" v-model="lastName"><br>全名:<span>{{ fullName }}</span> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {firstName: '张',lastName: '三',fullName: '张-三'},watch: {firstName(newVal) {this.fullName = newVal + '-' + this.lastName;},lastName(newVal) {/** 所有被 Vue 管理的函数最好写成普通函数,这样 this 的指向才是 vm 或组件实例对象* 所有不被 Vue 管理的函数(定时器的回调函数、ajax 的回调函数、Promise 的回调函数等)最好写成箭头函数,这样 this 的指向才是 vm 或组件实例对象*/setTimeout(() => {this.fullName = this.firstName + '-' + newVal;}, 3000);}}}) </script>
1.9 绑定样式
-
绑定 class 样式之字符串写法:适用于样式的类名不确定,需要动态绑定
<div id="root"><!-- 绑定 class 样式:字符串写法 --><div class="basic" :class="changeMood">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',mood: 'happy',},methods: {changeMood() {const arr = ['happy', 'sad', 'normal'];this.mood = arr[Math.floor(Math.random() * 3)]}}}) </script>
-
绑定 class 样式之数组写法:适用于要绑定的样式个数不确定,类名也不确定
<div id="root"><!-- 绑定 class 样式:数组写法 --><div class="basic" :class="classArr">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',mood: 'happy',classArr: ['sad', 'happy', 'funny']}}) </script>
-
绑定 class 样式之对象写法:适用于要绑定的样式个数确定、类名也确
<div id="root"><!-- 绑定 class 样式:对象写法 --><div class="basic" :class="classObj">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',mood: 'happy',classObj: {sad: true,happy: false}}}) </script>
-
绑定 style 样式之对象写法:
<div id="root"><!-- 绑定 style 样式:对象写法 --><div style="padding: 10px" :style="styleObj">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',styleObj: { fontSize: '40px' }},}) </script>
-
绑定 style 样式之数组对象写法:
<div id="root"><!-- 绑定 style 样式:数组对象写法 --><div style="padding: 10px" :style="styleArrObj">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',styleArrObj: [{fontSize: '40px'},{backgroundColor: 'red'}]},}) </script>
1.10 常用指令
-
条件渲染指令:
v-show
:控制元素的display
属性从而实现元素展示与隐藏v-if
:从document
中增、删对应的 DOM 元素
<div id="root"><h1>n = {{ n }}</h1><button @click="n++">n++</button><!-- v-show:display none --><div v-show="n === 3">n == 3</div><!-- v-if:增、删节点 --><div v-if="n < 1">n < 1</div><div v-else-if="n >= 1 && n < 5">1 <= n < 5</div><div v-else>n >= 5</div><!-- template 只能与 v-if 配合使用 --><template v-if="n === 1"><h1>北京</h1><h1>欢迎</h1><h1>您</h1></template> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {n: 1}}) </script>
-
列表渲染指令
v-for
:<div id="root"><h1>v-for 遍历数组</h1><ul><!-- in 也可以换为 of --><li v-for="(item,index) in persons" :key="item.id">{{ index }}-{{ item.name }}-{{ item.age }}</li></ul><h1>v-for 遍历对象</h1><ul><li v-for="(val,key,index) in car" :key="index">{{ key }} : {{ val }}</li></ul><h1>v-for 遍历数字</h1><ul><li v-for="(val,index) in 5" :key="index">{{ index }} : {{ val }}</li></ul><h1>v-for 遍历字符串</h1><ul><li v-for="(val,index) in 'HelloWorld'" :key="index">{{ index }} : {{ val }}</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {persons: [{id: '001', name: '张三', age: 3},{id: '002', name: '李四', age: 4},{id: '003', name: '王五', age: 5},],car: {name: '奥迪',color: '红色',price: '50w'}}}) </script>
-
元素文本指令
v-text
:<div id="root"><!-- 以下两种方式等价,均将 name 解析为 div 中的文本值 --><div>{{name}}</div><div v-text="name"></div> </div>
-
内置 html 结构指令
v-html
:v-html 存在安全性问题,在网站上动态渲染任意 HTML 都是非常危险的行为,容易导致 XSS 攻击<div id="root"><div v-text="'<h1>Hello World!</h1>'"></div><div v-html="'<h1>Hello World!</h1>'"></div><div v-html="str"></div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {// XSS 攻击示例str: '<a href=javascript:location.href="?"+document.cookie>兄弟快来,这儿有好东西!</a>'},}) </script>
-
元素渲染一次指令
v-once
:v-once 修饰的节点在初次动态渲染完成后就视为静态内容,不再发生变化,可以用于性能优化<div id="root"><!-- 第一次读取 n 值后不再发生变化 --><div v-once>初始值 n = {{ n }}</div><div>现在值 n = {{ n }}</div><button @click="n++">n++</button> </div>
-
免编译指令
v-pre
:v-pre 指令修饰的节点 Vue 不进行编译,可加快页面加载速度<div id="root"><div v-pre>初始值 n = {{ n }}</div><div>现在值 n = {{ n }}</div><button @click="n++">n++</button> </div>
-
v-clock
指令:v-cloak 指令配合 css 样式可以解决因网速慢导致页面展示出 {{xxx}} 的问题<!DOCTYPE html> <html lang="zh-cn"> <head><meta charset="UTF-8"><title>Title</title><script type="text/javascript" src="../js/vue.js"></script><style>[v-cloak] {display: none;opacity: 100%;}</style> </head> <body> <div id="root"><div><h1>姓名:</h1><!-- new Vue 实例创建完成并接管 #root 容器时,v-cloak 属性会被自动删除 --><h1 v-cloak>{{ name }}</h1></div> </div> </body> </html>
1.11 自定义指令
-
函数式自定义局部指令:自定义指令函数中的
this -> Window
<div id="root"><div>n = {{ n }}</div><!-- 通过自定义指令实现 n *= 10 --><div>n * 10 = <span v-multiply="n"></span></div><button @click="n++">n++</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {n: 1},// 自定义局部指令directives: {/** multiply 函数何时被调用:* 1. 指令与函数成功绑定时* 2. 指令所在的模板被重新解析时*/multiply(element, binding) {/* 此处的 this 指向 Window */element.innerText = binding.value * 10;}}}) </script>
-
对象式自定义局部指令:自定义指令函数中的
this -> Window
<div id="root"><div v-text="num"></div><button @click="num++">num++</button><!-- 通过自定义指令实现页面加载完成时自动聚焦 --><input type="text" v-myfocus:value="num"> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {num: 1},// 自定义局部指令directives: {myfocus: {// 指令与函数成功绑定时调用bind(element, binding) {element.value = binding.value},// 元素被插入到页面后调用inserted(element, binding) {element.focus()},// 指令所在的模板被重新解析时调用update(element, binding) {element.value = binding.value}}}}) </script>
-
自定义全局指令,可供多个 Vue 实例使用:
<div id="root"><div>n = {{ n }}</div><!-- 通过自定义全局指令实现 n *= 10 --><div>n * 10 = <span v-multiply="n"></span></div><button @click="n++">n++</button> </div><script type="text/javascript">Vue.config.productionTip = false// 自定义全局指令,可供多个 Vue 实例使用Vue.directive('multiply', function (element, binding) {element.innerText = binding.value * 10;});new Vue({el: '#root',data: {n: 1}}) </script>
1.12 列表数据处理
-
列表数据过滤之监视属性实现:
<div id="root"><input type="text" v-model="keyWord" placeholder="请输入关键字"><ul><li v-for="p in filterPersons" :key="p.id">{{ p.name }}</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {keyWord: '',persons: [{id: '001', name: '马冬梅'},{id: '002', name: '周冬雨'},{id: '003', name: '周杰伦'},{id: '004', name: '蔡家伦'}],// 存储过滤后的 persons 信息filterPersons: []},watch: {keyWord: {// 加载完成就立即监视一次以显示数据immediate: true,handler(newVal) {this.filterPersons = this.persons.filter((p) => {return p.name.indexOf(newVal) !== -1;});}}}}) </script>
-
列表数据过滤之计算属性实现:
<div id="root"><input type="text" v-model="keyWord" placeholder="请输入关键字"><ul><li v-for="p in filterPersons" :key="p.id">{{ p.name }}</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {keyWord: '',persons: [{id: '001', name: '马冬梅'},{id: '002', name: '周冬雨'},{id: '003', name: '周杰伦'},{id: '004', name: '蔡家伦'}]},computed: {filterPersons() {return this.persons.filter((p) => {return p.name.indexOf(this.keyWord) !== -1;});}}}) </script>
-
列表数据排序:
<div id="root"><input type="text" v-model="keyWord" placeholder="请输入关键字"><button @click="sortType = 0">原顺序</button><button @click="sortType = 1">年龄升序</button><button @click="sortType = 2">年龄降序</button><ul><li v-for="p in filterPersons" :key="p.id">姓名:{{ p.name }} 年龄:{{ p.age }}</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {keyWord: '',sortType: 0,persons: [{id: '001', name: '马冬梅', age: 12},{id: '002', name: '周冬雨', age: 5},{id: '003', name: '周杰伦', age: 34},{id: '004', name: '蔡家伦', age: 21}]},computed: {filterPersons() {const resArr = this.persons.filter((p) => {return p.name.indexOf(this.keyWord) !== -1;});// 对过滤后的结果进行排序if (this.sortType) {resArr.sort((p1, p2) => {return this.sortType === 1 ? p1.age - p2.age : p2.age - p1.age;});}return resArr;}}}) </script>
1.13 表单数据收集
<div id="root"><!-- prevent 阻止表单的默认提交行为 --><form @submit.prevent="register"><!-- trim 修饰符去除字符串前后空格 -->账户:<input type="text" v-model.trim="user.account"><br/><br/>密码:<input type="password" v-model.trim="user.password"><br/><br/><!-- number 修饰符将输入内容转换为数字 -->年龄:<input type="number" v-model.number="user.age"><br/><br/>性别:<input type="radio" name="sex" value="male" v-model="user.sex">男<input type="radio" name="sex" value="female" v-model="user.sex">女<br/><br/>爱好:<input type="checkbox" value="programming" v-model="user.hobbies">编程<input type="checkbox" value="read" v-model="user.hobbies">阅读<input type="checkbox" value="sleep" v-model="user.hobbies">睡觉<br/><br/>校区:<select v-model="user.school"><option value="请选择校区">请选择校区</option><option value="yu">余家头</option><option value="ma">马房山</option><option value="nan">南湖</option></select><br/><br/><!-- lazy 修饰符意为懒加载即当前元素失焦后收集 -->其它:<textarea v-model.lazy="user.comments"></textarea><br/><br/><input type="checkbox" v-model="user.agree"> 阅读并同意 <a href="">《用户协议》</a><br/><br/><button>提交</button></form>
</div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {user: {account: '',password: '',age: '',sex: 'male',hobbies: [],school: '请选择校区',comments: '',agree: false}},methods: {register() {console.log(JSON.stringify(this.user))}}})
</script>
1.14 过滤器
Vue 过滤器的功能是对要显示的数据进行格式化后,其并没有改变原本的数据,只是产生新的对应的数据。过滤器并不是必须要用的东西,它只是 Vue 给我们提供的新的数据处理方式,过滤器能做到的,用计算属性、methods方法依然可以实现
<!-- <script src=".11.5/dayjs.min.js"></script> -->
<div id="root">计算属性实现 Datetime:{{ computedDatetime }} <br/><br/>方法实现 Datetime: {{ methodsDatetime(Date.now()) }} <br/><br/><!-- 插值语法中使用过滤器 -->过滤器实现 Datetime: {{ Date.now() | timeFormatter }} <br/><br/><!-- 数据绑定中使用过滤器 -->过滤器传参实现 Date: <span :text="Date.now() | timeFormatter('YYYY-MM-DD')"></span> <br/><br/>多级过滤器 Year:{{ Date.now() | timeFormatter('YYYY-MM-DD') | mySlice }}
</div><script type="text/javascript">Vue.config.productionTip = false// 全局过滤器,多个 Vue 实例可用Vue.filter('mySlice', function (val) {return val.slice(0, 4);});new Vue({el: '#root',data: {},computed: {computedDatetime() {return dayjs().format('YYYY-MM-DD HH:mm:ss')}},methods: {methodsDatetime(now) {return dayjs(now).format('YYYY-MM-DD HH:mm:ss')}},filters: {// 若调用 timeFormatter 过滤器时传入了 formatStr 则使用传入值,否则使用默认值timeFormatter(val, formatStr = 'YYYY-MM-DD HH:mm:ss') {return dayjs(val).format(formatStr)}}})
</script>
1.15 :key 作用与原理
-
:key
作用:用于标识当前元素的唯一性,利于 Vue 中元素 Diff 算法的比较<ul><!-- in 也可以替换为 of --><li v-for="(item,index) in persons" :key="item.id">{{ index }}-{{ item.name }}-{{ item.age }}</li> </ul>
-
:key
使用index
作为元素唯一标识容易产生的问题:- 当对 data 中的数据进行逆序添加、逆序删除等
破坏顺序
的操作时,会产生不必要的真实 DOM 更新,效率低 - 当页面结构中还存在输入类的 DOM 时,会产生错误的 DOM 更新,如下图所示:
<div id="root"><h1>v-for 遍历数组</h1><ul><li v-for="(item,index) in persons" :key="index">{{ index }}-{{ item.name }}-{{ item.age }} <input type="text"></li></ul><button @click="addPerson">添加</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {persons: [{id: '001', name: '张三', age: 3},{id: '002', name: '李四', age: 4},{id: '003', name: '王五', age: 5},]},methods: {addPerson() {const p = {id: '004', name: '赵六', age: 6}this.persons.unshift(p);}}}) </script>
- 当对 data 中的数据进行逆序添加、逆序删除等
-
Vue 中 :key 的内部原理:
- key 是虚拟 DOM 的唯一标识,当数据发生变化时, Vue 会根据新数据生成新的虚拟 DOM,随后进行新虚拟 DOM 与旧虚拟 DOM 的 Diff 差异比较
- 若旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key 且若内容未发生变化则使用之前的真实 DOM,否则根据新的虚拟 DOM 生成新的真实 DOM 并替换页面中真实 DOM
1.16 数据监测原理
-
模拟 Vue 监测对象数据 data:
<script type="text/javascript">let data = {'name': 'Spring-_-Bear','age': 18}// 创建一个监视实例对象,用于检测 data 的变化const obs = new Observer(data);let vm = {};vm._data = data = obs;function Observer(obj) {const keys = Object.keys(obj);// 遍历为每个 key 生成 getter 和 setterkeys.forEach((key) => {// this 指向 Observer 实例对象Object.defineProperty(this, key, {get() {console.log('Data got successfully')return obj[key];},set(newVal) {console.log('Data has changed!');obj[key] = newVal;}})});} </script>
-
vm.$set()
给对象追加属性以实现数据响应式监测:<div id="root"><h1>学生信息</h1><button @click="addSex('男')">添加学生性别</button><button v-if="student.sex" @click="student.sex = student.sex === '男'? '女' : '男'">修改学生性别</button><p>姓名:{{ student.name }}</p><p>年龄:{{ student.age }}</p><p v-if="student.sex">性别:{{ student.sex }}</p> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {student: {name: 'Spring-_-Bear',age: 22}},methods: {addSex(val) {this.$set(this.student, 'sex', val);}}}) </script>
-
Vue 监测数组数据变化:Vue 将被侦听的数组的
变更方法
进行了包裹,通过这些方法操作数组数据也会触发视图更新。这些被包裹过的方法包括:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
<div id="root"><h1>学生信息</h1><p>姓名:{{ student.name }}</p><p>年龄:{{ student.age }}</p><p>爱好如下:</p><ul><li v-for="(h,index) in student.hobbies" :key="index">{{ h }}</li></ul><!-- push()、pop()、shift()、unshift()、splice()、sort()、reverse() --><button @click="student.hobbies.splice(0, 1, '睡觉')">修改爱好</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {student: {name: 'Spring-_-Bear',age: 22,hobbies: ['学习', '钓鱼', '阅读']}},}) </script>
-
Vue 数据监测原理:会监视 data 中所有层次的数据
- 如何监测对象中的数据:通过 setter 实现数据监测,需要在创建 Vue 实例时就传入要监测的数据。对于对象中后追加的属性,Vue 默认不做响应式处理,若需实现响应式需使用如下 API:
Vue.set(target, propertyName/index, value)
vm.$set(target, propertyName/index, value)
Vue.set()
和vm.$set()
不能给 vm 或 data 追加属性
- 如何监测数组中的数据:通过包裹数组更新元素的方法实现监测数组中的数据
- 如何监测对象中的数据:通过 setter 实现数据监测,需要在创建 Vue 实例时就传入要监测的数据。对于对象中后追加的属性,Vue 默认不做响应式处理,若需实现响应式需使用如下 API:
1.17 生命周期
-
生命周期:又名生命周期回调函数、生命周期函数、生命周期钩子,是一些在关键时刻 Vue 调用的特殊名称的函数。生命周期函数的名字不可更改,但函数体内容由程序员实现,函数中的 this 对象指向 vm 或组件实例对象
-
mounted 挂载函数的基本使用:
<div id="root"><!-- 若隐若现 --><div :style="{opacity: opacity}">Spring-_-Bear 学 Vue</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {opacity: 0},// Vue 完成模板的解析并把初始的真实 DOM 放入页面调用 mounted 函数,即挂载完成后调用mounted() {setInterval(() => {if (this.opacity > 1) {this.opacity = 0;}this.opacity += 0.01;}, 20);}}) </script>
-
生命周期流程图:
-
生命周期的注意事项:
destoryed
:销毁后借助 Vue 开发者工具看不到任何信息;自定义事件会失效,但原生 DOM 事件依然有效beforeDestroy
:一般不会在 beforeDestroy 中操作数据,即使操作了数据也不会触发更新流程
二、组件化
2.1 概述
-
组件:实现应用中局部功能代码和资源的集合
-
Vue 实例中
template
配置项的使用:<div id="root"><!-- 当从 template 中渲染页面时,<div id="root"> 中定义的属性将全部丢失 --><h1>Vue</h1> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',template:`<div><div>Spring-_-Bear 学 Vue</div><span>Hello World!</span></div>`}) </script>
2.2 组件的定义和使用
-
组件定义和使用的注意事项:
- el 不能配置:因为所有的组件最终都要经过一个 vm 的管理,由 vm 中的 el 决定所有的组件服务于哪个容器
- data 必须写成函数:目的是避免组件被复用时数据间存在引用关系
- 组件名:
kebab-case
风格,如 my-schoolCamelCase
风格,如 MySchool(需要 Vue 脚手架支持)
- 组件使用:
- 双标签写法:例如
<school></school>
- 单标签写法:例如
<school/>
,不使用脚手架时会导致后续组件不能渲染
- 双标签写法:例如
- 组件定义的简写方式:
const school = Vue.extend(options)
=>const school = options
- 可以在定义组件时使用 name 配置项指定组件在 Vue 开发者工具中呈现的名字
-
非单文件组件的定义和使用:
<div id="root"><!-- 使用组件 --><school></school><hr/><student></student> </div> <hr/> <script type="text/javascript">Vue.config.productionTip = false// 定义 school 组件const school = Vue.extend({template:`<div><h1>学校名称:{{ schoolName }}</h1><h1>学校地址:{{ address }}</h1><button @click="showSchool">展示学校</button></div>`,data() {return {schoolName: 'WHUT',address: '湖北省武汉市'}},methods: {showSchool() {alert(this.schoolName);}}});// 定义 student 组件const student = Vue.extend({template:`<div><h1>学生姓名:{{ studentName }}</h1><h1>学生年龄:{{ age }}</h1></div>`,whadata() {return {studentName: 'Spring-_-Bear',age: 18}}});new Vue({el: '#root',components: {// 注册局部组件school: school,student: student}}) </script>
-
嵌套组件的定义和使用:
<div id="root"></div><script type="text/javascript">Vue.config.productionTip = false// 定义 student 组件const student = Vue.extend({template:`<div><h1>学生姓名:{{ studentName }}</h1><h1>学生年龄:{{ age }}</h1></div>`,data() {return {studentName: 'Spring-_-Bear',age: 18}}});// 定义 school 组件const school = Vue.extend({template:`<div><h1>学校名称:{{ schoolName }}</h1><h1>学校地址:{{ address }}</h1><student></student></div>`,data() {return {schoolName: 'WHUT',address: '湖北省武汉市'}},// 嵌套组件注册components: {student: student}});// 定义 hello 组件const hello = Vue.extend({template:`<div><h1>Hello World</h1></div>`})// 管理组件的组件 app 组件const app = Vue.extend({template: `<div><school></school><hello></hello></div>`,components: {// school 组件中使用 student 组件school: school,hello: hello}});new Vue({template: `<app></app>`,el: '#root',components: {app: app}}) </script>
-
全局组件的定义和使用:
<div id="root"><h1>root</h1><hello></hello> </div> <hr/> <div id="app"><h1>app</h1><hello></hello> </div><script type="text/javascript">Vue.config.productionTip = false// 注册全局组件Vue.component('hello', Vue.extend({template:`<div><h1>Hello World</h1></div>`}));new Vue({el: '#root'})new Vue({el: '#app'}) </script>
-
单文件组件的定义和使用:
-
main.js
:应用入口文件,注册并使用 App 组件import App from "./App";new Vue({el: 'root',template: `<App></App>`,components: {App} });
-
App.vue
:顶级组件,用于管理其它组件<template><div><school></school><student></student></div> </template><script> // 引入其它组件 import School from "./School"; import Student from "./Student";export default {name: "App",// 注册组件components: {Student, School} } </script>
-
School.vue
:实现学校相关功能<!-- 组件的结构 --> <template><div class="demo"><h1>学校名称:{{ name }}</h1><h1>学校地址:{{ address }}</h1><button @click="showSchool">show</button></div> </template><!-- 组件的行为 --> <script> // 暴露组件 export default {name: 'School',data() {return {name: '武汉理工大学',address: '湖北省武汉市'}},methods: {showSchool() {alert(this.name);}} } </script><!-- 组件的样式 --> <style> .demo {background-color: aqua; } </style>
-
Student.vue
:实现学生相关功能<!-- 组件的结构 --> <template><div><h1>姓名:{{ name }}</h1><h1>年龄:{{ age }}</h1></div> </template><!-- 组件的行为 --> <script> // 暴露组件 export default {name: 'Student',data() {return {name: 'Spring-_-Bear',age: 18}} } </script>
-
index.html
:引入vue.js
和main.js
<!DOCTYPE html> <html lang="zh-cn"> <head><meta charset="UTF-8"><title>首页</title> </head> <body> <div id="root"></div><script type="text/javascript" src="../js/vue.js"></script> <script type="text/javascript" src="main.js"></script> </body> </html>
-
2.3 VueComponent
-
组件:本质是一个名为
VueComponent
的构造函数,由Vue.extend
函数调用。当使用组件时,Vue 解析组件标签的过程中会生成组件的实例对象。每次调用 Vue.extend,返回的都是一个全新的 VueComponent -
this 对象:
- 在组件实例 vc 中:data 函数、methods 中的函数、watch 中的函数、computed 中的函数,this 指向 VueComponent 实例对象 vc
- 在 Vue 实例 vm 中:data 函数、methods 中的函数、watch 中的函数、computed 中的函数,this 指向 Vue 实例 vm
-
显式原型对象与隐式原型对象间的关系:
<script type="text/javascript">function Demo() {this.a = 1this.b = 2}const demo = new Demo();console.log('显式原型对象:', Demo.prototype);console.log('隐式原型对象:', demo.__proto__);// output: trueconsole.log(Demo.prototype === demo.__proto__)// 通过显式原型属性操作原型对象,追加一个 x 属性,值为 38,则 demo 身上也能直接看到 xDemo.prototype.x = 38;/* * 实例的隐式原型属性永远指向自己缔造者的原型对象*/console.log(demo.x); </script>
-
组件重要的内置关系:
VueComponent.prototype.__proto__ === Vue.prototype
,即让组件实例对象(vc)可以访问到 Vue 原型上的属性和方法
三、脚手架
3.1 开发环境配置
-
安装
Node.js
并配置淘宝镜像:npm config set registry
-
全局安装 Vue 脚手架:
npm install -g @vue/cli
-
创建 Vue 项目:
vue create projectName
-
启动 Vue 项目:
npm run serve
-
修改默认配置:在工程路径下的
vue.config.js
中修改默认配置,如关闭语法检查:const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({transpileDependencies: true,// 关闭语法检查lintOnSave: false })
3.2 render 函数
// 默认引入 vue/dist/vue.runtime.esm.js
import Vue from 'vue'
import App from './App.vue'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App)
})
vue.js
与 vue.runtime.xxx.js
的区别:
-
vue.js 是完整版的 Vue,包含核心功能和模板解析器
-
vue.runtime.xxx.js 是运行时版的 Vue,只包含核心功能,没有模板解析器,所以不能使用 template 配置项,需要使用 render 函数接收到的 createElement 函数去创建具体内容
render(createElement) {// 通过 render 函数接收到的 createElement 函数创建具体内容return createElement('h1', 'Hello World') }
3.3 ref 引用
ref 属性被用来给元素或子组件注册引用信息:
- 应用在 html 标签上获取的是真实的 DOM 元素
- 应用在组件标签上是组件实例对象(VueComponent)
<template><div><h1 ref="hello">Hello World</h1><button @click="show">Click me</button><School ref="school"/></div>
</template><script>
import School from "@/components/School";export default {name: "App",components: {School},methods: {show() {// 真实 DOM 元素console.log(this.$refs.hello);// 组件实例对象console.log(this.$refs.school)}}
}
</script>
3.4 props 配置(组件通信)
props 配置用于组件接收外部传递的数据,一般用于接收父组件所传递的数据。props 传递的数据是 只读的
,Vue 底层会检测对 props 传递数据的修改行为,若进行了修改则会在控制台发出警告
App.vue
:给子组件传递数据
<template><div><!-- 给组件传递数据 --><Student name="Spring-_-Bear" sex="男" :age="18"/></div>
</template><script>
import Student from "@/components/Student";export default {name: "App",components: {Student}
}
</script>
Student.vue
:使用 props 配置接受来自父组件的数据
<template><div class="school"><h1>{{ msg }}</h1><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1><h1>年龄:{{ copyAge }}</h1><button @click="copyAge++">age++</button></div>
</template><script>
export default {name: "Student",data() {return {msg: '我是清华大学的学生',// 拷贝 props 中传递的 age 用于修改,因为 props 中接收到的数据是只读的copyAge: this.age}},// 方式一(数组式):简单声明接收// props: ['name', 'sex', 'age']// 方式二(对象式):接收的同时对数据类型进行限制// props: {// name: String,// sex: String,// age, Number// }// 方式三:数据类型限制 + 默认值指定 + 必要性限制props: {name: {type: String,required: true},sex: {type: String,required: false},age: {type: Number,default: 99}}
}
</script><style scoped>
.school {background-color: gray;
}
</style>
3.5 mixin 混入
mixin 混入:用于组件间复用相同的逻辑和数据,混入就是把组件多次使用的属性和方法等内容进行封装
-
配置局部混入:
-
mixin.js
:抽取多个组件的共用逻辑和数据进行封装export const mixin = {methods: {showName() {alert(this.name)}},data() {return {msg: 'Welcome to learn Vue'}} }
-
School.vue
:通过mixins
配置项配置局部混入,可配置多个 mixin<template><div class="school"><h1>{{ msg }}</h1><h1 @click="showName">学校:{{ name }}</h1><h1>地址:{{ address }}</h1></div> </template><script> import {mixin} from "@/mixin";export default {name: "Student",data() {return {name: '武汉理工大学',address: '湖北省武汉市'}},// 配置局部混入mixins: [mixin] } </script><style scoped> .school {background-color: gray; } </style>
-
Student.vue
:通过mixins
配置项配置局部混入,可配置多个 mixin<template><div class="school"><h1>{{ msg }}</h1><h1 @click="showName">姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1></div> </template><script> import {mixin} from "@/mixin";export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},// 配置局部混入mixins: [mixin] } </script><style scoped> .school {background-color: gray; } </style>
-
-
配置全局混入:与配置全局自定义指令、全局过滤器类似
-
mixin.js
:抽取所有组件共有的逻辑和数据进行封装export const mixin = {methods: {showName() {alert(this.name)}},data() {return {msg: 'Welcome to learn Vue'}} }
-
main.js
:引入混入文件并使用Vue.mixin()
方法配置全局混入,可供所有组件使用import Vue from "vue"; import App from './App.vue' import {mixin} from "@/mixin";Vue.config.productionTip = false // 配置全局混入 Vue.mixin(mixin)new Vue({el: '#app',render: h => h(App) })
-
3.6 plugin 插件
插件:用于增强 Vue,本质是一个包含 install()
方法的对象。install 函数的第一个参数是 Vue,第二个参数是插件使用者所传递的数据
pulgins.js
:在 install 方法中配置当前插件所拥有的各种功能,如全局过滤器、全局自定义指令、全局混入等
import {mixin} from "@/mixin";export default {install(Vue, params) {console.log('接收到的参数:', params)// 全局过滤器Vue.filter('mySlice', function (val) {return val.slice(0, 4);});// 全局自定义指令Vue.directive('myfocus', {bind(element, binding) {element.value = binding.value},inserted(element, binding) {element.focus()},update(element, binding) {element.value = binding.value}})// 全局混入Vue.mixin(mixin)// 往 Vue 原型上添加方法Vue.prototype.hello = () => {alert("Hello World")};}
}
main.js
:引入并使用插件
import Vue from "vue";
import App from './App.vue'
import plugins from "@/plugins";Vue.config.productionTip = false
// 使用插件,并给插件传递消息
Vue.use(plugins, 'Used plugins in the main.js')new Vue({el: '#app',render: h => h(App)
})
3.7 localStorage
本地存储内容大小一般为 5M,因浏览器而异。通过 Window.localStorage
和 Window.sessionStorage
对象中的 API 来实现本地存储和会话存储
<div id="root"><button onclick="add()">本地存储:添加</button><button onclick="delItem()">本地存储:删除</button><button onclick="update()">本地存储:修改</button><button onclick="read()">本地存储:读取</button><button onclick="clearAll()">本地存储:清除</button>
</div><script type="text/javascript">// 会话存储对象的 API 和本地存储一致,仅仅是生存周期不同而已const localStorage = window.localStorage;function add() {localStorage.setItem('name', 'Spring-_-Bear');// save objectlocalStorage.setItem('wut', JSON.stringify({name: 'WHUT', address: 'HBWH'}));}function delItem() {localStorage.removeItem('name');}function update() {localStorage.setItem('name', 'bear');}function read() {// read objectlet objStr = localStorage.getItem('wut');console.log(JSON.parse(objStr))}function clearAll() {localStorage.clear();}
</script>
3.8 组件自定义事件(组件通信)
-
组件自定义事件:适用于父组件给子组件绑定自定义事件,从而实现
子 ===> 父
组件通信。自定义事件的回调在父组件中,而事件的触发在子组件中-
方式一:v-on 实现
<template><div><!-- v-on 实现组件自定义事件(v-on.once.getName:事件只触发一次) --><Student v-on:getName="getStudentName"/></div> </template><script> import Student from "@/components/Student";export default {name: "App",components: {Student},methods: {getStudentName(val, ...params) {console.log(val)console.log(params)}} } </script>
-
方式二:ref 实现
<template><div><!-- ref 实现组件自定义事件 --><Student ref="student"/></div> </template><script> import Student from "@/components/Student";export default {name: "App",components: {Student},methods: {getStudentName(val, ...params) {console.log(val)console.log(params)}},mounted() {setTimeout(() => {// 给子组件绑定一个自定义事件this.$refs.student.$on('getName', this.getStudentName)/* this.$refs.student.$once('getName', this.getStudentName) 只触发一次 */}, 3000);} } </script>
-
-
子组件触发自定义事件实现组件通信:
<template><div class="student"><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1><button @click="sendName">发送我的名字</button></div> </template><script> export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},methods: {// 通过调用 VC 身上的自定义事件实现给父组件传递消息sendName() {this.$emit('getName', this.name, 1, 2, 3, 4, 5)}} } </script>
-
自定义事件的解绑:
<template><div class="student"><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1><button @click="sendName">发送我的名字</button><button @click="unbind">解绑自定义事件</button></div> </template><script> export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},methods: {// 通过调用 VC 身上的自定义事件实现给父组件传递消息sendName() {this.$emit('getName', this.name, 1, 2, 3, 4, 5)},unbind() {// 方式一:解绑单个自定义事件// this.$off('getName')// 方式二:解绑多个自定义事件// this.$off(['getName', 'test'])// 方式三:解绑所有自定义事件this.$off()}} } </script>
-
给组件绑定原生 DOM 事件:需要适用
native
事件修饰符<template><div><h1>Name received: {{ msg }}</h1><Student ref="student" @click.native="show"/></div> </template>
-
通过
this.$refs.xxx.$on('eventName', callFunction)
绑定自定义事件时,回调要么配置在methods
中,要么使用箭头函数,否则 this 指向会出问题,即以下两种方式等价<template><div><h1>Name received: {{ msg }}</h1><Student ref="student"/></div> </template><script> import Student from "@/components/Student";export default {name: "App",components: {Student},data() {return {'msg': ''}},mounted() {// 此处的回调函数必须为箭头函数this.$refs.student.$on('getName', val => {console.log(val)this.msg = val});} } </script>
<template><div><h1>Name received: {{ msg }}</h1><Student ref="student"/></div> </template><script> import Student from "@/components/Student";export default {name: "App",components: {Student},data() {return {'msg': ''}},methods: {getStudentName(val) {console.log(val)this.msg = val}},mounted() {this.$refs.student.$on('getName', this.getStudentName)} } </script>
3.9 全局事件总线(组件通信)
全局事件总线是一种组件间通信的方式,适用于任意组件间通信。全局事件总线并不是插件、配置文件等等,事件总线是程序员在做 Vue 开发中总结积累的一套方法、规则,只要满足这套规则就可以实现组件间的通信
-
main.js
:装配全局事件总线,供组件间通信使用import Vue from "vue"; import App from './App.vue'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),beforeCreate() {// 装配全局事件总线Vue.prototype.$bus = this} })
-
Studnet.vue
:消息接收者,在事件总线$bus
上注册事件(函数)<template><div><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1></div> </template><script> export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},mounted() {// 在总线上注册事件this.$bus.$on('stuReceiveMsg', msg => {console.log('我是 Student,收到消息如下:', msg)});},beforeDestroy() {// 解绑当前组件用到的事件this.$bus.$off('stuReceiveMsg')} } </script>
-
School.vue
:消息发送者,利用总线触发事件,实现组件通信<template><div><h1>学校:{{ name }}</h1><h1>地址:{{ address }}</h1><button @click="noticeStudent('所有学生今天必须做核酸')">通知学生做核酸</button></div> </template><script> export default {name: "School",data() {return {name: '武汉理工大学',address: '湖北省武汉市'}},methods: {noticeStudent(msg) {// 通过总线触发事件,从而实现组件通信this.$bus.$emit('stuReceiveMsg', '通知内容如下:' + msg)}} } </script>
-
App.vue
:引入并使用其它组件<template><div><School/><Student/></div> </template><script> import School from "@/components/School"; import Student from "@/components/Student";export default {name: "App",components: {School, Student} } </script>
3.10 消息订阅与发布(组件通信)
安装
pubsub-js
库用于实现消息订阅与发布:npm i pubsub-js
-
消息接收者:也即消息订阅者,注册函数用于订阅消息
<template><div><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1></div> </template><script> import pubsub from 'pubsub-js'export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},mounted() {// 订阅消息:使用箭头函数时 this 指向 VC,使用正常函数时 this 为 undefinedthis.pid = pubsub.subscribe('schoolNotice', (msgName, msg) => {console.log('Come from ' + msgName, ', data is ' + msg);})},beforeDestroy() {// 取消订阅pubsub.unsubscribe(this.pid);} } </script>
-
消息发送者:也即发布者,发布消息
<template><div><h1>学校:{{ name }}</h1><h1>地址:{{ address }}</h1><button @click="noticeStudent('所有学生今天必须做核酸')">通知学生做核酸</button></div> </template><script> import pubsub from 'pubsub-js'export default {name: "School",data() {return {name: '武汉理工大学',address: '湖北省武汉市'}},methods: {noticeStudent(msg) {// 发布消息pubsub.publish('schoolNotice', msg);}} } </script>
-
App.vue
:引入并使用其它组件<template><div><School/><Student/></div> </template><script> import School from "@/components/School"; import Student from "@/components/Student";export default {name: "App",components: {School, Student} } </script>
3.11 $nextTick
$nextTick
指定的回调函数会在下一次 DOM 更新结束后执行。适用于当数据改变后要基于更新后的 DOM 进行某些特殊操作,如输入框值 DOM 更新后下次打开时自动聚焦
用法详见
TodoList
案例中的TodoItem
组件
3.12 动画与过渡
-
动画效果:将要实现实现动画效果的单个元素使用
transition
标签包裹<template><div><button @click="isShow = !isShow">Display / Hide</button><!-- 将要实现实现动画的单个元素使用 `transition` 标签包裹 --><transition name="hello" appear><h1 v-show="isShow">Hello World</h1></transition></div> </template><script> export default {name: "Animation",data() {return {isShow: true}} } </script><style scoped> h1 {background-color: skyblue;text-align: center; }/* 入场激活 */ .hello-enter-active {animation: helloAnimation 0.5s linear; }/* 离开激活 */ .hello-leave-active {animation: helloAnimation 0.5s linear reverse; }/* 定义动画 */ @keyframes helloAnimation {from {transform: translateX(-100%);}to {transform: translateX(0px);} } </style>
-
过渡效果:将要实现实现过渡效果的单个元素使用
transition
标签包裹<template><div><button @click="isShow = !isShow">Display / Hide</button><!-- 将要实现实现过渡效果的单个元素使用 `transition` 标签包裹 --><transition name="hello" appear><h1 v-show="isShow">Hello World</h1></transition></div> </template><script> export default {name: "Animation",data() {return {isShow: true}} } </script><style scoped> h1 {background-color: skyblue;text-align: center; }.hello-enter, .hello-leave-to {transform: translateX(-100%); }.hello-enter-active, .hello-leave-active {transition: 0.5s linear; }.hello-leave, .hello-enter-to {transform: translateX(0); } </style>
-
多个元素过渡:
-
方式一:增加一个顶级
div
囊括需要实现过渡的元素<template><div><button @click="isShow = !isShow">Display / Hide</button><transition name="hello" appear><div v-show="isShow"><h1>Hello World</h1><h1>Vue</h1></div></transition></div> </template><script> export default {name: "Animation",data() {return {isShow: true}} } </script><style scoped> h1 {background-color: skyblue;text-align: center; }.hello-enter, .hello-leave-to {transform: translateX(-100%); }.hello-enter-active, .hello-leave-active {transition: 0.5s linear; }.hello-leave, .hello-enter-to {transform: translateX(0); } </style>
-
方式二:使用
transition-group
元素包裹需要实现过渡效果的多个元素<template><div><button @click="isShow = !isShow">Display / Hide</button><!-- 多个元素过渡效果使用 transition-group 标签包裹 --><transition-group name="hello" appear><h1 v-show="isShow" :key="1">Hello World</h1><h1 v-show="isShow" :key="2">Vue</h1></transition-group></div> </template><script> export default {name: "Animation",data() {return {isShow: true}} } </script><style scoped> h1 {background-color: skyblue;text-align: center; }.hello-enter, .hello-leave-to {transform: translateX(-100%); }.hello-enter-active, .hello-leave-active {transition: 0.5s linear; }.hello-leave, .hello-enter-to {transform: translateX(0); } </style>
-
3.13 TodoList 案例
安装 nanoid 用于生成 UUID:
npm i nanoid
用到的知识点:
- 双向数据绑定、键盘事件、监视属性、条件渲染、事件绑定、钩子函数、嵌套组件
- props、组件自定义事件、全局事件总线
- 多个元素过渡、浏览器本地存储
- nanoid、ref、$nexttick
-
main.js
import Vue from "vue"; import App from './App.vue'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),beforeCreate() {// 装配全局事件总线Vue.prototype.$bus = this} })
-
App.vue
<template><div id="root"><div class="todo-container"><div class="todo-wrap"><!-- 自定义事件 addTodoItem --><TodoHeader @addTodoItem="addTodoItem"/><!-- 给组件传递数据 --><TodoList :todos="todos"/><!-- 自定义事件 checkAllItems、clearCompletedItems --><TodoFooter :todos="todos" @checkAllItems="checkAllItems" @clearCompletedItems="clearCompletedItems"/></div></div></div> </template><script> import TodoHeader from "@/components/TodoHeader"; import TodoList from "@/components/TodoList"; import TodoFooter from "@/components/TodoFooter";export default {name: 'App',components: {TodoHeader, TodoList, TodoFooter},data() {return {// 浏览器本地读取 todos 数据todos: JSON.parse(localStorage.getItem('todos')) || []}},methods: {// 增addTodoItem(todoItem) {this.todos.unshift(todoItem)},// 删deleteTodoItem(id) {this.todos = this.todos.filter((todo) => {return id !== todo.id;});},// 删:清除已完成项clearCompletedItems() {this.todos = this.todos.filter((todo) => {return !todo.completed});},// 改:修改项描述updateTodoItem(id, description) {this.todos.forEach((todo) => {if (id === todo.id) {todo.description = description}});},// 改:选中全部checkAllItems(status) {this.todos.forEach((todo) => {todo.completed = status});},// 改:修改项状态changeTodoItemStatus(id) {this.todos.forEach((todo) => {if (id === todo.id) {todo.completed = !todo.completed;}});}},watch: {todos: {deep: true,handler(newVal) {// 浏览器本地存储localStorage.setItem('todos', JSON.stringify(newVal))}}},mounted() {// 在全局事件总线上注册事件this.$bus.$on('changeTodoItemStatus', this.changeTodoItemStatus)this.$bus.$on('deleteTodoItem', this.deleteTodoItem)this.$bus.$on('updateTodoItem', this.updateTodoItem)},beforeDestroy() {// 解绑全局事件总线事件this.$bus.$off(['changeTodoItemStatus', 'deleteTodoItem', 'updateTodoItem'])} } </script><style> body {background: #fff; }.btn {display: inline-block;padding: 4px 12px;margin-bottom: 0;font-size: 14px;line-height: 20px;text-align: center;vertical-align: middle;cursor: pointer;box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);border-radius: 4px; }.btn-danger {color: #fff;background-color: #da4f49;border: 1px solid #bd362f; }.btn-info {color: #fff;background-color: skyblue;border: 1px solid #2e5968;margin-right: 5px; }.btn-info:hover {color: #fff;background-color: #2e5968; }.btn-danger:hover {color: #fff;background-color: #bd362f; }.btn:focus {outline: none; }.todo-container {width: 600px;margin: 0 auto; }.todo-container .todo-wrap {padding: 10px;border: 1px solid #ddd;border-radius: 5px; } </style>
-
TodoHeader.vue
<template><div class="todo-header"><input type="text" placeholder="请输入您的任务,按回车键确认" v-model.trim="description" @keyup.enter="addItem"/></div> </template><script> import {nanoid} from "nanoid";export default {name: "TodoHeader",data() {return {description: ''}},methods: {addItem() {if (!this.description) {return alert("添加项目不能为空!");}// 全局事件总线:触发 App 组件中的自定义事件实现消息通信(子 => 父)this.$emit('addTodoItem', {id: nanoid(), description: this.description, completed: false});this.description = '';}} } </script><style scoped> .todo-header input {width: 560px;height: 28px;font-size: 14px;border: 1px solid #ccc;border-radius: 4px;padding: 4px 7px; }.todo-header input:focus {outline: none;border-color: rgba(82, 168, 236, 0.8);box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); } </style>
-
TodoList.vue
<template><ul class="todo-main"><!-- 多个元素过渡 --><transition-group name="todo" appear><TodoListItem v-for="todo in todos" :todo="todo" :key="todo.id"/></transition-group></ul> </template><script> import TodoListItem from "@/components/TodoItem";export default {name: "TodoList",components: {TodoListItem},props: ['todos'] } </script><style scoped> .todo-main {margin-left: 0px;border: 1px solid #ddd;border-radius: 2px;padding: 0px; }.todo-empty {height: 40px;line-height: 40px;border: 1px solid #ddd;border-radius: 2px;padding-left: 5px;margin-top: 10px; }.todo-enter, .todo-leave-to {transform: translateX(100%); }.todo-enter-active, .todo-leave-active {transition: 0.3s linear; }.todo-leave, .todo-enter-to {transform: translateX(0); } </style>
-
TodoItem.vue
<template><li><label><!-- 选中状态 --><input type="checkbox" :checked="todo.completed" @change="changeItemStatus(todo.id)"/> <!-- 内容 --><span v-show="!todo.isEdit">{{ todo.description }}</span><!-- 编辑输入框 --><input type="text" :value="todo.description" v-show="todo.isEdit" @blur="handleBlur($event, todo)" ref="updateInputEle"></label><!-- 删除与编辑 --><button class="btn btn-danger" @click="deleteItem(todo.id)">删除</button><button class="btn btn-info" @click="editItem(todo)" v-show="!todo.isEdit">编辑</button></li> </template><script> export default {name: "TodoListItem",props: ['todo'],methods: {deleteItem(id) {// 触发全局事件总线实现消息通信(孙 => 爷)this.$bus.$emit('deleteTodoItem', id)},changeItemStatus(id) {// 触发全局事件总线实现消息通信(孙 => 爷)this.$bus.$emit('changeTodoItemStatus', id)},editItem(todo) {if (todo.hasOwnProperty('isEdit')) {todo.isEdit = true} else {this.$set(todo, 'isEdit', true)}// $nextTick 指定的回调函数会在 DOM 节点更新之后再执行this.$nextTick(function () {this.$refs.updateInputEle.focus()});},handleBlur(e, todo) {if (e.target.value.trim()) {// 触发全局事件总线实现消息通信(孙 => 爷)this.$bus.$emit('updateTodoItem', todo.id, e.target.value)}todo.isEdit = false}} } </script><style scoped> li {list-style: none;height: 36px;line-height: 36px;padding: 0 5px;border-bottom: 1px solid #ddd; }li label {float: left;cursor: pointer; }li label li input {vertical-align: middle;margin-right: 6px;position: relative;top: -1px; }li button {float: right;display: none;margin-top: 3px; }li:before {content: initial; }li:last-child {border-bottom: none; }li:hover {background-color: gray; }li:hover button {display: block; } </style>
-
TodoFooter.vue
<template><div class="todo-footer" v-show="todos.length"><!-- 选中全部 --><label><input type="checkbox" @change="checkAll" :checked="todos.length === completedTotal && todos.length > 0"/></label><span><span>已完成 {{ completedTotal }}</span> / 全部 {{ todos.length }}</span><button class="btn btn-danger" @click="clearCompleted">清除已完成</button></div> </template><script> export default {name: "TodoFooter",props: ['todos'],computed: {completedTotal() {return this.todos.reduce((pre, cur) => {return pre + (cur.completed ? 1 : 0)}, 0);}},methods: {checkAll(e) {// 触发 App 组件中的自定义事件实现消息通信(子 => 父)this.$emit('checkAllItems', e.target.checked)},clearCompleted() {// 触发 App 组件中的自定义事件实现消息通信(子 => 父)this.$emit('clearCompletedItems');}} } </script><style scoped> .todo-footer {height: 40px;line-height: 40px;padding-left: 6px;margin-top: 5px; }.todo-footer label {display: inline-block;margin-right: 20px;cursor: pointer; }.todo-footer label input {position: relative;top: -1px;vertical-align: middle;margin-right: 5px; }.todo-footer button {float: right;margin-top: 5px; } </style>
3.14 配置代理
安装 axios 请求库:
npm i axios
-
单台服务器代理配置:
-
vue.config.js
中配置代理信息const {defineConfig} = require('@vue/cli-service') module.exports = defineConfig({transpileDependencies: true,// 关闭语法检查lintOnSave: false,// 配置代理devServer: {proxy: 'http://localhost:5000'} })
-
axios
请求库向代理服务器发起请求,由代理服务器向真实服务器发请求<template><button @click="getStudents">获取学生数据</button> </template><script> import axios from 'axios'export default {name: "App",methods: {getStudents() {// 请求代理服务器,由代理服务器发起请求axios.get('http://localhost:8080/students').then(response => {console.log('请求成功', response.data)},error => {console.log('请求失败', error.message)})}} } </script>
-
-
多台服务器代理配置:
-
vue.config.js
中依次配置多台服务器代理const {defineConfig} = require('@vue/cli-service') module.exports = defineConfig({transpileDependencies: true,// 关闭语法检查lintOnSave: false,// 配置代理devServer: {proxy: {'/stu': {target: 'http://localhost:5000',// 重写请求路径pathRewrite: {'^/stu': ''},// WebSocket,默认为 truews: true,// true 时修改请求头中的 HOST 为 target 中的值,默认为 truechangeOrigin: true},'/car': {target: 'http://localhost:5001',pathRewrite: {'^/car': ''}}}} })
-
axios
请求库根据需求向代理服务器发起不同的路径请求,由代理服务器向真实服务器发请求<template><div><button @click="getStudents">获取学生数据</button><button @click="getCars">获取汽车数据</button></div> </template><script> import axios from 'axios'export default {name: "App",methods: {getStudents() {// 请求代理服务器axios.get('http://localhost:8080/stu/students').then(response => {console.log('请求成功', response.data)},error => {console.log('请求失败', error.message)})},// 请求代理服务器getCars() {axios.get('http://localhost:8080/car/cars').then(response => {console.log('请求成功', response.data)},error => {console.log('请求失败', error.message)})}} } </script>
-
-
使用
vue-resource
插件代替axios
请求库:安装 vue-resource 插件:
npm i vue-resource
-
main.js
:引入并使用插件import Vue from "vue"; import App from './App.vue'; import VueResource from 'vue-resource'Vue.config.productionTip = false // 使用 vue-resource 插件 Vue.use(VueResource)new Vue({el: '#app',render: h => h(App) })
-
this.$http.get
:发起请求,获取响应<template><button @click="getStudents">获取学生数据</button> </template><script>export default {name: "App",methods: {getStudents() {// 使用 this.$http.get 发起请求this.$http.get('http://localhost:8080/students').then(response => {console.log('请求成功', response.data)},error => {console.log('请求失败', error.message)})}} } </script>
-
3.15 GitHub 案例
需在 public 目录中的
index.html
中引入bootstrap.css
用到的知识点:
- 全局事件总线
- axios 请求库
-
main.js
:装配全局事件总线import Vue from "vue"; import App from './App.vue'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),beforeCreate() {// 装配全局事件总线Vue.prototype.$bus = this} })
-
App.vue
:引入并使用其它组件<template><div class="container"><Search/><List/></div> </template><script> import Search from "@/components/Search"; import List from "@/components/List";export default {name: "App",components: {List, Search} } </script>
-
Search.vue
:发起请求,获取响应,通过全局事件总线传递数据<template><section class="jumbotron"><h3 class="jumbotron-heading">GitHub Users Search</h3><div><input v-model="keyWord" type="text" placeholder="enter the username you want to search"/> <button @click="searchUsers">Search</button></div></section> </template><script> import axios from 'axios'export default {name: "Search",data() {return {'keyWord': ''}},methods: {searchUsers() {// 加载中···this.$bus.$emit('getUserInfo', {isFirst: false, isLoading: true, errorMsg: '', users: []})// 发起请求,获取响应axios.get(`=${this.keyWord}`).then(response => {this.$bus.$emit('getUserInfo', {isFirst: false, isLoading: false, errorMsg: '', users: response.data.items})},error => {this.$bus.$emit('getUserInfo', {isFirst: false, isLoading: false, errorMsg: error.message, users: []})})}} } </script>
-
List.vue
:通过全局事件总线接收数据并解析展示<template><div class="row"><div class="card" v-for="user in info.users" :key="user.id" v-show="info.users.length"><a :href="user.html_url" target="_blank"><img :src="user.avatar_url" alt="user's avatar" style='width: 100px'/></a><p class="card-text">{{ user.login }}</p></div><h1 v-show="info.isFirst">欢迎使用 Github 用户搜索</h1><h1 v-show="info.isLoading">Loading···</h1><h1 v-show="info.errorMsg">{{ info.errorMsg }}</h1></div> </template><script> export default {name: "List",data() {return {info: {isFirst: true,isLoading: false,errorMsg: '',users: []}}},mounted() {this.$bus.$on('getUserInfo', (info) => {// 字面量对比更新this.info = {...this.info, ...info}});},beforeDestroy() {this.$bus.$off('getUserInfo')} } </script><style scoped> .card {float: left;width: 33.333%;padding: .75rem;margin-bottom: 2rem;border: 1px solid #efefef;text-align: center; }.card > img {margin-bottom: .75rem;border-radius: 50%; }.card-text {font-size: 85%; } </style>
3.16 slot 插槽(组件通信)
插槽(Slot
)是 Vue 提出来的一个概念,正如其名字一样,插槽用于决定将所携带的内容插入到指定的某个位置,从而使模板分块。插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示由子组件来进行控制。插槽可以让父组件可以向子组件指定位置插入 HTML 结构,也是一种组件间的通信方式,适用于 父组件 -> 子组件
-
默认插槽:父组件定义插槽,子组件中使用
slot
标签使用插槽-
App.vue
:往子组件的标签体中定义插槽需要显示的内容<template><div class="container"><Category title="美食"><img src=".png" alt="WHUT"></Category><Category title="游戏"><ul><li v-for="(movie, index) in movies" :key="index">{{ movie }}</li></ul></Category><Category title="电影"><video controls src=".png"></video></Category></div> </template><script> import Category from "@/components/Category";export default {name: "App",components: {Category},data() {return {foods: ['火锅', '早茶', '烤鸭'],games: ['原神', '崩坏3', '守望先锋', '王者荣耀', '和平精英'],movies: ['肖申克的救赎', '美丽人生', '我们的父辈', '阿甘正传', '这个杀手不太冷']}} } </script><style> .container {display: flex;justify-content: space-around; }h3 {text-align: center;background-color: orange; } </style>
-
Category.vue
:使用slot
标签显示父组件定义的插槽内容<template><div class="category"><h3>{{ title }}</h3><!-- 默认插槽,显示父组件在 Category 组件体中定义的内容 --><slot>default value</slot></div> </template><script> export default {name: "Category",props: ['title'] } </script><style scoped> .category {background-color: skyblue;width: 200px;height: 300px; }img {width: 100%; }video {width: 100%; } </style>
-
-
具名插槽:顾名思义,就是有名字的插槽
-
App.vue
:在标签的属性上使用slot
属性指定当前标签属于哪个插槽<template><div class="container"><Category title="美食"><!-- 使用 slot 属性指定具体的插槽 --><img slot="first" src=".png" alt="WHUT"><div slot="second" class="container"><a href="">B 大学</a></div></Category><Category title="游戏"><ul slot="first"><li v-for="(movie, index) in movies" :key="index">{{ movie }}</li></ul><!-- 复用同一个插槽 --><div slot="second" class="container"><a href="">百度</a><a href="">谷歌</a></div></Category><Category title="电影"><video slot="first" controls src=".png"></video><!-- 只能在 template 标签中使用 v-slot:second,等价于 slot="second" --><template v-slot:second><div class="container"><a href="">经典</a><a href="">热门</a><a href="">推荐</a></div><h4>前往购票</h4></template></Category></div> </template><script> import Category from "@/components/Category";export default {name: "App",components: {Category},data() {return {foods: ['火锅', '早茶', '烤鸭'],games: ['原神', '崩坏3', '守望先锋', '王者荣耀', '和平精英'],movies: ['肖申克的救赎', '美丽人生', '我们的父辈', '阿甘正传', '这个杀手不太冷']}} } </script><style> .container {display: flex;justify-content: space-around; }h3 {text-align: center;background-color: orange; }h4 {text-align: center; } </style>
-
Category.vue
:使用slot
标签的name
属性值指定使用的具体插槽<template><div class="category"><h3>{{ title }}</h3><!-- 具名插槽 --><slot name="first">default value</slot><slot name="second">default value</slot></div> </template><script> export default {name: "Category",props: ['title'] } </script><style scoped> .category {background-color: skyblue;width: 200px;height: 300px; }img {width: 100%; }video {width: 100%; } </style>
-
-
作用域插槽:作用域插槽就是带参数(数据)的插槽,强调的则是数据作用的范围。在子组件的插槽中带入参数(数据)提供给父组件使用,该参数(数据)仅在插槽内有效,父组件可以根据子组件中传过来的插槽参数(数据)对展示内容进行定制
-
App.vue
:在template
标签上使用scope
或slot-scope
属性接收来自子组件(插槽使用者)传递的数据<template><div class="container"><Category title="电影"><!-- 接收子组件传递的数据(方式一) --><template scope="data"><h4>{{ data.msg }}</h4><ul><li v-for="(movie, index) in data.movies" :key="index">{{ movie }}</li></ul></template></Category><Category title="电影"><!-- 接收子组件传递的数据(方式二) --><template slot-scope="{movies, msg}"><!-- scope="{movies, msg}" <=> slot-scope="{movies, msg} --><h4>{{ msg }}</h4><ol><li v-for="(movie, index) in movies" :key="index">{{ movie }}</li></ol></template></Category></div> </template><script> import Category from "@/components/Category";export default {name: "App",components: {Category} } </script><style> .container {display: flex;justify-content: space-around; }h4 {text-align: center; } </style>
-
Category.vue
:给父组件(插槽定义者)传递数据<template><div class="category"><h3>{{ title }}</h3><!-- 作用域插槽:给插槽定义者传递数据 --><slot :movies="movies" :msg="'快来购票观影吧'">default value</slot></div> </template><script> export default {name: "Category",props: ['title'],data() {return {movies: ['肖申克的救赎', '美丽人生', '我们的父辈', '阿甘正传', '这个杀手不太冷']}} } </script><style scoped> .category {background-color: skyblue;width: 200px;height: 300px; }h3 {text-align: center;background-color: orange; } </style>
-
四、vuex 插件
4.1 概述(组件通信)
-
定义:Vuex 是专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 Vue 应用中多个组件的共享状态进行集中式管理,是一种适用于任意组件间通信的方式
-
Vuex 工作原理图:其中 Actions、Mutations、State 称为 Store
4.2 搭建 Vuex 环境
Vue2 中安装 Vuex3:
npm i vuex@3
-
src/store/index.js
:配置 Vuex 的核心Store
:import Vuex from 'vuex' import Vue from "vue";// 必须先使用 Vuex 插件才能创建 Store 实例 Vue.use(Vuex)// 响应组件的动作 const actions = {} // 操作数据 state const mutations = {} // 存储数据 const state = {} // 加工数据 state const getters = {}export default new Vuex.Store({actions,mutations,state,getters })
-
main.js
:引入为 Vuex 配置的 Store 并配置:import Vue from "vue"; import App from './App.vue'; import store from './store/index'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),// vuex 的核心配置store: store })
4.3 求和案例
-
main.js
:引入为 Vuex 配置的 Store 并配置:import Vue from "vue"; import App from './App.vue'; import store from './store/index'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),// vuex 的核心配置store: store })
-
src/store/index.js
:配置 Vuex 的核心Store
:import Vuex from 'vuex' import Vue from "vue";// 必须先使用 Vuex 插件才能创建 Store 实例 Vue.use(Vuex)// 响应组件中的动作 const actions = {jiaOdd(context, val) {if (context.state.sum % 2 === 1) {context.commit('JIA', val)}},jiaWait(context, val) {setTimeout(() => {context.commit('JIA', val)}, 1000);} } // 操作数据 const mutations = {JIA(state, val) {state.sum += val},JIAN(state, val) {state.sum -= val} } // 存储数据 const state = {sum: 0 }// 加工 state 中的数据 const getters = {enlarge(state) {return state.sum * 10} }export default new Vuex.Store({actions,mutations,state,getters })
-
Count.vue
:通过this.$store.dispath()
、this.$store.commit()
将组件中的动作分别分发给Actions
处理业务逻辑、Mutations
操作数据<template><div><h1>当前求和结果:{{ $store.state.sum }}</h1><h1>数据处理结果:{{ $store.getters.enlarge }}</h1><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option></select><button @click="add">+</button><button @click="sub">-</button><button @click="addOdd">当前求和为奇数时加</button><button @click="addWait">等一会再加</button></div> </template><script> export default {name: "Count",data() {return {n: 1}},methods: {add() {// 直接提交给 Mutations 处理数据this.$store.commit('JIA', this.n)},sub() {this.$store.commit('JIAN', this.n)},addOdd() {// 分发给 Actions 处理业务逻辑this.$store.dispatch('jiaOdd', this.n)},addWait() {this.$store.dispatch('jiaWait', this.n)}} } </script><style scoped> button {margin: 5px; } </style>
4.4 vuex 中的四种 map
mapState
与mapGetters
:将 Store 中的 state 和 getters 映射为 computed 计算属性mapMutations
与mapActions
:将 Store 中的 mutations 和 actions 映射为 methods 方法
<template><div><h1>当前求和结果:{{ sum }}</h1><h1>数据处理结果:{{ enlarge }}</h1><button @click="add(n)">+</button><button @click="sub(n)">-</button><button @click="addOdd(n)">当前求和为奇数时加</button><button @click="addWait(n)">等一会再加</button><hr/><h1>{{ school }}</h1><h1>{{ address }}</h1></div>
</template><script>
import {mapState, mapGetters, mapActions, mapMutations} from 'vuex'export default {name: "Count",data() {return {n: 1}},computed: {/** 方式一(对象式)* ...mapState({sum: 'sum', school: 'school', address: 'address'})* 方式二(数组式):当 state 中数据 key 与计算属性的函数名相同时,可使用数组简写法* ...mapState(['sum', 'school', 'address'])* * 注:mapGetters、mapMutations、mapActions 同理*/// 将 state 映射为计算属性...mapState(['sum', 'school', 'address']),// 将 getters 映射为计算属性...mapGetters(['enlarge'])},methods: {// 用于生成与 Mutations 对话的方法...mapMutations({add: 'JIA', sub: 'JIAN'}),// 用于生成与 Actions 对话的方法...mapActions({addOdd: 'jiaOdd', addWait: 'jiaWait'})},
}
</script>
4.5 vuex 模块化
-
main.js
:引入为 Vuex 配置的 Store 并配置:import Vue from "vue"; import App from './App.vue'; import store from './store/index'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),store: store })
-
src/store/index.js
:引入其它模块化配置import Vuex from 'vuex' import Vue from "vue" import countConfig from "@/store/countConfig"; import personConfig from "@/store/personConfig";Vue.use(Vuex)export default new Vuex.Store({modules: {countStore: countConfig,personStore: personConfig} })
-
src/store/*.js
:各种模块化的 Store 配置-
src/store/countConfig.js
export default {namespaced: true,actions: {jiaOdd(context, val) {if (context.state.sum % 2 === 1) {context.commit('JIA', val)}},jiaWait(context, val) {setTimeout(() => {context.commit('JIA', val)}, 1000);}},mutations: {JIA(state, val) {state.sum += val},JIAN(state, val) {state.sum -= val}},state: {sum: 0},getters: {enlarge(state) {return state.sum * 10}} }
-
src/store/personConfig.js
export default {namespaced: true,actions: {addPersonLi(context, value) {if (value.name.indexOf('李') === 0) {context.commit('ADD_PERSON', value)} else {alert(value.name + '不姓李')}}},mutations: {ADD_PERSON(state, value) {state.persons.unshift(value)}},state: {persons: [{id: 1, name: 'bear'}]},getters: {firstPersonName(state) {return state.persons[0].name}} }
-
-
src/component/*.vue
:在各种组件中通过对应的 store 访问和操作数据-
Person.vue
<template><div><input type="text" v-model="name"> <button @click="addPerson">添加</button> <button @click="addPersonWang">添加一个姓李的人</button><h2>第一个人的姓名:{{ first }}</h2><ol><li v-for="person in persons" :key="person.id">{{ person.name }}</li></ol><h2 style="color: red">结果:{{ sum }}</h2></div> </template><script> import {nanoid} from 'nanoid'export default {name: "Person",data() {return {name: ''}},computed: {sum() {return this.$store.state.countStore.sum},persons() {return this.$store.state.personStore.persons},first() {return this.$store.getters['personStore/firstPersonName']}},methods: {addPerson() {const obj = {id: nanoid(), name: this.name}this.$store.commit('personStore/ADD_PERSON', obj)this.name = ''},addPersonWang() {const obj = {id: nanoid(), name: this.name}this.$store.dispatch('personStore/addPersonLi', obj)this.name = ''}} } </script><style scoped></style>
-
Count.vue
<template><div><h1>求和:{{ sum }}</h1><h1>十倍:{{ enlarge }}</h1><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add(n)">+</button><button @click="sub(n)">-</button><button @click="addOdd(n)">当前求和为奇数时加</button><button @click="addWait(n)">等一会再加</button><h2 style="color: red">Person 总人数:{{ persons.length }}</h2></div> </template><script> import {mapState, mapGetters, mapActions, mapMutations} from 'vuex'export default {name: "Count",data() {return {n: 1}},methods: {...mapMutations('countStore', {add: 'JIA', sub: 'JIAN'}),...mapActions('countStore', {addOdd: 'jiaOdd', addWait: 'jiaWait'})},computed: {...mapState('countStore', {sum: 'sum'}),...mapGetters('countStore', ['enlarge']),...mapState('personStore', ['persons'])}, } </script><style scoped> button {margin: 5px; } </style>
-
-
App.vue
<template><div><Count></Count><Person></Person></div> </template><script> import Count from "@/components/Count"; import Person from "@/components/Person";export default {name: 'App',components: {Person, Count} } </script>
五、vue-router 插件
5.1 概述
- 定义:vue-router 是 vue 的一个插件库,专门用来实现 SPA(
Simple Page web Application
)单页应用 - 路由使用的注意事项:
- 路由组件通常放在
pages
文件夹,一般组件通常放在components
文件夹 - 通过切换隐藏的路由组件,默认立即销毁,需要展示时再挂载
- 每个组件都有自己的
$route
属性,其中存储着自己的路由信息 - 整个应用只有一个 router,可以通过组件的
$router
获取
- 路由组件通常放在
5.2 路由基本使用
Vue2 中安装 vue-router3:
npm i vue-router@3
注:需在
public
目录下的index.html
中引入bootstrap.css
-
main.js
:引入路由插件并进行配置import Vue from "vue"; import App from './App.vue'; import VueRouter from 'vue-router' import router from './router/index'Vue.config.productionTip = falseVue.use(VueRouter)new Vue({el: '#app',render: h => h(App),// 配置路由器router: router })
-
src/router/index.js
:配置路由插件import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home";export default new VueRouter({routes: [{path: '/about', component: About},{path: '/home', component: Home}] })
-
src/pages/*.vue
:各种路由组件-
src/pages/Home.vue
<template><h2>我是 Home 的内容</h2> </template><script> export default {name: "Home" } </script>
-
src/pagse/About.vue
<template><h2>我是 About 的内容</h2> </template><script> export default {name: "About" } </script>
-
-
App.vue
:router-link
标签实现路由组件页面切换,router-view
标签实现组件页面展示<template><div><div class="row"><div class="col-xs-offset-2 col-xs-8"><div class="page-header"><h2>Vue Router Demo</h2></div></div></div><div class="row"><div class="col-xs-2 col-xs-offset-2"><div class="list-group"><!-- 路由实现 SPA 应用页面切换 --><router-link active-class="active" class="list-group-item" to="/home">Home</router-link><router-link active-class="active" class="list-group-item" to="/about">About</router-link></div></div><div class="col-xs-6"><div class="panel"><div class="panel-body"><!-- 展示路由组件 --><router-view></router-view></div></div></div></div></div> </template><script> export default {name: "App" } </script>
5.3 嵌套与命名路由
-
嵌套路由:通过
children
配置项在当前路由路径下配置子路由路径export default new VueRouter({routes: [{// 命名路由name: 'zhuye',path: '/home',component: Home,// 嵌套路由配置children: [{name: 'xinwen',path: 'news',component: News},{name: 'xiaoxi',path: 'message',component: Message}]},{path: '/about',component: About}] })
-
命名路由:使用路由配置的
name
配置项为当前路由路径指定别名以简化路由路径的书写<!-- 简化前:需要书写路由完整路径 --> <route-link to="/home/message">消息</route-link><!-- 简化后:通过路由别名简化路径 --> <route-link :to="{name: 'xiaoxi'}">消息</route-link>
5.4 路由参数
-
路由的
query
参数:用于在路由路径后传递参数(key=val&key=val
)-
src/router/index.js
:配置嵌套路由import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home"; import Message from "@/pages/Message"; import News from "@/pages/News"; import Detail from "@/pages/Detail";export default new VueRouter({routes: [{path: '/about',component: About},{path: '/home',component: Home,children: [{path: 'message',component: Message,children: [{path: 'detail',component: Detail}]},{path: 'news',component: News}]}] })
-
src/pages/Home.vue
:可路由到News.vue
或Message.vue
<template><div><h2>Home 组件内容</h2><div><ul class="nav nav-tabs"><li><router-link class="list-group-item" active-class="active" to="/home/news">News</router-link></li><li><router-link class="list-group-item" active-class="active" to="/home/message">Message</router-link></li></ul><router-view></router-view></div></div> </template><script> export default {name: "Home" } </script>
-
src/pages/Message.vue
:点击消息项可展示详情组件Detail.vue
<template><div><ul><li v-for="m in messages" :key="m.id"><!-- 方式一:数据绑定 + 模板字符串 --><router-link :to="`/home/message/detail?id=${m.id}&title=${m.title}`">{{ m.title }}</router-link><!-- 方式二:to 的对象写法 --><!--<router-link:to="{ path: '/home/message/detail',query: {id: m.id,title: m.title }}">{{ m.title }}</router-link>--></li></ul><hr/><router-view></router-view></div> </template><script> export default {name: "Message",data() {return {messages: [{id: 1, title: 'm1'},{id: 2, title: 'm2'},{id: 3, title: 'm3'}]}} } </script>
-
src/pages/Detail.vue
:接收query
路由参数,展示消息详情<template><div><ul><!-- 通过 VC 配置的 $route 信息读取 query 参数 --><li>消息编号:{{ $route.query.id }}</li><li>消息内容:{{ $route.query.title }}</li></ul></div> </template><script> export default {name: "Detail" } </script>
-
-
路由的
params
参数:路由配置时使用占位符为参数占位,传递参数时使用RESTful
风格-
src/router/index.js
:在路由路径中使用占位符为参数占位import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home"; import Message from "@/pages/Message"; import News from "@/pages/News"; import Detail from "@/pages/Detail";export default new VueRouter({routes: [{path: '/home',component: Home,children: [{path: 'news',component: News},{path: 'message',component: Message,children: [{name: 'xiangqing',// 占位符,声明路径参数占位path: 'detail/:id/:title',component: Detail}]}]},{path: '/about',component: About}] })
-
src/pages/Message.vue
:使用RESTful
风格传递参数<template><div><ul><li v-for="m in messages" :key="m.id"><!-- 方式一:数据绑定 + 模板字符串 --><router-link :to="`/home/message/detail/${m.id}/${m.title}`">{{ m.title }}</router-link><!-- 方式二:to 的对象写法,必须使用路由配置的 name 别名,而不能使用 path --><!--<router-link:to="{ name: 'xiangqing', params: {id: m.id,title: m.title }}">{{ m.title }}</router-link>--></li></ul><hr/><router-view></router-view></div> </template><script> export default {name: "Message",data() {return {messages: [{id: 1, title: 'm1'},{id: 2, title: 'm2'},{id: 3, title: 'm3'}]}} } </script>
-
src/pages/Detail.vue
:接收params
参数,展示详情<template><div><ul><!-- 通过 VC 配置的 $route 信息读取 params 参数 --><li>消息编号:{{ $route.params.id }}</li><li>消息内容:{{ $route.params.title }}</li></ul></div> </template><script> export default {name: "Detail" } </script>
-
-
路由的
props
配置:让路由组件更方便地接收参数-
src/router/index.js
:通过props
配置项接收路由参数,方便当前组件通过 props 获取传递的参数import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home"; import Message from "@/pages/Message"; import News from "@/pages/News"; import Detail from "@/pages/Detail";export default new VueRouter({routes: [{path: '/home',component: Home,children: [{path: 'news',component: News},{path: 'message',component: Message,children: [{name: 'xiangqing',path: 'detail/:id/:title',component: Detail,// 方式一(对象式):将所有 key-val 通过 props 传递给 Detail 组件// props: {id: 123, title: 'Spring-_-Bear'},// 方式二(布尔值):取值为 true 则将路由收到的所有 `params` 参数通过 props 传递给 Detail 组件// props: true,// 方式三(函数式):返回的每一组 key-val 通过 props 传递给 Detail 组件props($route) {return {id: $route.params.id,title: $route.params.title}}}]}]},{path: '/about',component: About}] })
-
src/pages/Details.vue
:通过props
接收本路由组件中定义的属性<template><div><ul><li>消息编号:{{ id }}</li><li>消息内容:{{ title }}</li></ul></div> </template><script> export default {name: "Detail",props: ['id', 'title'] } </script>
-
5.5 编程式路由导航
-
两种浏览器页面跳转记录模式:默认为 push
- push 模式:新纪录压栈
- replace 模式:新纪录替换栈顶元素
-
router-link
的replace
和push
:控制路由跳转时操作浏览器的页面跳转记录模式<router-link replace></router-link>
-
编程式路由导航实现页面跳转:
<template><div><div class="row"><div class="col-xs-offset-2 col-xs-8"><div class="page-header"><h2>Vue Router Demo</h2></div><button @click="back">后退</button><button @click="forward">前进</button><button @click="go">任意跳转</button><button @click="pushView">push to detail</button><button @click="replaceView">replace to detail</button></div></div><div class="row"><div class="col-xs-2 col-xs-offset-2"><div class="list-group"><!-- SPA 实现页面切换,可使用 push 或 replace 属性控制跳转记录模式 --><router-link active-class="active" class="list-group-item" to="/home">Home</router-link><router-link active-class="active" class="list-group-item" to="/about">About</router-link></div></div><div class="col-xs-6"><div class="panel"><div class="panel-body"><!-- 展示组件 --><router-view></router-view></div></div></div></div></div> </template><script> export default {name: "App",methods: {back() {this.$router.back();},forward() {this.$router.forward();},go() {// 参数:正数表示前进,负数表示前进this.$router.go(-1);},pushView() {this.$router.push({path: '/home/message/detail',query: {id: '1',title: 'spring'}})},replaceView() {this.$router.replace({path: '/home/message/detail',query: {id: '2',title: 'bear'}})}} } </script><style> button {margin: 5px; } </style>
5.6 缓存路由组件
通过切换隐藏的路由组件,默认立即销毁,需要展示时再挂载。可以通过配置修改销毁规则
<!-- 切换页面时对 include 中指定的组件进行缓存,不销毁 -->
<keep-alive include="News"><router-view></router-view>
</keep-alive><!-- 缓存多个路由组件,页面切换时不销毁 -->
<keep-alive :include="['News', 'Message']"><router-view></router-view>
</keep-alive>
5.7 路由生命周期钩子
<script>
export default {name: "News",data() {return {opacity: 1}},// 当前路由组件激活时回调此钩子activated() {this.timer = setInterval(() => {this.opacity -= 0.01;console.log(this.opacity)if (this.opacity <= 0) {this.opacity = 1;}}, 20);},// 当前路由组件失活时回调此钩子deactivated() {console.log('deactivated')clearInterval(this.timer)},
}
</script>
5.8 路由守卫
-
全局路由守卫:
src/router/index.js
import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home";const router = new VueRouter({routes: [{path: '/home',component: Home,meta: {title: '主页'}},{path: '/about',component: About,meta: {isAuth: true,title: '关于'}}] })// 全局前置路由守卫 router.beforeEach((to, from, next) => {// 是否需要权限检查if (to.meta.isAuth) {if (localStorage.getItem('username') !== 'root') {alert('权限不足,无权查看')} else {next()}} else {next();}});// 全局后置路由守卫 router.afterEach((to, from) => {// 切换页面标题名称if (to.meta.title) {document.title = to.meta.title} else {document.title = '主页'} });export default router
-
独享路由守卫:
src/router/index.js
import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home";export default new VueRouter({routes: [{path: '/home',component: Home,meta: {title: '主页'}},{path: '/about',component: About,meta: {isAuth: true,title: '关于'},// 独享路由守卫只有前置,没有后置beforeEnter(to, from, next) {if (localStorage.getItem('username') !== 'root') {alert('权限不足,无权查看')} else {next()}}}] })
-
组件内路由守卫:
<script> export default {name: "Message",// 组件路由守卫,进入该组件之前调用beforeRouteEnter(to, from, next) {if (localStorage.getItem('username') !== 'root') {alert('权限不足,无权查看')} else {next()}},// 组件路由守卫,离开该组件之前调用beforeRouteLeave(to, from, next) {} } </script>
5.9 history 与 hash
- hash 模式:浏览器地址栏
#
及其之后的内容都是 hash 值,不会包含在 HTTP 请求路径中- 地址中永远带着 # 号,不美观
- 兼容性较好
- 将地址通过第三方 app 分享,若 app 检验严格,则地址会被标记为不合法
- history 模式:
- 地址干净,美观
- 兼容性相较略差
- 部署上线时需要后端解决 SPA 应用页面刷新时cun’zai
六、Vue3
- vue3 简介
- 使用 vue-cli 创建工程
- 使用 vite 创建工程
- 分析工程结构
- 安装开发者工具
- 初识 setup
- ref 函数 - 处理基本类型
- ref 函数 - 处理对象类型
- reactive 函数
- 回顾 vue2 的响应式原理
- Vue3 响应式原理 - Proxy
- Vue3 响应式原理 - Reflect
- reactive 对比 ref
- setup 的两个注意点
- computed 计算属性
- watch 监视 ref 定义的数据
- watch 监视 reactive 定义的数据
- watch 时的 value 的问题
- watchEffect 函数
- vue3 生命周期
- 自定义 hook
- toRef 与 toRefs
- shallowReactive 与 shallowRef
- readonly 与 shallowReadonly
- toRaw 与 markRaw
- customRef
- provide 与 inject
- 响应式数据的判断
- CompositionAPI 的优势
- Fragment 组件
- Teleport 组件
- Suspense 组件
- vue3 中的其它改变
[Study]Vue
文章目录
- 一、基础语法
- 1.1 概述
- 1.2 入门案例
- 1.3 el 与 data
- 1.4 MVVM
- 1.5 数据代理
- 1.6 事件处理
- 1.7 计算属性
- 1.8 监视属性
- 1.9 绑定样式
- 1.10 常用指令
- 1.11 自定义指令
- 1.12 列表数据处理
- 1.13 表单数据收集
- 1.14 过滤器
- 1.15 :key 作用与原理
- 1.16 数据监测原理
- 1.17 生命周期
- 二、组件化
- 2.1 概述
- 2.2 组件的定义和使用
- 2.3 VueComponent
- 三、脚手架
- 3.1 开发环境配置
- 3.2 render 函数
- 3.3 ref 引用
- 3.4 props 配置(组件通信)
- 3.5 mixin 混入
- 3.6 plugin 插件
- 3.7 localStorage
- 3.8 组件自定义事件(组件通信)
- 3.9 全局事件总线(组件通信)
- 3.10 消息订阅与发布(组件通信)
- 3.11 $nextTick
- 3.12 动画与过渡
- 3.13 TodoList 案例
- 3.14 配置代理
- 3.15 GitHub 案例
- 3.16 slot 插槽(组件通信)
- 四、vuex 插件
- 4.1 概述(组件通信)
- 4.2 搭建 Vuex 环境
- 4.3 求和案例
- 4.4 vuex 中的四种 map
- 4.5 vuex 模块化
- 五、vue-router 插件
- 5.1 概述
- 5.2 路由基本使用
- 5.3 嵌套与命名路由
- 5.4 路由参数
- 5.5 编程式路由导航
- 5.6 缓存路由组件
- 5.7 路由生命周期钩子
- 5.8 路由守卫
- 5.9 history 与 hash
- 六、Vue3
一、基础语法
1.1 概述
- Vue:一套用于构建用户界面的渐进式 JavaScript 框架。何为渐进式?是指 Vue 可以自底向上逐层地应用,也即按需引入 Vue 的各种组件
- Vue 的发展历程:2013(0.6.0)、2014(0.8.0)、2015(1.0.0)、2016(2.0.0)、2020(3.0.0)
- Vue 的特点:
- 采用组件化模式,提高代码复用率,易于维护
- 声明式编码,无需直接操作 DOM,提高开发效率
- 使用虚拟 DOM + 优秀的
Diff
算法,尽量复用 DOM 节点
1.2 入门案例
- 容器与 Vue 实例的关系:容器与 Vue 实例是一对一的关系,真实开发环境中只有一个 Vue 实例,并且会配合着组件一起使用
- 插值语法
{{}}
:用于解析标签体内容,{{xxx}} 插值语法中 xxx 要写 js 表达式,且 xxx 可以自动读取到data
中的所有属性,一旦 data 中的数据发生改变,那么页面中用到该数据的地方都会自动解析并更新 - 指令语法:用于解析标签属性,如
v-bind
等 - 数据绑定:
- 单向数据绑定(
v-bind
):v-bind:
可简写为:
- 双向数据绑定(
v-model
):只能用于表单类元素,v-model:value
可以简写为v-model
,因为 v-model 默认收集的就是表单元素的 value 值
- 单向数据绑定(
<!DOCTYPE html>
<html lang="zh-cn">
<head><meta charset="UTF-8"><title>Vue 入门案例</title><script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root"><!-- 插值语法 --><h1>Hello, {{ name.toUpperCase() }}</h1><!-- 指令语法 --><a v-bind:href="url">百度一下</a><hr/><!-- 数据绑定 -->单向数据绑定(data -> 页面):<input type="text" v-bind:value="name"/><br/>双向数据绑定(data <-> 页面):<input type="text" v-model:value="name">
</div><script type="text/javascript">// 阻止 vue 在启动时生成生产提示Vue.config.productionTip = falsenew Vue({// el 即 element 指明当前 vue 实例为哪个容器服务el: '#root',// data 用于存储数据,供 el 指定的容器使用data: {name: 'Spring-_-Bear',url: ''}})
</script>
</body>
</html>
1.3 el 与 data
-
el 的两种写法:
// el 的第一种写法 new Vue({el: '#root' })// el 的第二种写法 const vm = new Vue({}) vm.$mount('#root')
-
data 的两种写法:
-
对象式:
data: {name: 'Spring-_-Bear' }
-
函数式:由 Vue 管理的函数不能写为箭头函数,否则 this 指向存在问题
// 函数式完整写法 data: function () {return {name: 'Spring-_-Bear'} } // 函数式简写写法 data() {return {name: 'Spring-_-Bear'} }
-
1.4 MVVM
-
MVVM:
- M(Model):模型,对应 data 数据
- V(View):视图,对应模板代码
- VM(ViewModel):视图模型,对应 Vue 实例对象
-
vm:data 中所有的属性最终都出现在了视图模型 vm 身上,vm 身上所有的属性以及 Vue 原型上的所有属性在模板中都可以直接使用
<div id="root"><h1>{{ name }}</h1><!-- vm 的 _data 属性,对应数据 data --><h1>{{ _data }}</h1><!-- Vue 的原型属性 $delete --><h1>{{ $delete }}</h1> </div><script type="text/javascript">Vue.config.productionTip = falseconst vm = new Vue({el: '#root',data: {name: 'Spring-_-Bear'}})console.log(vm) </script>
1.5 数据代理
-
Object.defineProperty
即为实现数据代理的基本原理:<script type="text/javascript">let number = 18;let person = {name: 'Spring-_-Bear',sex: '男'}Object.defineProperty(person, 'age', {// value: 18, // 设置 person 对象的 age 属性值为 18// enumerable: true, // 控制属性是否可以被遍历,默认 false// writable: true, // 控制属性是否可以被修改,默认 false// configurable: true, // 控制属性是否可以被删除,默认 false// 当读取 person.age 时,get 函数自动调用,且返回值就是 person.age 的值get: function () {return number;},// 当修改 person.age 时,set 函数自动调用,且会收到具体修改的值set(val) {number = val;}}) </script>
-
数据代理:通过一个对象代理对另一个对象的属性操作
<script type="text/javascript">let obj1 = {age: 18};let obj2 = {};// obj2 代理 obj1 的属性操作Object.defineProperty(obj2, 'age', {get() {return obj1.age;},set(val) {obj1.age = val;}}) </script>
-
Vue 数据代理的基本原理:通过 vm 对象来代理 data 对象中属性的操作,如此可以更加方便地操作 data 中的数据
- 复制 data:将 data 对象中的属性复制到
vm._data
对象中 - _data 到 vm:通过
Object.defineProperty()
把vm._data
对象中所有属性添加到 vm 上 - getter/setter:为每一个添加到 vm 上的属性都指定一个
getter/setter
,在getter/setter
方法内部操作 data 对应的属性
- 复制 data:将 data 对象中的属性复制到
1.6 事件处理
-
事件绑定:使用
v-on:
或@
简写形式绑定对应的事件:<div id="root"><!-- v-on:click:不传递参数 --><button v-on:click="clickMe">点我呀(不传参)</button><!-- @click:传递参数 --><button @click="sayHello($event, 'Hello World')">说你好(传递参数)</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',/** 受 Vue 管理的函数不要写成箭头函数,否则 this 对象为 Window 而非 vm*/methods: {clickMe(event) {// 事件对象即 button 按钮元素console.log(event.target.innerText)},sayHello(event, content) {// this -> vmconsole.log(this)alert(content)}}}) </script>
-
事件修饰符:
-
prevent:阻止事件的默认行为
<a @click.prevent="sayHello" href="">走,百度两下</a>
-
stop:阻止事件冒泡
<div @click="sayHello"><!-- 先阻止事件冒泡,后阻止事件默认行为 --><button @click.stop.prevent="sayHello">点我呀</button> </div>
-
once:事件只触发一次
<button @click.once="sayHello">点击只生效一次哦</button>
-
capture:使用事件的捕获行为
<div @click.capture="sayHello('div1')" style="width: 100px; height: 100px; background: red">div1<!-- 点击 div2 时总是先输出 div1 再输出 div2 --><div @click="sayHello('div2')" style="width: 50px; height: 50px; background: skyblue">div2</div> </div>
-
self:只有
event.target
是当前操作的元素才触发事件<div @click.self="sayHello" style="height: 500px; width: 500px; background: red"><!-- 点击 button 时 div 的 click 事件不会响应 --><button @click="sayHello">点我呀</button> </div>
-
passive:事件的默认行为立即执行,无需等待事件回调执行完毕
<div id="root"><!-- 滚动条滑动事件 --><ul @scroll="msg" style="height: 200px; width: 200px; background: red; overflow: auto"><li style="height: 100px">1</li><li style="height: 100px">2</li><li style="height: 100px">3</li><li style="height: 100px">4</li><li style="height: 100px">5</li></ul><!-- 鼠标滚轮滚动事件,加上 passive 事件修饰符后立即滑动滑动条,无需等待 msg 回调执行完成 --><ul @wheel.passive="msg" style="height: 200px; width: 200px; background: red; overflow: auto"><li style="height: 100px">1</li><li style="height: 100px">2</li><li style="height: 100px">3</li><li style="height: 100px">4</li><li style="height: 100px">5</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',methods: {msg() {for (let i = 0; i < 100000; i++) {console.log('bear')}}}}) </script>
-
-
键盘事件:
-
Vue 中提供的按键别名,可直接用于绑定按键事件:
enter、delete、esc、space、tab、up、down、left、right
<div id="root">enter:<input type="text" @keyup.enter="show"/><br/><!-- Tab 键的功能是从当前元素失焦,较为特殊,需绑定 @keydown 按下事件 -->tab:<input type="text" @keydown.tab="show" placeholder="绑定 @keydown 事件"> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',methods: {show(e) {console.log(e.key, e.keyCode)}}}) </script>
-
Vue 未提供别名的按键,可以使用按键原始名称绑定,但多英文时注意要转为英文小写加短横线连接方式如
caps-lock
大小写切换提示:<input type="text" @keyup.caps-lock="show"/>
-
系统修饰键用法特殊:
ctrl、alt、shift、meta
(即 win 键)-
配置
@keydown
使用时:正常触发事件 -
配合
@keyup
使用时:按下修饰键的同时按下其它键,释放其它键时事件才被触发Ctrl + Y 释放 Y 时触发事件:<input type="text" @keyup.ctrl="show">
-
-
1.7 计算属性
-
姓名案例使用函数方式实现:
<div id="root">姓:<input type="text" v-model="firstName"><br>名:<input type="text" v-model="lastName"><br>全名:<span>{{ fullName() }}</span> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {firstName: '张',lastName: '三'},methods: {fullName() {return this.firstName + '-' + this.lastName}}}) </script>
-
姓名案例使用计算属性实现:
computed
计算属性与methods
方法实现相比有读取缓存机制,效率更高、调试方便<div id="root">姓:<input type="text" v-model="firstName"><br>名:<input type="text" v-model="lastName"><br>全名:<span>{{ fullName }}</span> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {firstName: '张',lastName: '三'},computed: {fullName: {/** get() 调用时机:* 1. 初次读取 fullName 属性时* 2. fullName 所依赖的数据发生变化时*/get() {return this.firstName + '-' + this.lastName;},/** set(val) 调用时机:fullName 被修改时*/set(val) {let arr = val.split('-');this.firstName = arr[0];this.lastName = arr[1];}}}}) </script>
-
姓名案例计算属性的简写形式:
computed: {// 当计算属性只读不改时可使用以下简写方式fullName() {return this.firstName + '-' + this.lastName;} }
1.8 监视属性
-
监视属性的第一种配置方式
watch
:<div id="root"><h1>今日天气:{{ weather }}</h1><button @click="isHot = !isHot">变天咯</button> </div><script type="text/javascript">Vue.config.productionTip = falseconst vm = new Vue({el: '#root',data: {isHot: true},computed: {weather() {return this.isHot ? '炎热' : '凉爽';}},watch: {// 监视数据isHot: {// immediate: true 初始化完成立即监视一次 isHot 的变化immediate: true,handler(newValue, oldValue) {console.log(newValue, oldValue)}},// 监视计算属性weather: {handler(newValue, oldValue) {console.log(newValue, oldValue)}}}}) </script>
-
监视属性的第二种配置方式
vm.$watch()
:// 第二种监视方式 vm.$watch('isHot', {handler(newValue, oldValue) {console.log(newValue, oldValue)} })
-
监视属性的简写形式:当监视的属性不需要其它配置项时可简写 handler 函数
watch: {isHot(newValue, oldValue) {console.log(newValue, oldValue)} }
-
监视属性的深度监视配置:
<div id="root"><h1>a = {{ numbers.a }}</h1><button @click="numbers.a++">a++</button><h1>d = {{ numbers.b.c.d }}</h1><button @click="numbers.b.c.d++">d++</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {numbers: {a: 1,b: {c: {d: 1}}}},watch: {// 监视多级数据结构中某个属性的变化'numbers.a': {handler(newVal, oldVal) {console.log('a: ' + oldVal + ' -> ' + newVal)}},// 监视多级数据结构中所有属性的变化numbers: {// 开启深度监视deep: true,handler() {console.log('numbers 发生了变化')}}}}) </script>
-
监视属性对比计算属性:computed 能完成的功能 watch 都可以完成,watch 能完成的功能 computed 不一定能完成,如 watch 可以进行异步操作等
<div id="root">姓:<input type="text" v-model="firstName"><br>名:<input type="text" v-model="lastName"><br>全名:<span>{{ fullName }}</span> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {firstName: '张',lastName: '三',fullName: '张-三'},watch: {firstName(newVal) {this.fullName = newVal + '-' + this.lastName;},lastName(newVal) {/** 所有被 Vue 管理的函数最好写成普通函数,这样 this 的指向才是 vm 或组件实例对象* 所有不被 Vue 管理的函数(定时器的回调函数、ajax 的回调函数、Promise 的回调函数等)最好写成箭头函数,这样 this 的指向才是 vm 或组件实例对象*/setTimeout(() => {this.fullName = this.firstName + '-' + newVal;}, 3000);}}}) </script>
1.9 绑定样式
-
绑定 class 样式之字符串写法:适用于样式的类名不确定,需要动态绑定
<div id="root"><!-- 绑定 class 样式:字符串写法 --><div class="basic" :class="changeMood">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',mood: 'happy',},methods: {changeMood() {const arr = ['happy', 'sad', 'normal'];this.mood = arr[Math.floor(Math.random() * 3)]}}}) </script>
-
绑定 class 样式之数组写法:适用于要绑定的样式个数不确定,类名也不确定
<div id="root"><!-- 绑定 class 样式:数组写法 --><div class="basic" :class="classArr">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',mood: 'happy',classArr: ['sad', 'happy', 'funny']}}) </script>
-
绑定 class 样式之对象写法:适用于要绑定的样式个数确定、类名也确
<div id="root"><!-- 绑定 class 样式:对象写法 --><div class="basic" :class="classObj">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',mood: 'happy',classObj: {sad: true,happy: false}}}) </script>
-
绑定 style 样式之对象写法:
<div id="root"><!-- 绑定 style 样式:对象写法 --><div style="padding: 10px" :style="styleObj">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',styleObj: { fontSize: '40px' }},}) </script>
-
绑定 style 样式之数组对象写法:
<div id="root"><!-- 绑定 style 样式:数组对象写法 --><div style="padding: 10px" :style="styleArrObj">{{ name }}</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {name: 'Spring-_-Bear',styleArrObj: [{fontSize: '40px'},{backgroundColor: 'red'}]},}) </script>
1.10 常用指令
-
条件渲染指令:
v-show
:控制元素的display
属性从而实现元素展示与隐藏v-if
:从document
中增、删对应的 DOM 元素
<div id="root"><h1>n = {{ n }}</h1><button @click="n++">n++</button><!-- v-show:display none --><div v-show="n === 3">n == 3</div><!-- v-if:增、删节点 --><div v-if="n < 1">n < 1</div><div v-else-if="n >= 1 && n < 5">1 <= n < 5</div><div v-else>n >= 5</div><!-- template 只能与 v-if 配合使用 --><template v-if="n === 1"><h1>北京</h1><h1>欢迎</h1><h1>您</h1></template> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {n: 1}}) </script>
-
列表渲染指令
v-for
:<div id="root"><h1>v-for 遍历数组</h1><ul><!-- in 也可以换为 of --><li v-for="(item,index) in persons" :key="item.id">{{ index }}-{{ item.name }}-{{ item.age }}</li></ul><h1>v-for 遍历对象</h1><ul><li v-for="(val,key,index) in car" :key="index">{{ key }} : {{ val }}</li></ul><h1>v-for 遍历数字</h1><ul><li v-for="(val,index) in 5" :key="index">{{ index }} : {{ val }}</li></ul><h1>v-for 遍历字符串</h1><ul><li v-for="(val,index) in 'HelloWorld'" :key="index">{{ index }} : {{ val }}</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {persons: [{id: '001', name: '张三', age: 3},{id: '002', name: '李四', age: 4},{id: '003', name: '王五', age: 5},],car: {name: '奥迪',color: '红色',price: '50w'}}}) </script>
-
元素文本指令
v-text
:<div id="root"><!-- 以下两种方式等价,均将 name 解析为 div 中的文本值 --><div>{{name}}</div><div v-text="name"></div> </div>
-
内置 html 结构指令
v-html
:v-html 存在安全性问题,在网站上动态渲染任意 HTML 都是非常危险的行为,容易导致 XSS 攻击<div id="root"><div v-text="'<h1>Hello World!</h1>'"></div><div v-html="'<h1>Hello World!</h1>'"></div><div v-html="str"></div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {// XSS 攻击示例str: '<a href=javascript:location.href="?"+document.cookie>兄弟快来,这儿有好东西!</a>'},}) </script>
-
元素渲染一次指令
v-once
:v-once 修饰的节点在初次动态渲染完成后就视为静态内容,不再发生变化,可以用于性能优化<div id="root"><!-- 第一次读取 n 值后不再发生变化 --><div v-once>初始值 n = {{ n }}</div><div>现在值 n = {{ n }}</div><button @click="n++">n++</button> </div>
-
免编译指令
v-pre
:v-pre 指令修饰的节点 Vue 不进行编译,可加快页面加载速度<div id="root"><div v-pre>初始值 n = {{ n }}</div><div>现在值 n = {{ n }}</div><button @click="n++">n++</button> </div>
-
v-clock
指令:v-cloak 指令配合 css 样式可以解决因网速慢导致页面展示出 {{xxx}} 的问题<!DOCTYPE html> <html lang="zh-cn"> <head><meta charset="UTF-8"><title>Title</title><script type="text/javascript" src="../js/vue.js"></script><style>[v-cloak] {display: none;opacity: 100%;}</style> </head> <body> <div id="root"><div><h1>姓名:</h1><!-- new Vue 实例创建完成并接管 #root 容器时,v-cloak 属性会被自动删除 --><h1 v-cloak>{{ name }}</h1></div> </div> </body> </html>
1.11 自定义指令
-
函数式自定义局部指令:自定义指令函数中的
this -> Window
<div id="root"><div>n = {{ n }}</div><!-- 通过自定义指令实现 n *= 10 --><div>n * 10 = <span v-multiply="n"></span></div><button @click="n++">n++</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {n: 1},// 自定义局部指令directives: {/** multiply 函数何时被调用:* 1. 指令与函数成功绑定时* 2. 指令所在的模板被重新解析时*/multiply(element, binding) {/* 此处的 this 指向 Window */element.innerText = binding.value * 10;}}}) </script>
-
对象式自定义局部指令:自定义指令函数中的
this -> Window
<div id="root"><div v-text="num"></div><button @click="num++">num++</button><!-- 通过自定义指令实现页面加载完成时自动聚焦 --><input type="text" v-myfocus:value="num"> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {num: 1},// 自定义局部指令directives: {myfocus: {// 指令与函数成功绑定时调用bind(element, binding) {element.value = binding.value},// 元素被插入到页面后调用inserted(element, binding) {element.focus()},// 指令所在的模板被重新解析时调用update(element, binding) {element.value = binding.value}}}}) </script>
-
自定义全局指令,可供多个 Vue 实例使用:
<div id="root"><div>n = {{ n }}</div><!-- 通过自定义全局指令实现 n *= 10 --><div>n * 10 = <span v-multiply="n"></span></div><button @click="n++">n++</button> </div><script type="text/javascript">Vue.config.productionTip = false// 自定义全局指令,可供多个 Vue 实例使用Vue.directive('multiply', function (element, binding) {element.innerText = binding.value * 10;});new Vue({el: '#root',data: {n: 1}}) </script>
1.12 列表数据处理
-
列表数据过滤之监视属性实现:
<div id="root"><input type="text" v-model="keyWord" placeholder="请输入关键字"><ul><li v-for="p in filterPersons" :key="p.id">{{ p.name }}</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {keyWord: '',persons: [{id: '001', name: '马冬梅'},{id: '002', name: '周冬雨'},{id: '003', name: '周杰伦'},{id: '004', name: '蔡家伦'}],// 存储过滤后的 persons 信息filterPersons: []},watch: {keyWord: {// 加载完成就立即监视一次以显示数据immediate: true,handler(newVal) {this.filterPersons = this.persons.filter((p) => {return p.name.indexOf(newVal) !== -1;});}}}}) </script>
-
列表数据过滤之计算属性实现:
<div id="root"><input type="text" v-model="keyWord" placeholder="请输入关键字"><ul><li v-for="p in filterPersons" :key="p.id">{{ p.name }}</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {keyWord: '',persons: [{id: '001', name: '马冬梅'},{id: '002', name: '周冬雨'},{id: '003', name: '周杰伦'},{id: '004', name: '蔡家伦'}]},computed: {filterPersons() {return this.persons.filter((p) => {return p.name.indexOf(this.keyWord) !== -1;});}}}) </script>
-
列表数据排序:
<div id="root"><input type="text" v-model="keyWord" placeholder="请输入关键字"><button @click="sortType = 0">原顺序</button><button @click="sortType = 1">年龄升序</button><button @click="sortType = 2">年龄降序</button><ul><li v-for="p in filterPersons" :key="p.id">姓名:{{ p.name }} 年龄:{{ p.age }}</li></ul> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {keyWord: '',sortType: 0,persons: [{id: '001', name: '马冬梅', age: 12},{id: '002', name: '周冬雨', age: 5},{id: '003', name: '周杰伦', age: 34},{id: '004', name: '蔡家伦', age: 21}]},computed: {filterPersons() {const resArr = this.persons.filter((p) => {return p.name.indexOf(this.keyWord) !== -1;});// 对过滤后的结果进行排序if (this.sortType) {resArr.sort((p1, p2) => {return this.sortType === 1 ? p1.age - p2.age : p2.age - p1.age;});}return resArr;}}}) </script>
1.13 表单数据收集
<div id="root"><!-- prevent 阻止表单的默认提交行为 --><form @submit.prevent="register"><!-- trim 修饰符去除字符串前后空格 -->账户:<input type="text" v-model.trim="user.account"><br/><br/>密码:<input type="password" v-model.trim="user.password"><br/><br/><!-- number 修饰符将输入内容转换为数字 -->年龄:<input type="number" v-model.number="user.age"><br/><br/>性别:<input type="radio" name="sex" value="male" v-model="user.sex">男<input type="radio" name="sex" value="female" v-model="user.sex">女<br/><br/>爱好:<input type="checkbox" value="programming" v-model="user.hobbies">编程<input type="checkbox" value="read" v-model="user.hobbies">阅读<input type="checkbox" value="sleep" v-model="user.hobbies">睡觉<br/><br/>校区:<select v-model="user.school"><option value="请选择校区">请选择校区</option><option value="yu">余家头</option><option value="ma">马房山</option><option value="nan">南湖</option></select><br/><br/><!-- lazy 修饰符意为懒加载即当前元素失焦后收集 -->其它:<textarea v-model.lazy="user.comments"></textarea><br/><br/><input type="checkbox" v-model="user.agree"> 阅读并同意 <a href="">《用户协议》</a><br/><br/><button>提交</button></form>
</div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {user: {account: '',password: '',age: '',sex: 'male',hobbies: [],school: '请选择校区',comments: '',agree: false}},methods: {register() {console.log(JSON.stringify(this.user))}}})
</script>
1.14 过滤器
Vue 过滤器的功能是对要显示的数据进行格式化后,其并没有改变原本的数据,只是产生新的对应的数据。过滤器并不是必须要用的东西,它只是 Vue 给我们提供的新的数据处理方式,过滤器能做到的,用计算属性、methods方法依然可以实现
<!-- <script src=".11.5/dayjs.min.js"></script> -->
<div id="root">计算属性实现 Datetime:{{ computedDatetime }} <br/><br/>方法实现 Datetime: {{ methodsDatetime(Date.now()) }} <br/><br/><!-- 插值语法中使用过滤器 -->过滤器实现 Datetime: {{ Date.now() | timeFormatter }} <br/><br/><!-- 数据绑定中使用过滤器 -->过滤器传参实现 Date: <span :text="Date.now() | timeFormatter('YYYY-MM-DD')"></span> <br/><br/>多级过滤器 Year:{{ Date.now() | timeFormatter('YYYY-MM-DD') | mySlice }}
</div><script type="text/javascript">Vue.config.productionTip = false// 全局过滤器,多个 Vue 实例可用Vue.filter('mySlice', function (val) {return val.slice(0, 4);});new Vue({el: '#root',data: {},computed: {computedDatetime() {return dayjs().format('YYYY-MM-DD HH:mm:ss')}},methods: {methodsDatetime(now) {return dayjs(now).format('YYYY-MM-DD HH:mm:ss')}},filters: {// 若调用 timeFormatter 过滤器时传入了 formatStr 则使用传入值,否则使用默认值timeFormatter(val, formatStr = 'YYYY-MM-DD HH:mm:ss') {return dayjs(val).format(formatStr)}}})
</script>
1.15 :key 作用与原理
-
:key
作用:用于标识当前元素的唯一性,利于 Vue 中元素 Diff 算法的比较<ul><!-- in 也可以替换为 of --><li v-for="(item,index) in persons" :key="item.id">{{ index }}-{{ item.name }}-{{ item.age }}</li> </ul>
-
:key
使用index
作为元素唯一标识容易产生的问题:- 当对 data 中的数据进行逆序添加、逆序删除等
破坏顺序
的操作时,会产生不必要的真实 DOM 更新,效率低 - 当页面结构中还存在输入类的 DOM 时,会产生错误的 DOM 更新,如下图所示:
<div id="root"><h1>v-for 遍历数组</h1><ul><li v-for="(item,index) in persons" :key="index">{{ index }}-{{ item.name }}-{{ item.age }} <input type="text"></li></ul><button @click="addPerson">添加</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {persons: [{id: '001', name: '张三', age: 3},{id: '002', name: '李四', age: 4},{id: '003', name: '王五', age: 5},]},methods: {addPerson() {const p = {id: '004', name: '赵六', age: 6}this.persons.unshift(p);}}}) </script>
- 当对 data 中的数据进行逆序添加、逆序删除等
-
Vue 中 :key 的内部原理:
- key 是虚拟 DOM 的唯一标识,当数据发生变化时, Vue 会根据新数据生成新的虚拟 DOM,随后进行新虚拟 DOM 与旧虚拟 DOM 的 Diff 差异比较
- 若旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key 且若内容未发生变化则使用之前的真实 DOM,否则根据新的虚拟 DOM 生成新的真实 DOM 并替换页面中真实 DOM
1.16 数据监测原理
-
模拟 Vue 监测对象数据 data:
<script type="text/javascript">let data = {'name': 'Spring-_-Bear','age': 18}// 创建一个监视实例对象,用于检测 data 的变化const obs = new Observer(data);let vm = {};vm._data = data = obs;function Observer(obj) {const keys = Object.keys(obj);// 遍历为每个 key 生成 getter 和 setterkeys.forEach((key) => {// this 指向 Observer 实例对象Object.defineProperty(this, key, {get() {console.log('Data got successfully')return obj[key];},set(newVal) {console.log('Data has changed!');obj[key] = newVal;}})});} </script>
-
vm.$set()
给对象追加属性以实现数据响应式监测:<div id="root"><h1>学生信息</h1><button @click="addSex('男')">添加学生性别</button><button v-if="student.sex" @click="student.sex = student.sex === '男'? '女' : '男'">修改学生性别</button><p>姓名:{{ student.name }}</p><p>年龄:{{ student.age }}</p><p v-if="student.sex">性别:{{ student.sex }}</p> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {student: {name: 'Spring-_-Bear',age: 22}},methods: {addSex(val) {this.$set(this.student, 'sex', val);}}}) </script>
-
Vue 监测数组数据变化:Vue 将被侦听的数组的
变更方法
进行了包裹,通过这些方法操作数组数据也会触发视图更新。这些被包裹过的方法包括:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
<div id="root"><h1>学生信息</h1><p>姓名:{{ student.name }}</p><p>年龄:{{ student.age }}</p><p>爱好如下:</p><ul><li v-for="(h,index) in student.hobbies" :key="index">{{ h }}</li></ul><!-- push()、pop()、shift()、unshift()、splice()、sort()、reverse() --><button @click="student.hobbies.splice(0, 1, '睡觉')">修改爱好</button> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {student: {name: 'Spring-_-Bear',age: 22,hobbies: ['学习', '钓鱼', '阅读']}},}) </script>
-
Vue 数据监测原理:会监视 data 中所有层次的数据
- 如何监测对象中的数据:通过 setter 实现数据监测,需要在创建 Vue 实例时就传入要监测的数据。对于对象中后追加的属性,Vue 默认不做响应式处理,若需实现响应式需使用如下 API:
Vue.set(target, propertyName/index, value)
vm.$set(target, propertyName/index, value)
Vue.set()
和vm.$set()
不能给 vm 或 data 追加属性
- 如何监测数组中的数据:通过包裹数组更新元素的方法实现监测数组中的数据
- 如何监测对象中的数据:通过 setter 实现数据监测,需要在创建 Vue 实例时就传入要监测的数据。对于对象中后追加的属性,Vue 默认不做响应式处理,若需实现响应式需使用如下 API:
1.17 生命周期
-
生命周期:又名生命周期回调函数、生命周期函数、生命周期钩子,是一些在关键时刻 Vue 调用的特殊名称的函数。生命周期函数的名字不可更改,但函数体内容由程序员实现,函数中的 this 对象指向 vm 或组件实例对象
-
mounted 挂载函数的基本使用:
<div id="root"><!-- 若隐若现 --><div :style="{opacity: opacity}">Spring-_-Bear 学 Vue</div> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',data: {opacity: 0},// Vue 完成模板的解析并把初始的真实 DOM 放入页面调用 mounted 函数,即挂载完成后调用mounted() {setInterval(() => {if (this.opacity > 1) {this.opacity = 0;}this.opacity += 0.01;}, 20);}}) </script>
-
生命周期流程图:
-
生命周期的注意事项:
destoryed
:销毁后借助 Vue 开发者工具看不到任何信息;自定义事件会失效,但原生 DOM 事件依然有效beforeDestroy
:一般不会在 beforeDestroy 中操作数据,即使操作了数据也不会触发更新流程
二、组件化
2.1 概述
-
组件:实现应用中局部功能代码和资源的集合
-
Vue 实例中
template
配置项的使用:<div id="root"><!-- 当从 template 中渲染页面时,<div id="root"> 中定义的属性将全部丢失 --><h1>Vue</h1> </div><script type="text/javascript">Vue.config.productionTip = falsenew Vue({el: '#root',template:`<div><div>Spring-_-Bear 学 Vue</div><span>Hello World!</span></div>`}) </script>
2.2 组件的定义和使用
-
组件定义和使用的注意事项:
- el 不能配置:因为所有的组件最终都要经过一个 vm 的管理,由 vm 中的 el 决定所有的组件服务于哪个容器
- data 必须写成函数:目的是避免组件被复用时数据间存在引用关系
- 组件名:
kebab-case
风格,如 my-schoolCamelCase
风格,如 MySchool(需要 Vue 脚手架支持)
- 组件使用:
- 双标签写法:例如
<school></school>
- 单标签写法:例如
<school/>
,不使用脚手架时会导致后续组件不能渲染
- 双标签写法:例如
- 组件定义的简写方式:
const school = Vue.extend(options)
=>const school = options
- 可以在定义组件时使用 name 配置项指定组件在 Vue 开发者工具中呈现的名字
-
非单文件组件的定义和使用:
<div id="root"><!-- 使用组件 --><school></school><hr/><student></student> </div> <hr/> <script type="text/javascript">Vue.config.productionTip = false// 定义 school 组件const school = Vue.extend({template:`<div><h1>学校名称:{{ schoolName }}</h1><h1>学校地址:{{ address }}</h1><button @click="showSchool">展示学校</button></div>`,data() {return {schoolName: 'WHUT',address: '湖北省武汉市'}},methods: {showSchool() {alert(this.schoolName);}}});// 定义 student 组件const student = Vue.extend({template:`<div><h1>学生姓名:{{ studentName }}</h1><h1>学生年龄:{{ age }}</h1></div>`,whadata() {return {studentName: 'Spring-_-Bear',age: 18}}});new Vue({el: '#root',components: {// 注册局部组件school: school,student: student}}) </script>
-
嵌套组件的定义和使用:
<div id="root"></div><script type="text/javascript">Vue.config.productionTip = false// 定义 student 组件const student = Vue.extend({template:`<div><h1>学生姓名:{{ studentName }}</h1><h1>学生年龄:{{ age }}</h1></div>`,data() {return {studentName: 'Spring-_-Bear',age: 18}}});// 定义 school 组件const school = Vue.extend({template:`<div><h1>学校名称:{{ schoolName }}</h1><h1>学校地址:{{ address }}</h1><student></student></div>`,data() {return {schoolName: 'WHUT',address: '湖北省武汉市'}},// 嵌套组件注册components: {student: student}});// 定义 hello 组件const hello = Vue.extend({template:`<div><h1>Hello World</h1></div>`})// 管理组件的组件 app 组件const app = Vue.extend({template: `<div><school></school><hello></hello></div>`,components: {// school 组件中使用 student 组件school: school,hello: hello}});new Vue({template: `<app></app>`,el: '#root',components: {app: app}}) </script>
-
全局组件的定义和使用:
<div id="root"><h1>root</h1><hello></hello> </div> <hr/> <div id="app"><h1>app</h1><hello></hello> </div><script type="text/javascript">Vue.config.productionTip = false// 注册全局组件Vue.component('hello', Vue.extend({template:`<div><h1>Hello World</h1></div>`}));new Vue({el: '#root'})new Vue({el: '#app'}) </script>
-
单文件组件的定义和使用:
-
main.js
:应用入口文件,注册并使用 App 组件import App from "./App";new Vue({el: 'root',template: `<App></App>`,components: {App} });
-
App.vue
:顶级组件,用于管理其它组件<template><div><school></school><student></student></div> </template><script> // 引入其它组件 import School from "./School"; import Student from "./Student";export default {name: "App",// 注册组件components: {Student, School} } </script>
-
School.vue
:实现学校相关功能<!-- 组件的结构 --> <template><div class="demo"><h1>学校名称:{{ name }}</h1><h1>学校地址:{{ address }}</h1><button @click="showSchool">show</button></div> </template><!-- 组件的行为 --> <script> // 暴露组件 export default {name: 'School',data() {return {name: '武汉理工大学',address: '湖北省武汉市'}},methods: {showSchool() {alert(this.name);}} } </script><!-- 组件的样式 --> <style> .demo {background-color: aqua; } </style>
-
Student.vue
:实现学生相关功能<!-- 组件的结构 --> <template><div><h1>姓名:{{ name }}</h1><h1>年龄:{{ age }}</h1></div> </template><!-- 组件的行为 --> <script> // 暴露组件 export default {name: 'Student',data() {return {name: 'Spring-_-Bear',age: 18}} } </script>
-
index.html
:引入vue.js
和main.js
<!DOCTYPE html> <html lang="zh-cn"> <head><meta charset="UTF-8"><title>首页</title> </head> <body> <div id="root"></div><script type="text/javascript" src="../js/vue.js"></script> <script type="text/javascript" src="main.js"></script> </body> </html>
-
2.3 VueComponent
-
组件:本质是一个名为
VueComponent
的构造函数,由Vue.extend
函数调用。当使用组件时,Vue 解析组件标签的过程中会生成组件的实例对象。每次调用 Vue.extend,返回的都是一个全新的 VueComponent -
this 对象:
- 在组件实例 vc 中:data 函数、methods 中的函数、watch 中的函数、computed 中的函数,this 指向 VueComponent 实例对象 vc
- 在 Vue 实例 vm 中:data 函数、methods 中的函数、watch 中的函数、computed 中的函数,this 指向 Vue 实例 vm
-
显式原型对象与隐式原型对象间的关系:
<script type="text/javascript">function Demo() {this.a = 1this.b = 2}const demo = new Demo();console.log('显式原型对象:', Demo.prototype);console.log('隐式原型对象:', demo.__proto__);// output: trueconsole.log(Demo.prototype === demo.__proto__)// 通过显式原型属性操作原型对象,追加一个 x 属性,值为 38,则 demo 身上也能直接看到 xDemo.prototype.x = 38;/* * 实例的隐式原型属性永远指向自己缔造者的原型对象*/console.log(demo.x); </script>
-
组件重要的内置关系:
VueComponent.prototype.__proto__ === Vue.prototype
,即让组件实例对象(vc)可以访问到 Vue 原型上的属性和方法
三、脚手架
3.1 开发环境配置
-
安装
Node.js
并配置淘宝镜像:npm config set registry
-
全局安装 Vue 脚手架:
npm install -g @vue/cli
-
创建 Vue 项目:
vue create projectName
-
启动 Vue 项目:
npm run serve
-
修改默认配置:在工程路径下的
vue.config.js
中修改默认配置,如关闭语法检查:const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({transpileDependencies: true,// 关闭语法检查lintOnSave: false })
3.2 render 函数
// 默认引入 vue/dist/vue.runtime.esm.js
import Vue from 'vue'
import App from './App.vue'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App)
})
vue.js
与 vue.runtime.xxx.js
的区别:
-
vue.js 是完整版的 Vue,包含核心功能和模板解析器
-
vue.runtime.xxx.js 是运行时版的 Vue,只包含核心功能,没有模板解析器,所以不能使用 template 配置项,需要使用 render 函数接收到的 createElement 函数去创建具体内容
render(createElement) {// 通过 render 函数接收到的 createElement 函数创建具体内容return createElement('h1', 'Hello World') }
3.3 ref 引用
ref 属性被用来给元素或子组件注册引用信息:
- 应用在 html 标签上获取的是真实的 DOM 元素
- 应用在组件标签上是组件实例对象(VueComponent)
<template><div><h1 ref="hello">Hello World</h1><button @click="show">Click me</button><School ref="school"/></div>
</template><script>
import School from "@/components/School";export default {name: "App",components: {School},methods: {show() {// 真实 DOM 元素console.log(this.$refs.hello);// 组件实例对象console.log(this.$refs.school)}}
}
</script>
3.4 props 配置(组件通信)
props 配置用于组件接收外部传递的数据,一般用于接收父组件所传递的数据。props 传递的数据是 只读的
,Vue 底层会检测对 props 传递数据的修改行为,若进行了修改则会在控制台发出警告
App.vue
:给子组件传递数据
<template><div><!-- 给组件传递数据 --><Student name="Spring-_-Bear" sex="男" :age="18"/></div>
</template><script>
import Student from "@/components/Student";export default {name: "App",components: {Student}
}
</script>
Student.vue
:使用 props 配置接受来自父组件的数据
<template><div class="school"><h1>{{ msg }}</h1><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1><h1>年龄:{{ copyAge }}</h1><button @click="copyAge++">age++</button></div>
</template><script>
export default {name: "Student",data() {return {msg: '我是清华大学的学生',// 拷贝 props 中传递的 age 用于修改,因为 props 中接收到的数据是只读的copyAge: this.age}},// 方式一(数组式):简单声明接收// props: ['name', 'sex', 'age']// 方式二(对象式):接收的同时对数据类型进行限制// props: {// name: String,// sex: String,// age, Number// }// 方式三:数据类型限制 + 默认值指定 + 必要性限制props: {name: {type: String,required: true},sex: {type: String,required: false},age: {type: Number,default: 99}}
}
</script><style scoped>
.school {background-color: gray;
}
</style>
3.5 mixin 混入
mixin 混入:用于组件间复用相同的逻辑和数据,混入就是把组件多次使用的属性和方法等内容进行封装
-
配置局部混入:
-
mixin.js
:抽取多个组件的共用逻辑和数据进行封装export const mixin = {methods: {showName() {alert(this.name)}},data() {return {msg: 'Welcome to learn Vue'}} }
-
School.vue
:通过mixins
配置项配置局部混入,可配置多个 mixin<template><div class="school"><h1>{{ msg }}</h1><h1 @click="showName">学校:{{ name }}</h1><h1>地址:{{ address }}</h1></div> </template><script> import {mixin} from "@/mixin";export default {name: "Student",data() {return {name: '武汉理工大学',address: '湖北省武汉市'}},// 配置局部混入mixins: [mixin] } </script><style scoped> .school {background-color: gray; } </style>
-
Student.vue
:通过mixins
配置项配置局部混入,可配置多个 mixin<template><div class="school"><h1>{{ msg }}</h1><h1 @click="showName">姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1></div> </template><script> import {mixin} from "@/mixin";export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},// 配置局部混入mixins: [mixin] } </script><style scoped> .school {background-color: gray; } </style>
-
-
配置全局混入:与配置全局自定义指令、全局过滤器类似
-
mixin.js
:抽取所有组件共有的逻辑和数据进行封装export const mixin = {methods: {showName() {alert(this.name)}},data() {return {msg: 'Welcome to learn Vue'}} }
-
main.js
:引入混入文件并使用Vue.mixin()
方法配置全局混入,可供所有组件使用import Vue from "vue"; import App from './App.vue' import {mixin} from "@/mixin";Vue.config.productionTip = false // 配置全局混入 Vue.mixin(mixin)new Vue({el: '#app',render: h => h(App) })
-
3.6 plugin 插件
插件:用于增强 Vue,本质是一个包含 install()
方法的对象。install 函数的第一个参数是 Vue,第二个参数是插件使用者所传递的数据
pulgins.js
:在 install 方法中配置当前插件所拥有的各种功能,如全局过滤器、全局自定义指令、全局混入等
import {mixin} from "@/mixin";export default {install(Vue, params) {console.log('接收到的参数:', params)// 全局过滤器Vue.filter('mySlice', function (val) {return val.slice(0, 4);});// 全局自定义指令Vue.directive('myfocus', {bind(element, binding) {element.value = binding.value},inserted(element, binding) {element.focus()},update(element, binding) {element.value = binding.value}})// 全局混入Vue.mixin(mixin)// 往 Vue 原型上添加方法Vue.prototype.hello = () => {alert("Hello World")};}
}
main.js
:引入并使用插件
import Vue from "vue";
import App from './App.vue'
import plugins from "@/plugins";Vue.config.productionTip = false
// 使用插件,并给插件传递消息
Vue.use(plugins, 'Used plugins in the main.js')new Vue({el: '#app',render: h => h(App)
})
3.7 localStorage
本地存储内容大小一般为 5M,因浏览器而异。通过 Window.localStorage
和 Window.sessionStorage
对象中的 API 来实现本地存储和会话存储
<div id="root"><button onclick="add()">本地存储:添加</button><button onclick="delItem()">本地存储:删除</button><button onclick="update()">本地存储:修改</button><button onclick="read()">本地存储:读取</button><button onclick="clearAll()">本地存储:清除</button>
</div><script type="text/javascript">// 会话存储对象的 API 和本地存储一致,仅仅是生存周期不同而已const localStorage = window.localStorage;function add() {localStorage.setItem('name', 'Spring-_-Bear');// save objectlocalStorage.setItem('wut', JSON.stringify({name: 'WHUT', address: 'HBWH'}));}function delItem() {localStorage.removeItem('name');}function update() {localStorage.setItem('name', 'bear');}function read() {// read objectlet objStr = localStorage.getItem('wut');console.log(JSON.parse(objStr))}function clearAll() {localStorage.clear();}
</script>
3.8 组件自定义事件(组件通信)
-
组件自定义事件:适用于父组件给子组件绑定自定义事件,从而实现
子 ===> 父
组件通信。自定义事件的回调在父组件中,而事件的触发在子组件中-
方式一:v-on 实现
<template><div><!-- v-on 实现组件自定义事件(v-on.once.getName:事件只触发一次) --><Student v-on:getName="getStudentName"/></div> </template><script> import Student from "@/components/Student";export default {name: "App",components: {Student},methods: {getStudentName(val, ...params) {console.log(val)console.log(params)}} } </script>
-
方式二:ref 实现
<template><div><!-- ref 实现组件自定义事件 --><Student ref="student"/></div> </template><script> import Student from "@/components/Student";export default {name: "App",components: {Student},methods: {getStudentName(val, ...params) {console.log(val)console.log(params)}},mounted() {setTimeout(() => {// 给子组件绑定一个自定义事件this.$refs.student.$on('getName', this.getStudentName)/* this.$refs.student.$once('getName', this.getStudentName) 只触发一次 */}, 3000);} } </script>
-
-
子组件触发自定义事件实现组件通信:
<template><div class="student"><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1><button @click="sendName">发送我的名字</button></div> </template><script> export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},methods: {// 通过调用 VC 身上的自定义事件实现给父组件传递消息sendName() {this.$emit('getName', this.name, 1, 2, 3, 4, 5)}} } </script>
-
自定义事件的解绑:
<template><div class="student"><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1><button @click="sendName">发送我的名字</button><button @click="unbind">解绑自定义事件</button></div> </template><script> export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},methods: {// 通过调用 VC 身上的自定义事件实现给父组件传递消息sendName() {this.$emit('getName', this.name, 1, 2, 3, 4, 5)},unbind() {// 方式一:解绑单个自定义事件// this.$off('getName')// 方式二:解绑多个自定义事件// this.$off(['getName', 'test'])// 方式三:解绑所有自定义事件this.$off()}} } </script>
-
给组件绑定原生 DOM 事件:需要适用
native
事件修饰符<template><div><h1>Name received: {{ msg }}</h1><Student ref="student" @click.native="show"/></div> </template>
-
通过
this.$refs.xxx.$on('eventName', callFunction)
绑定自定义事件时,回调要么配置在methods
中,要么使用箭头函数,否则 this 指向会出问题,即以下两种方式等价<template><div><h1>Name received: {{ msg }}</h1><Student ref="student"/></div> </template><script> import Student from "@/components/Student";export default {name: "App",components: {Student},data() {return {'msg': ''}},mounted() {// 此处的回调函数必须为箭头函数this.$refs.student.$on('getName', val => {console.log(val)this.msg = val});} } </script>
<template><div><h1>Name received: {{ msg }}</h1><Student ref="student"/></div> </template><script> import Student from "@/components/Student";export default {name: "App",components: {Student},data() {return {'msg': ''}},methods: {getStudentName(val) {console.log(val)this.msg = val}},mounted() {this.$refs.student.$on('getName', this.getStudentName)} } </script>
3.9 全局事件总线(组件通信)
全局事件总线是一种组件间通信的方式,适用于任意组件间通信。全局事件总线并不是插件、配置文件等等,事件总线是程序员在做 Vue 开发中总结积累的一套方法、规则,只要满足这套规则就可以实现组件间的通信
-
main.js
:装配全局事件总线,供组件间通信使用import Vue from "vue"; import App from './App.vue'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),beforeCreate() {// 装配全局事件总线Vue.prototype.$bus = this} })
-
Studnet.vue
:消息接收者,在事件总线$bus
上注册事件(函数)<template><div><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1></div> </template><script> export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},mounted() {// 在总线上注册事件this.$bus.$on('stuReceiveMsg', msg => {console.log('我是 Student,收到消息如下:', msg)});},beforeDestroy() {// 解绑当前组件用到的事件this.$bus.$off('stuReceiveMsg')} } </script>
-
School.vue
:消息发送者,利用总线触发事件,实现组件通信<template><div><h1>学校:{{ name }}</h1><h1>地址:{{ address }}</h1><button @click="noticeStudent('所有学生今天必须做核酸')">通知学生做核酸</button></div> </template><script> export default {name: "School",data() {return {name: '武汉理工大学',address: '湖北省武汉市'}},methods: {noticeStudent(msg) {// 通过总线触发事件,从而实现组件通信this.$bus.$emit('stuReceiveMsg', '通知内容如下:' + msg)}} } </script>
-
App.vue
:引入并使用其它组件<template><div><School/><Student/></div> </template><script> import School from "@/components/School"; import Student from "@/components/Student";export default {name: "App",components: {School, Student} } </script>
3.10 消息订阅与发布(组件通信)
安装
pubsub-js
库用于实现消息订阅与发布:npm i pubsub-js
-
消息接收者:也即消息订阅者,注册函数用于订阅消息
<template><div><h1>姓名:{{ name }}</h1><h1>性别:{{ sex }}</h1></div> </template><script> import pubsub from 'pubsub-js'export default {name: "Student",data() {return {name: 'Spring-_-Bear',sex: 'male'}},mounted() {// 订阅消息:使用箭头函数时 this 指向 VC,使用正常函数时 this 为 undefinedthis.pid = pubsub.subscribe('schoolNotice', (msgName, msg) => {console.log('Come from ' + msgName, ', data is ' + msg);})},beforeDestroy() {// 取消订阅pubsub.unsubscribe(this.pid);} } </script>
-
消息发送者:也即发布者,发布消息
<template><div><h1>学校:{{ name }}</h1><h1>地址:{{ address }}</h1><button @click="noticeStudent('所有学生今天必须做核酸')">通知学生做核酸</button></div> </template><script> import pubsub from 'pubsub-js'export default {name: "School",data() {return {name: '武汉理工大学',address: '湖北省武汉市'}},methods: {noticeStudent(msg) {// 发布消息pubsub.publish('schoolNotice', msg);}} } </script>
-
App.vue
:引入并使用其它组件<template><div><School/><Student/></div> </template><script> import School from "@/components/School"; import Student from "@/components/Student";export default {name: "App",components: {School, Student} } </script>
3.11 $nextTick
$nextTick
指定的回调函数会在下一次 DOM 更新结束后执行。适用于当数据改变后要基于更新后的 DOM 进行某些特殊操作,如输入框值 DOM 更新后下次打开时自动聚焦
用法详见
TodoList
案例中的TodoItem
组件
3.12 动画与过渡
-
动画效果:将要实现实现动画效果的单个元素使用
transition
标签包裹<template><div><button @click="isShow = !isShow">Display / Hide</button><!-- 将要实现实现动画的单个元素使用 `transition` 标签包裹 --><transition name="hello" appear><h1 v-show="isShow">Hello World</h1></transition></div> </template><script> export default {name: "Animation",data() {return {isShow: true}} } </script><style scoped> h1 {background-color: skyblue;text-align: center; }/* 入场激活 */ .hello-enter-active {animation: helloAnimation 0.5s linear; }/* 离开激活 */ .hello-leave-active {animation: helloAnimation 0.5s linear reverse; }/* 定义动画 */ @keyframes helloAnimation {from {transform: translateX(-100%);}to {transform: translateX(0px);} } </style>
-
过渡效果:将要实现实现过渡效果的单个元素使用
transition
标签包裹<template><div><button @click="isShow = !isShow">Display / Hide</button><!-- 将要实现实现过渡效果的单个元素使用 `transition` 标签包裹 --><transition name="hello" appear><h1 v-show="isShow">Hello World</h1></transition></div> </template><script> export default {name: "Animation",data() {return {isShow: true}} } </script><style scoped> h1 {background-color: skyblue;text-align: center; }.hello-enter, .hello-leave-to {transform: translateX(-100%); }.hello-enter-active, .hello-leave-active {transition: 0.5s linear; }.hello-leave, .hello-enter-to {transform: translateX(0); } </style>
-
多个元素过渡:
-
方式一:增加一个顶级
div
囊括需要实现过渡的元素<template><div><button @click="isShow = !isShow">Display / Hide</button><transition name="hello" appear><div v-show="isShow"><h1>Hello World</h1><h1>Vue</h1></div></transition></div> </template><script> export default {name: "Animation",data() {return {isShow: true}} } </script><style scoped> h1 {background-color: skyblue;text-align: center; }.hello-enter, .hello-leave-to {transform: translateX(-100%); }.hello-enter-active, .hello-leave-active {transition: 0.5s linear; }.hello-leave, .hello-enter-to {transform: translateX(0); } </style>
-
方式二:使用
transition-group
元素包裹需要实现过渡效果的多个元素<template><div><button @click="isShow = !isShow">Display / Hide</button><!-- 多个元素过渡效果使用 transition-group 标签包裹 --><transition-group name="hello" appear><h1 v-show="isShow" :key="1">Hello World</h1><h1 v-show="isShow" :key="2">Vue</h1></transition-group></div> </template><script> export default {name: "Animation",data() {return {isShow: true}} } </script><style scoped> h1 {background-color: skyblue;text-align: center; }.hello-enter, .hello-leave-to {transform: translateX(-100%); }.hello-enter-active, .hello-leave-active {transition: 0.5s linear; }.hello-leave, .hello-enter-to {transform: translateX(0); } </style>
-
3.13 TodoList 案例
安装 nanoid 用于生成 UUID:
npm i nanoid
用到的知识点:
- 双向数据绑定、键盘事件、监视属性、条件渲染、事件绑定、钩子函数、嵌套组件
- props、组件自定义事件、全局事件总线
- 多个元素过渡、浏览器本地存储
- nanoid、ref、$nexttick
-
main.js
import Vue from "vue"; import App from './App.vue'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),beforeCreate() {// 装配全局事件总线Vue.prototype.$bus = this} })
-
App.vue
<template><div id="root"><div class="todo-container"><div class="todo-wrap"><!-- 自定义事件 addTodoItem --><TodoHeader @addTodoItem="addTodoItem"/><!-- 给组件传递数据 --><TodoList :todos="todos"/><!-- 自定义事件 checkAllItems、clearCompletedItems --><TodoFooter :todos="todos" @checkAllItems="checkAllItems" @clearCompletedItems="clearCompletedItems"/></div></div></div> </template><script> import TodoHeader from "@/components/TodoHeader"; import TodoList from "@/components/TodoList"; import TodoFooter from "@/components/TodoFooter";export default {name: 'App',components: {TodoHeader, TodoList, TodoFooter},data() {return {// 浏览器本地读取 todos 数据todos: JSON.parse(localStorage.getItem('todos')) || []}},methods: {// 增addTodoItem(todoItem) {this.todos.unshift(todoItem)},// 删deleteTodoItem(id) {this.todos = this.todos.filter((todo) => {return id !== todo.id;});},// 删:清除已完成项clearCompletedItems() {this.todos = this.todos.filter((todo) => {return !todo.completed});},// 改:修改项描述updateTodoItem(id, description) {this.todos.forEach((todo) => {if (id === todo.id) {todo.description = description}});},// 改:选中全部checkAllItems(status) {this.todos.forEach((todo) => {todo.completed = status});},// 改:修改项状态changeTodoItemStatus(id) {this.todos.forEach((todo) => {if (id === todo.id) {todo.completed = !todo.completed;}});}},watch: {todos: {deep: true,handler(newVal) {// 浏览器本地存储localStorage.setItem('todos', JSON.stringify(newVal))}}},mounted() {// 在全局事件总线上注册事件this.$bus.$on('changeTodoItemStatus', this.changeTodoItemStatus)this.$bus.$on('deleteTodoItem', this.deleteTodoItem)this.$bus.$on('updateTodoItem', this.updateTodoItem)},beforeDestroy() {// 解绑全局事件总线事件this.$bus.$off(['changeTodoItemStatus', 'deleteTodoItem', 'updateTodoItem'])} } </script><style> body {background: #fff; }.btn {display: inline-block;padding: 4px 12px;margin-bottom: 0;font-size: 14px;line-height: 20px;text-align: center;vertical-align: middle;cursor: pointer;box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);border-radius: 4px; }.btn-danger {color: #fff;background-color: #da4f49;border: 1px solid #bd362f; }.btn-info {color: #fff;background-color: skyblue;border: 1px solid #2e5968;margin-right: 5px; }.btn-info:hover {color: #fff;background-color: #2e5968; }.btn-danger:hover {color: #fff;background-color: #bd362f; }.btn:focus {outline: none; }.todo-container {width: 600px;margin: 0 auto; }.todo-container .todo-wrap {padding: 10px;border: 1px solid #ddd;border-radius: 5px; } </style>
-
TodoHeader.vue
<template><div class="todo-header"><input type="text" placeholder="请输入您的任务,按回车键确认" v-model.trim="description" @keyup.enter="addItem"/></div> </template><script> import {nanoid} from "nanoid";export default {name: "TodoHeader",data() {return {description: ''}},methods: {addItem() {if (!this.description) {return alert("添加项目不能为空!");}// 全局事件总线:触发 App 组件中的自定义事件实现消息通信(子 => 父)this.$emit('addTodoItem', {id: nanoid(), description: this.description, completed: false});this.description = '';}} } </script><style scoped> .todo-header input {width: 560px;height: 28px;font-size: 14px;border: 1px solid #ccc;border-radius: 4px;padding: 4px 7px; }.todo-header input:focus {outline: none;border-color: rgba(82, 168, 236, 0.8);box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); } </style>
-
TodoList.vue
<template><ul class="todo-main"><!-- 多个元素过渡 --><transition-group name="todo" appear><TodoListItem v-for="todo in todos" :todo="todo" :key="todo.id"/></transition-group></ul> </template><script> import TodoListItem from "@/components/TodoItem";export default {name: "TodoList",components: {TodoListItem},props: ['todos'] } </script><style scoped> .todo-main {margin-left: 0px;border: 1px solid #ddd;border-radius: 2px;padding: 0px; }.todo-empty {height: 40px;line-height: 40px;border: 1px solid #ddd;border-radius: 2px;padding-left: 5px;margin-top: 10px; }.todo-enter, .todo-leave-to {transform: translateX(100%); }.todo-enter-active, .todo-leave-active {transition: 0.3s linear; }.todo-leave, .todo-enter-to {transform: translateX(0); } </style>
-
TodoItem.vue
<template><li><label><!-- 选中状态 --><input type="checkbox" :checked="todo.completed" @change="changeItemStatus(todo.id)"/> <!-- 内容 --><span v-show="!todo.isEdit">{{ todo.description }}</span><!-- 编辑输入框 --><input type="text" :value="todo.description" v-show="todo.isEdit" @blur="handleBlur($event, todo)" ref="updateInputEle"></label><!-- 删除与编辑 --><button class="btn btn-danger" @click="deleteItem(todo.id)">删除</button><button class="btn btn-info" @click="editItem(todo)" v-show="!todo.isEdit">编辑</button></li> </template><script> export default {name: "TodoListItem",props: ['todo'],methods: {deleteItem(id) {// 触发全局事件总线实现消息通信(孙 => 爷)this.$bus.$emit('deleteTodoItem', id)},changeItemStatus(id) {// 触发全局事件总线实现消息通信(孙 => 爷)this.$bus.$emit('changeTodoItemStatus', id)},editItem(todo) {if (todo.hasOwnProperty('isEdit')) {todo.isEdit = true} else {this.$set(todo, 'isEdit', true)}// $nextTick 指定的回调函数会在 DOM 节点更新之后再执行this.$nextTick(function () {this.$refs.updateInputEle.focus()});},handleBlur(e, todo) {if (e.target.value.trim()) {// 触发全局事件总线实现消息通信(孙 => 爷)this.$bus.$emit('updateTodoItem', todo.id, e.target.value)}todo.isEdit = false}} } </script><style scoped> li {list-style: none;height: 36px;line-height: 36px;padding: 0 5px;border-bottom: 1px solid #ddd; }li label {float: left;cursor: pointer; }li label li input {vertical-align: middle;margin-right: 6px;position: relative;top: -1px; }li button {float: right;display: none;margin-top: 3px; }li:before {content: initial; }li:last-child {border-bottom: none; }li:hover {background-color: gray; }li:hover button {display: block; } </style>
-
TodoFooter.vue
<template><div class="todo-footer" v-show="todos.length"><!-- 选中全部 --><label><input type="checkbox" @change="checkAll" :checked="todos.length === completedTotal && todos.length > 0"/></label><span><span>已完成 {{ completedTotal }}</span> / 全部 {{ todos.length }}</span><button class="btn btn-danger" @click="clearCompleted">清除已完成</button></div> </template><script> export default {name: "TodoFooter",props: ['todos'],computed: {completedTotal() {return this.todos.reduce((pre, cur) => {return pre + (cur.completed ? 1 : 0)}, 0);}},methods: {checkAll(e) {// 触发 App 组件中的自定义事件实现消息通信(子 => 父)this.$emit('checkAllItems', e.target.checked)},clearCompleted() {// 触发 App 组件中的自定义事件实现消息通信(子 => 父)this.$emit('clearCompletedItems');}} } </script><style scoped> .todo-footer {height: 40px;line-height: 40px;padding-left: 6px;margin-top: 5px; }.todo-footer label {display: inline-block;margin-right: 20px;cursor: pointer; }.todo-footer label input {position: relative;top: -1px;vertical-align: middle;margin-right: 5px; }.todo-footer button {float: right;margin-top: 5px; } </style>
3.14 配置代理
安装 axios 请求库:
npm i axios
-
单台服务器代理配置:
-
vue.config.js
中配置代理信息const {defineConfig} = require('@vue/cli-service') module.exports = defineConfig({transpileDependencies: true,// 关闭语法检查lintOnSave: false,// 配置代理devServer: {proxy: 'http://localhost:5000'} })
-
axios
请求库向代理服务器发起请求,由代理服务器向真实服务器发请求<template><button @click="getStudents">获取学生数据</button> </template><script> import axios from 'axios'export default {name: "App",methods: {getStudents() {// 请求代理服务器,由代理服务器发起请求axios.get('http://localhost:8080/students').then(response => {console.log('请求成功', response.data)},error => {console.log('请求失败', error.message)})}} } </script>
-
-
多台服务器代理配置:
-
vue.config.js
中依次配置多台服务器代理const {defineConfig} = require('@vue/cli-service') module.exports = defineConfig({transpileDependencies: true,// 关闭语法检查lintOnSave: false,// 配置代理devServer: {proxy: {'/stu': {target: 'http://localhost:5000',// 重写请求路径pathRewrite: {'^/stu': ''},// WebSocket,默认为 truews: true,// true 时修改请求头中的 HOST 为 target 中的值,默认为 truechangeOrigin: true},'/car': {target: 'http://localhost:5001',pathRewrite: {'^/car': ''}}}} })
-
axios
请求库根据需求向代理服务器发起不同的路径请求,由代理服务器向真实服务器发请求<template><div><button @click="getStudents">获取学生数据</button><button @click="getCars">获取汽车数据</button></div> </template><script> import axios from 'axios'export default {name: "App",methods: {getStudents() {// 请求代理服务器axios.get('http://localhost:8080/stu/students').then(response => {console.log('请求成功', response.data)},error => {console.log('请求失败', error.message)})},// 请求代理服务器getCars() {axios.get('http://localhost:8080/car/cars').then(response => {console.log('请求成功', response.data)},error => {console.log('请求失败', error.message)})}} } </script>
-
-
使用
vue-resource
插件代替axios
请求库:安装 vue-resource 插件:
npm i vue-resource
-
main.js
:引入并使用插件import Vue from "vue"; import App from './App.vue'; import VueResource from 'vue-resource'Vue.config.productionTip = false // 使用 vue-resource 插件 Vue.use(VueResource)new Vue({el: '#app',render: h => h(App) })
-
this.$http.get
:发起请求,获取响应<template><button @click="getStudents">获取学生数据</button> </template><script>export default {name: "App",methods: {getStudents() {// 使用 this.$http.get 发起请求this.$http.get('http://localhost:8080/students').then(response => {console.log('请求成功', response.data)},error => {console.log('请求失败', error.message)})}} } </script>
-
3.15 GitHub 案例
需在 public 目录中的
index.html
中引入bootstrap.css
用到的知识点:
- 全局事件总线
- axios 请求库
-
main.js
:装配全局事件总线import Vue from "vue"; import App from './App.vue'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),beforeCreate() {// 装配全局事件总线Vue.prototype.$bus = this} })
-
App.vue
:引入并使用其它组件<template><div class="container"><Search/><List/></div> </template><script> import Search from "@/components/Search"; import List from "@/components/List";export default {name: "App",components: {List, Search} } </script>
-
Search.vue
:发起请求,获取响应,通过全局事件总线传递数据<template><section class="jumbotron"><h3 class="jumbotron-heading">GitHub Users Search</h3><div><input v-model="keyWord" type="text" placeholder="enter the username you want to search"/> <button @click="searchUsers">Search</button></div></section> </template><script> import axios from 'axios'export default {name: "Search",data() {return {'keyWord': ''}},methods: {searchUsers() {// 加载中···this.$bus.$emit('getUserInfo', {isFirst: false, isLoading: true, errorMsg: '', users: []})// 发起请求,获取响应axios.get(`=${this.keyWord}`).then(response => {this.$bus.$emit('getUserInfo', {isFirst: false, isLoading: false, errorMsg: '', users: response.data.items})},error => {this.$bus.$emit('getUserInfo', {isFirst: false, isLoading: false, errorMsg: error.message, users: []})})}} } </script>
-
List.vue
:通过全局事件总线接收数据并解析展示<template><div class="row"><div class="card" v-for="user in info.users" :key="user.id" v-show="info.users.length"><a :href="user.html_url" target="_blank"><img :src="user.avatar_url" alt="user's avatar" style='width: 100px'/></a><p class="card-text">{{ user.login }}</p></div><h1 v-show="info.isFirst">欢迎使用 Github 用户搜索</h1><h1 v-show="info.isLoading">Loading···</h1><h1 v-show="info.errorMsg">{{ info.errorMsg }}</h1></div> </template><script> export default {name: "List",data() {return {info: {isFirst: true,isLoading: false,errorMsg: '',users: []}}},mounted() {this.$bus.$on('getUserInfo', (info) => {// 字面量对比更新this.info = {...this.info, ...info}});},beforeDestroy() {this.$bus.$off('getUserInfo')} } </script><style scoped> .card {float: left;width: 33.333%;padding: .75rem;margin-bottom: 2rem;border: 1px solid #efefef;text-align: center; }.card > img {margin-bottom: .75rem;border-radius: 50%; }.card-text {font-size: 85%; } </style>
3.16 slot 插槽(组件通信)
插槽(Slot
)是 Vue 提出来的一个概念,正如其名字一样,插槽用于决定将所携带的内容插入到指定的某个位置,从而使模板分块。插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示由子组件来进行控制。插槽可以让父组件可以向子组件指定位置插入 HTML 结构,也是一种组件间的通信方式,适用于 父组件 -> 子组件
-
默认插槽:父组件定义插槽,子组件中使用
slot
标签使用插槽-
App.vue
:往子组件的标签体中定义插槽需要显示的内容<template><div class="container"><Category title="美食"><img src=".png" alt="WHUT"></Category><Category title="游戏"><ul><li v-for="(movie, index) in movies" :key="index">{{ movie }}</li></ul></Category><Category title="电影"><video controls src=".png"></video></Category></div> </template><script> import Category from "@/components/Category";export default {name: "App",components: {Category},data() {return {foods: ['火锅', '早茶', '烤鸭'],games: ['原神', '崩坏3', '守望先锋', '王者荣耀', '和平精英'],movies: ['肖申克的救赎', '美丽人生', '我们的父辈', '阿甘正传', '这个杀手不太冷']}} } </script><style> .container {display: flex;justify-content: space-around; }h3 {text-align: center;background-color: orange; } </style>
-
Category.vue
:使用slot
标签显示父组件定义的插槽内容<template><div class="category"><h3>{{ title }}</h3><!-- 默认插槽,显示父组件在 Category 组件体中定义的内容 --><slot>default value</slot></div> </template><script> export default {name: "Category",props: ['title'] } </script><style scoped> .category {background-color: skyblue;width: 200px;height: 300px; }img {width: 100%; }video {width: 100%; } </style>
-
-
具名插槽:顾名思义,就是有名字的插槽
-
App.vue
:在标签的属性上使用slot
属性指定当前标签属于哪个插槽<template><div class="container"><Category title="美食"><!-- 使用 slot 属性指定具体的插槽 --><img slot="first" src=".png" alt="WHUT"><div slot="second" class="container"><a href="">B 大学</a></div></Category><Category title="游戏"><ul slot="first"><li v-for="(movie, index) in movies" :key="index">{{ movie }}</li></ul><!-- 复用同一个插槽 --><div slot="second" class="container"><a href="">百度</a><a href="">谷歌</a></div></Category><Category title="电影"><video slot="first" controls src=".png"></video><!-- 只能在 template 标签中使用 v-slot:second,等价于 slot="second" --><template v-slot:second><div class="container"><a href="">经典</a><a href="">热门</a><a href="">推荐</a></div><h4>前往购票</h4></template></Category></div> </template><script> import Category from "@/components/Category";export default {name: "App",components: {Category},data() {return {foods: ['火锅', '早茶', '烤鸭'],games: ['原神', '崩坏3', '守望先锋', '王者荣耀', '和平精英'],movies: ['肖申克的救赎', '美丽人生', '我们的父辈', '阿甘正传', '这个杀手不太冷']}} } </script><style> .container {display: flex;justify-content: space-around; }h3 {text-align: center;background-color: orange; }h4 {text-align: center; } </style>
-
Category.vue
:使用slot
标签的name
属性值指定使用的具体插槽<template><div class="category"><h3>{{ title }}</h3><!-- 具名插槽 --><slot name="first">default value</slot><slot name="second">default value</slot></div> </template><script> export default {name: "Category",props: ['title'] } </script><style scoped> .category {background-color: skyblue;width: 200px;height: 300px; }img {width: 100%; }video {width: 100%; } </style>
-
-
作用域插槽:作用域插槽就是带参数(数据)的插槽,强调的则是数据作用的范围。在子组件的插槽中带入参数(数据)提供给父组件使用,该参数(数据)仅在插槽内有效,父组件可以根据子组件中传过来的插槽参数(数据)对展示内容进行定制
-
App.vue
:在template
标签上使用scope
或slot-scope
属性接收来自子组件(插槽使用者)传递的数据<template><div class="container"><Category title="电影"><!-- 接收子组件传递的数据(方式一) --><template scope="data"><h4>{{ data.msg }}</h4><ul><li v-for="(movie, index) in data.movies" :key="index">{{ movie }}</li></ul></template></Category><Category title="电影"><!-- 接收子组件传递的数据(方式二) --><template slot-scope="{movies, msg}"><!-- scope="{movies, msg}" <=> slot-scope="{movies, msg} --><h4>{{ msg }}</h4><ol><li v-for="(movie, index) in movies" :key="index">{{ movie }}</li></ol></template></Category></div> </template><script> import Category from "@/components/Category";export default {name: "App",components: {Category} } </script><style> .container {display: flex;justify-content: space-around; }h4 {text-align: center; } </style>
-
Category.vue
:给父组件(插槽定义者)传递数据<template><div class="category"><h3>{{ title }}</h3><!-- 作用域插槽:给插槽定义者传递数据 --><slot :movies="movies" :msg="'快来购票观影吧'">default value</slot></div> </template><script> export default {name: "Category",props: ['title'],data() {return {movies: ['肖申克的救赎', '美丽人生', '我们的父辈', '阿甘正传', '这个杀手不太冷']}} } </script><style scoped> .category {background-color: skyblue;width: 200px;height: 300px; }h3 {text-align: center;background-color: orange; } </style>
-
四、vuex 插件
4.1 概述(组件通信)
-
定义:Vuex 是专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 Vue 应用中多个组件的共享状态进行集中式管理,是一种适用于任意组件间通信的方式
-
Vuex 工作原理图:其中 Actions、Mutations、State 称为 Store
4.2 搭建 Vuex 环境
Vue2 中安装 Vuex3:
npm i vuex@3
-
src/store/index.js
:配置 Vuex 的核心Store
:import Vuex from 'vuex' import Vue from "vue";// 必须先使用 Vuex 插件才能创建 Store 实例 Vue.use(Vuex)// 响应组件的动作 const actions = {} // 操作数据 state const mutations = {} // 存储数据 const state = {} // 加工数据 state const getters = {}export default new Vuex.Store({actions,mutations,state,getters })
-
main.js
:引入为 Vuex 配置的 Store 并配置:import Vue from "vue"; import App from './App.vue'; import store from './store/index'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),// vuex 的核心配置store: store })
4.3 求和案例
-
main.js
:引入为 Vuex 配置的 Store 并配置:import Vue from "vue"; import App from './App.vue'; import store from './store/index'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),// vuex 的核心配置store: store })
-
src/store/index.js
:配置 Vuex 的核心Store
:import Vuex from 'vuex' import Vue from "vue";// 必须先使用 Vuex 插件才能创建 Store 实例 Vue.use(Vuex)// 响应组件中的动作 const actions = {jiaOdd(context, val) {if (context.state.sum % 2 === 1) {context.commit('JIA', val)}},jiaWait(context, val) {setTimeout(() => {context.commit('JIA', val)}, 1000);} } // 操作数据 const mutations = {JIA(state, val) {state.sum += val},JIAN(state, val) {state.sum -= val} } // 存储数据 const state = {sum: 0 }// 加工 state 中的数据 const getters = {enlarge(state) {return state.sum * 10} }export default new Vuex.Store({actions,mutations,state,getters })
-
Count.vue
:通过this.$store.dispath()
、this.$store.commit()
将组件中的动作分别分发给Actions
处理业务逻辑、Mutations
操作数据<template><div><h1>当前求和结果:{{ $store.state.sum }}</h1><h1>数据处理结果:{{ $store.getters.enlarge }}</h1><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option></select><button @click="add">+</button><button @click="sub">-</button><button @click="addOdd">当前求和为奇数时加</button><button @click="addWait">等一会再加</button></div> </template><script> export default {name: "Count",data() {return {n: 1}},methods: {add() {// 直接提交给 Mutations 处理数据this.$store.commit('JIA', this.n)},sub() {this.$store.commit('JIAN', this.n)},addOdd() {// 分发给 Actions 处理业务逻辑this.$store.dispatch('jiaOdd', this.n)},addWait() {this.$store.dispatch('jiaWait', this.n)}} } </script><style scoped> button {margin: 5px; } </style>
4.4 vuex 中的四种 map
mapState
与mapGetters
:将 Store 中的 state 和 getters 映射为 computed 计算属性mapMutations
与mapActions
:将 Store 中的 mutations 和 actions 映射为 methods 方法
<template><div><h1>当前求和结果:{{ sum }}</h1><h1>数据处理结果:{{ enlarge }}</h1><button @click="add(n)">+</button><button @click="sub(n)">-</button><button @click="addOdd(n)">当前求和为奇数时加</button><button @click="addWait(n)">等一会再加</button><hr/><h1>{{ school }}</h1><h1>{{ address }}</h1></div>
</template><script>
import {mapState, mapGetters, mapActions, mapMutations} from 'vuex'export default {name: "Count",data() {return {n: 1}},computed: {/** 方式一(对象式)* ...mapState({sum: 'sum', school: 'school', address: 'address'})* 方式二(数组式):当 state 中数据 key 与计算属性的函数名相同时,可使用数组简写法* ...mapState(['sum', 'school', 'address'])* * 注:mapGetters、mapMutations、mapActions 同理*/// 将 state 映射为计算属性...mapState(['sum', 'school', 'address']),// 将 getters 映射为计算属性...mapGetters(['enlarge'])},methods: {// 用于生成与 Mutations 对话的方法...mapMutations({add: 'JIA', sub: 'JIAN'}),// 用于生成与 Actions 对话的方法...mapActions({addOdd: 'jiaOdd', addWait: 'jiaWait'})},
}
</script>
4.5 vuex 模块化
-
main.js
:引入为 Vuex 配置的 Store 并配置:import Vue from "vue"; import App from './App.vue'; import store from './store/index'Vue.config.productionTip = falsenew Vue({el: '#app',render: h => h(App),store: store })
-
src/store/index.js
:引入其它模块化配置import Vuex from 'vuex' import Vue from "vue" import countConfig from "@/store/countConfig"; import personConfig from "@/store/personConfig";Vue.use(Vuex)export default new Vuex.Store({modules: {countStore: countConfig,personStore: personConfig} })
-
src/store/*.js
:各种模块化的 Store 配置-
src/store/countConfig.js
export default {namespaced: true,actions: {jiaOdd(context, val) {if (context.state.sum % 2 === 1) {context.commit('JIA', val)}},jiaWait(context, val) {setTimeout(() => {context.commit('JIA', val)}, 1000);}},mutations: {JIA(state, val) {state.sum += val},JIAN(state, val) {state.sum -= val}},state: {sum: 0},getters: {enlarge(state) {return state.sum * 10}} }
-
src/store/personConfig.js
export default {namespaced: true,actions: {addPersonLi(context, value) {if (value.name.indexOf('李') === 0) {context.commit('ADD_PERSON', value)} else {alert(value.name + '不姓李')}}},mutations: {ADD_PERSON(state, value) {state.persons.unshift(value)}},state: {persons: [{id: 1, name: 'bear'}]},getters: {firstPersonName(state) {return state.persons[0].name}} }
-
-
src/component/*.vue
:在各种组件中通过对应的 store 访问和操作数据-
Person.vue
<template><div><input type="text" v-model="name"> <button @click="addPerson">添加</button> <button @click="addPersonWang">添加一个姓李的人</button><h2>第一个人的姓名:{{ first }}</h2><ol><li v-for="person in persons" :key="person.id">{{ person.name }}</li></ol><h2 style="color: red">结果:{{ sum }}</h2></div> </template><script> import {nanoid} from 'nanoid'export default {name: "Person",data() {return {name: ''}},computed: {sum() {return this.$store.state.countStore.sum},persons() {return this.$store.state.personStore.persons},first() {return this.$store.getters['personStore/firstPersonName']}},methods: {addPerson() {const obj = {id: nanoid(), name: this.name}this.$store.commit('personStore/ADD_PERSON', obj)this.name = ''},addPersonWang() {const obj = {id: nanoid(), name: this.name}this.$store.dispatch('personStore/addPersonLi', obj)this.name = ''}} } </script><style scoped></style>
-
Count.vue
<template><div><h1>求和:{{ sum }}</h1><h1>十倍:{{ enlarge }}</h1><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add(n)">+</button><button @click="sub(n)">-</button><button @click="addOdd(n)">当前求和为奇数时加</button><button @click="addWait(n)">等一会再加</button><h2 style="color: red">Person 总人数:{{ persons.length }}</h2></div> </template><script> import {mapState, mapGetters, mapActions, mapMutations} from 'vuex'export default {name: "Count",data() {return {n: 1}},methods: {...mapMutations('countStore', {add: 'JIA', sub: 'JIAN'}),...mapActions('countStore', {addOdd: 'jiaOdd', addWait: 'jiaWait'})},computed: {...mapState('countStore', {sum: 'sum'}),...mapGetters('countStore', ['enlarge']),...mapState('personStore', ['persons'])}, } </script><style scoped> button {margin: 5px; } </style>
-
-
App.vue
<template><div><Count></Count><Person></Person></div> </template><script> import Count from "@/components/Count"; import Person from "@/components/Person";export default {name: 'App',components: {Person, Count} } </script>
五、vue-router 插件
5.1 概述
- 定义:vue-router 是 vue 的一个插件库,专门用来实现 SPA(
Simple Page web Application
)单页应用 - 路由使用的注意事项:
- 路由组件通常放在
pages
文件夹,一般组件通常放在components
文件夹 - 通过切换隐藏的路由组件,默认立即销毁,需要展示时再挂载
- 每个组件都有自己的
$route
属性,其中存储着自己的路由信息 - 整个应用只有一个 router,可以通过组件的
$router
获取
- 路由组件通常放在
5.2 路由基本使用
Vue2 中安装 vue-router3:
npm i vue-router@3
注:需在
public
目录下的index.html
中引入bootstrap.css
-
main.js
:引入路由插件并进行配置import Vue from "vue"; import App from './App.vue'; import VueRouter from 'vue-router' import router from './router/index'Vue.config.productionTip = falseVue.use(VueRouter)new Vue({el: '#app',render: h => h(App),// 配置路由器router: router })
-
src/router/index.js
:配置路由插件import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home";export default new VueRouter({routes: [{path: '/about', component: About},{path: '/home', component: Home}] })
-
src/pages/*.vue
:各种路由组件-
src/pages/Home.vue
<template><h2>我是 Home 的内容</h2> </template><script> export default {name: "Home" } </script>
-
src/pagse/About.vue
<template><h2>我是 About 的内容</h2> </template><script> export default {name: "About" } </script>
-
-
App.vue
:router-link
标签实现路由组件页面切换,router-view
标签实现组件页面展示<template><div><div class="row"><div class="col-xs-offset-2 col-xs-8"><div class="page-header"><h2>Vue Router Demo</h2></div></div></div><div class="row"><div class="col-xs-2 col-xs-offset-2"><div class="list-group"><!-- 路由实现 SPA 应用页面切换 --><router-link active-class="active" class="list-group-item" to="/home">Home</router-link><router-link active-class="active" class="list-group-item" to="/about">About</router-link></div></div><div class="col-xs-6"><div class="panel"><div class="panel-body"><!-- 展示路由组件 --><router-view></router-view></div></div></div></div></div> </template><script> export default {name: "App" } </script>
5.3 嵌套与命名路由
-
嵌套路由:通过
children
配置项在当前路由路径下配置子路由路径export default new VueRouter({routes: [{// 命名路由name: 'zhuye',path: '/home',component: Home,// 嵌套路由配置children: [{name: 'xinwen',path: 'news',component: News},{name: 'xiaoxi',path: 'message',component: Message}]},{path: '/about',component: About}] })
-
命名路由:使用路由配置的
name
配置项为当前路由路径指定别名以简化路由路径的书写<!-- 简化前:需要书写路由完整路径 --> <route-link to="/home/message">消息</route-link><!-- 简化后:通过路由别名简化路径 --> <route-link :to="{name: 'xiaoxi'}">消息</route-link>
5.4 路由参数
-
路由的
query
参数:用于在路由路径后传递参数(key=val&key=val
)-
src/router/index.js
:配置嵌套路由import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home"; import Message from "@/pages/Message"; import News from "@/pages/News"; import Detail from "@/pages/Detail";export default new VueRouter({routes: [{path: '/about',component: About},{path: '/home',component: Home,children: [{path: 'message',component: Message,children: [{path: 'detail',component: Detail}]},{path: 'news',component: News}]}] })
-
src/pages/Home.vue
:可路由到News.vue
或Message.vue
<template><div><h2>Home 组件内容</h2><div><ul class="nav nav-tabs"><li><router-link class="list-group-item" active-class="active" to="/home/news">News</router-link></li><li><router-link class="list-group-item" active-class="active" to="/home/message">Message</router-link></li></ul><router-view></router-view></div></div> </template><script> export default {name: "Home" } </script>
-
src/pages/Message.vue
:点击消息项可展示详情组件Detail.vue
<template><div><ul><li v-for="m in messages" :key="m.id"><!-- 方式一:数据绑定 + 模板字符串 --><router-link :to="`/home/message/detail?id=${m.id}&title=${m.title}`">{{ m.title }}</router-link><!-- 方式二:to 的对象写法 --><!--<router-link:to="{ path: '/home/message/detail',query: {id: m.id,title: m.title }}">{{ m.title }}</router-link>--></li></ul><hr/><router-view></router-view></div> </template><script> export default {name: "Message",data() {return {messages: [{id: 1, title: 'm1'},{id: 2, title: 'm2'},{id: 3, title: 'm3'}]}} } </script>
-
src/pages/Detail.vue
:接收query
路由参数,展示消息详情<template><div><ul><!-- 通过 VC 配置的 $route 信息读取 query 参数 --><li>消息编号:{{ $route.query.id }}</li><li>消息内容:{{ $route.query.title }}</li></ul></div> </template><script> export default {name: "Detail" } </script>
-
-
路由的
params
参数:路由配置时使用占位符为参数占位,传递参数时使用RESTful
风格-
src/router/index.js
:在路由路径中使用占位符为参数占位import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home"; import Message from "@/pages/Message"; import News from "@/pages/News"; import Detail from "@/pages/Detail";export default new VueRouter({routes: [{path: '/home',component: Home,children: [{path: 'news',component: News},{path: 'message',component: Message,children: [{name: 'xiangqing',// 占位符,声明路径参数占位path: 'detail/:id/:title',component: Detail}]}]},{path: '/about',component: About}] })
-
src/pages/Message.vue
:使用RESTful
风格传递参数<template><div><ul><li v-for="m in messages" :key="m.id"><!-- 方式一:数据绑定 + 模板字符串 --><router-link :to="`/home/message/detail/${m.id}/${m.title}`">{{ m.title }}</router-link><!-- 方式二:to 的对象写法,必须使用路由配置的 name 别名,而不能使用 path --><!--<router-link:to="{ name: 'xiangqing', params: {id: m.id,title: m.title }}">{{ m.title }}</router-link>--></li></ul><hr/><router-view></router-view></div> </template><script> export default {name: "Message",data() {return {messages: [{id: 1, title: 'm1'},{id: 2, title: 'm2'},{id: 3, title: 'm3'}]}} } </script>
-
src/pages/Detail.vue
:接收params
参数,展示详情<template><div><ul><!-- 通过 VC 配置的 $route 信息读取 params 参数 --><li>消息编号:{{ $route.params.id }}</li><li>消息内容:{{ $route.params.title }}</li></ul></div> </template><script> export default {name: "Detail" } </script>
-
-
路由的
props
配置:让路由组件更方便地接收参数-
src/router/index.js
:通过props
配置项接收路由参数,方便当前组件通过 props 获取传递的参数import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home"; import Message from "@/pages/Message"; import News from "@/pages/News"; import Detail from "@/pages/Detail";export default new VueRouter({routes: [{path: '/home',component: Home,children: [{path: 'news',component: News},{path: 'message',component: Message,children: [{name: 'xiangqing',path: 'detail/:id/:title',component: Detail,// 方式一(对象式):将所有 key-val 通过 props 传递给 Detail 组件// props: {id: 123, title: 'Spring-_-Bear'},// 方式二(布尔值):取值为 true 则将路由收到的所有 `params` 参数通过 props 传递给 Detail 组件// props: true,// 方式三(函数式):返回的每一组 key-val 通过 props 传递给 Detail 组件props($route) {return {id: $route.params.id,title: $route.params.title}}}]}]},{path: '/about',component: About}] })
-
src/pages/Details.vue
:通过props
接收本路由组件中定义的属性<template><div><ul><li>消息编号:{{ id }}</li><li>消息内容:{{ title }}</li></ul></div> </template><script> export default {name: "Detail",props: ['id', 'title'] } </script>
-
5.5 编程式路由导航
-
两种浏览器页面跳转记录模式:默认为 push
- push 模式:新纪录压栈
- replace 模式:新纪录替换栈顶元素
-
router-link
的replace
和push
:控制路由跳转时操作浏览器的页面跳转记录模式<router-link replace></router-link>
-
编程式路由导航实现页面跳转:
<template><div><div class="row"><div class="col-xs-offset-2 col-xs-8"><div class="page-header"><h2>Vue Router Demo</h2></div><button @click="back">后退</button><button @click="forward">前进</button><button @click="go">任意跳转</button><button @click="pushView">push to detail</button><button @click="replaceView">replace to detail</button></div></div><div class="row"><div class="col-xs-2 col-xs-offset-2"><div class="list-group"><!-- SPA 实现页面切换,可使用 push 或 replace 属性控制跳转记录模式 --><router-link active-class="active" class="list-group-item" to="/home">Home</router-link><router-link active-class="active" class="list-group-item" to="/about">About</router-link></div></div><div class="col-xs-6"><div class="panel"><div class="panel-body"><!-- 展示组件 --><router-view></router-view></div></div></div></div></div> </template><script> export default {name: "App",methods: {back() {this.$router.back();},forward() {this.$router.forward();},go() {// 参数:正数表示前进,负数表示前进this.$router.go(-1);},pushView() {this.$router.push({path: '/home/message/detail',query: {id: '1',title: 'spring'}})},replaceView() {this.$router.replace({path: '/home/message/detail',query: {id: '2',title: 'bear'}})}} } </script><style> button {margin: 5px; } </style>
5.6 缓存路由组件
通过切换隐藏的路由组件,默认立即销毁,需要展示时再挂载。可以通过配置修改销毁规则
<!-- 切换页面时对 include 中指定的组件进行缓存,不销毁 -->
<keep-alive include="News"><router-view></router-view>
</keep-alive><!-- 缓存多个路由组件,页面切换时不销毁 -->
<keep-alive :include="['News', 'Message']"><router-view></router-view>
</keep-alive>
5.7 路由生命周期钩子
<script>
export default {name: "News",data() {return {opacity: 1}},// 当前路由组件激活时回调此钩子activated() {this.timer = setInterval(() => {this.opacity -= 0.01;console.log(this.opacity)if (this.opacity <= 0) {this.opacity = 1;}}, 20);},// 当前路由组件失活时回调此钩子deactivated() {console.log('deactivated')clearInterval(this.timer)},
}
</script>
5.8 路由守卫
-
全局路由守卫:
src/router/index.js
import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home";const router = new VueRouter({routes: [{path: '/home',component: Home,meta: {title: '主页'}},{path: '/about',component: About,meta: {isAuth: true,title: '关于'}}] })// 全局前置路由守卫 router.beforeEach((to, from, next) => {// 是否需要权限检查if (to.meta.isAuth) {if (localStorage.getItem('username') !== 'root') {alert('权限不足,无权查看')} else {next()}} else {next();}});// 全局后置路由守卫 router.afterEach((to, from) => {// 切换页面标题名称if (to.meta.title) {document.title = to.meta.title} else {document.title = '主页'} });export default router
-
独享路由守卫:
src/router/index.js
import VueRouter from 'vue-router' import About from "@/pages/About"; import Home from "@/pages/Home";export default new VueRouter({routes: [{path: '/home',component: Home,meta: {title: '主页'}},{path: '/about',component: About,meta: {isAuth: true,title: '关于'},// 独享路由守卫只有前置,没有后置beforeEnter(to, from, next) {if (localStorage.getItem('username') !== 'root') {alert('权限不足,无权查看')} else {next()}}}] })
-
组件内路由守卫:
<script> export default {name: "Message",// 组件路由守卫,进入该组件之前调用beforeRouteEnter(to, from, next) {if (localStorage.getItem('username') !== 'root') {alert('权限不足,无权查看')} else {next()}},// 组件路由守卫,离开该组件之前调用beforeRouteLeave(to, from, next) {} } </script>
5.9 history 与 hash
- hash 模式:浏览器地址栏
#
及其之后的内容都是 hash 值,不会包含在 HTTP 请求路径中- 地址中永远带着 # 号,不美观
- 兼容性较好
- 将地址通过第三方 app 分享,若 app 检验严格,则地址会被标记为不合法
- history 模式:
- 地址干净,美观
- 兼容性相较略差
- 部署上线时需要后端解决 SPA 应用页面刷新时cun’zai
六、Vue3
- vue3 简介
- 使用 vue-cli 创建工程
- 使用 vite 创建工程
- 分析工程结构
- 安装开发者工具
- 初识 setup
- ref 函数 - 处理基本类型
- ref 函数 - 处理对象类型
- reactive 函数
- 回顾 vue2 的响应式原理
- Vue3 响应式原理 - Proxy
- Vue3 响应式原理 - Reflect
- reactive 对比 ref
- setup 的两个注意点
- computed 计算属性
- watch 监视 ref 定义的数据
- watch 监视 reactive 定义的数据
- watch 时的 value 的问题
- watchEffect 函数
- vue3 生命周期
- 自定义 hook
- toRef 与 toRefs
- shallowReactive 与 shallowRef
- readonly 与 shallowReadonly
- toRaw 与 markRaw
- customRef
- provide 与 inject
- 响应式数据的判断
- CompositionAPI 的优势
- Fragment 组件
- Teleport 组件
- Suspense 组件
- vue3 中的其它改变