Vue2学习文档
英文官网:
- Vue2: https://vuejs.org/
- Vue3:https://vuejs.org/
中文官网
- Vue2: https://v2.cn.vuejs.org/
- Vue3: https://cn.vuejs.org/
入门
前言
用来构建用户界面的渐进式的js库
与其它前端JS框架的关联
- 借鉴Angular的模板和数据绑定技术
- 借鉴React的组件化和虚拟DOM技术
特点
-
声明式
无需亲自操作DOM,利用模板描述界面
只要更新data数据,Vue就会自动更新DOM
-
组件化
将功能界面拆分成多组件组合使用,提高代码复用率,更好维护
-
虚拟DOM+DIff算法
实现最小化DOM更新
MVVM
MVVM(Model-View-ViewModel)是一种软件架构设计模式
Vue是MVVM模式的一个实现库
MVVM支持双向绑定,当M层数据进行修改时,VM层会监测到变化,并且通知V层进行相应的修改,反之修改V层则会通知M层数据进行修改
MVVM的组成
-
M: Model 模型
- 数据模型,主要负责业务数据相关
- 包含n个可变数据的data对象
- 作用:给View提供响应式数据
-
V: View 视图
- 模板页面,负责视图相关
- 作用:读取data数据进行动态显示
-
VM: ViewModel 视图模型
-
V与M沟通的桥梁,负责监听M或者V的修改,是实现MVVM双向绑定的要点
-
ViewModel是Vue.js的核心,是Vue的实例对象,简称vm对象
-
作用:model到view和view到model的双向数据绑定
-
Diff算法❗
概述❗
-
什么是diff算法
diff算法就是比较新旧虚拟DOM树,寻找差异的算法。在源码中是通过patch函数来完成的,所以也称为patch算法
-
diff算法比较思路:深度优先,同级比较
-
深度优先即针对一个元素一路深挖下去,广度优先的反面
-
同级比较只进行同级比较,不进行父子级等比较
-
执行过程❗
1.基本执行过程
- 当组件内部的响应式数据发生更新的时候,就会执行vue内部的updateComponent函数,在函数内部先执行
_render
方法生成新的虚拟DOM,把虚拟DOM传递给_update
方法,并调用_update
方法 - 在
_update
函数中,首先定义一个变量保存旧的虚拟DOM(在vm._vnode_属性上),然后再把_update
接受的新的虚拟DOM vNode放在vm的_vnode_属性上,此时在_update
函数中就有了新旧虚拟DOM,最后使用patch方法对新旧虚拟DOM进行比较
function Vue(){
const updateComponent = ()=>{
this._update(this._render())
}
}
function _update(vNode){
//将旧虚拟DOM存下来,以便新虚拟DOM赋值vm._vnode
const oldVnode = vm._vnode;
vm._vnode = vNode;
//使用patch函数进行新旧虚拟DOM比较
patch(oldVnode, vNode)
}
2.patch比较过程
-
patch函数首先使用sameVnode方法专门比较两个节点是否相同(相同:两个虚拟DOM节点的标签类型相同
[input还要比较type类型]
,并且key的值也要相同,如果没有写key,则key的值是undefined) -
如果sameVnode比较两个节点相同,则直接进入更新流程
更新流程:
- 把旧节点的真实DOM拿到新节点位置复用
- 对比新旧节点的属性是否相同,如果不同则更新
- 开始比较子节点
-
如果sameVnode比较两个不相同,则直接遍历新的节点创建新的元素,并直接删除旧的元素
3.比较子元素
- vue使用四个指针分别指向 新旧子节点列表 的 首尾节点
- 首先比较新旧树的头指针,判断是否相同,如果相同则进入更新流程…(即比较两个的子节点,深度优先原则)
- 继续比较两个树的头指针,如果不相同,则比较新旧树的尾指针,如果相同则进入更新流程…
- 如果头指针比较和尾指针比较都不相同,则比较头尾指针是否相同,如果相同则进入更新流程…
- 如果上边的规则都不相等,则会以新树的头指针为基础,循环旧的虚拟DOM节点,如果存在相等则直接拿过进入更新流程,如果找不到则直接拿当前的新的虚拟DOM节点创建真实DOM
- 当新树的头指针超过尾指针的时候,比较结束,如果旧树中存在剩余节点,则删除这个剩余节点
key的作用❗
- 在新旧虚拟DOM对比更新的时候,默认的diff算法是"就地复用"原则
- “就地复用”:多个子节点比较的时候,如果没有添加key属性,则key属性都是undefined,所以每一个新旧DOM的key都是相同的,所以就会简单的按照节点的顺序依次比较(如果新旧节点的顺序发生变化,vue仍然都是创建新节点删除旧节点)
- 我们可以给每一个节点添加一个key属性,方便Vue跟踪每一个元素的身份,从而在diff算法计算的时候可以按照key确定比较节点
- key的作用:高效的更新渲染虚拟DOM
- 不要使用遍历出来的index作为key(index不是稳定性),key的要求是 唯一性!!! 稳定性!!!
简短版Diff算法流程
- 当组件创建和更新的时候,vue会执行内部的update函数,该函数使用render函数生成的虚拟DOM树,将新旧DOM树调用patch方法进行比较找到差异,并更新真实DOM
- 对比的过程被称作diff,vue内部使用patch函数完成对比过程
- 在对比的时候,vue采用深度优先,同级比较的方式
- 在比较的时候,通过虚拟DOM的key和tag来判断是否相同
- 第一:先对根节点的新旧虚拟DOM进行对比,如果相同则将旧节点复用,然后更新数据,递归比较子节点,如果不同则直接根据新的虚拟DOM递归创建新的节点,并且删除旧的DOM
- 第二:比较子节点,首先vue给新旧虚拟DOM都使用了两个指针,分别指向头尾,然后不断向中间靠拢进行对比(头头,尾尾,头尾,尾头,乱序),提高对比性能,在比较的过程中如果发现相同则进入更新流程,否则新增和移除dom
- 按照上边的两点一直递归,直到整个DOM对比完成
Vue扩展插件
- vue-cli: vue脚手架
- vue-router: 路由
- vuex: 状态管理
- vue-lazyload: 图片懒加载
- vee-validate: 表单校验
- vant-ui: 基于vue的UI组件库(移动端)
- element-ui: 基于vue的UI组件库(PC端)
插件配置
谷歌插件
- Vue.js devtools.crx
vsCode插件
-
Vetur
vue-helper
基本使用
使用
<head>
<!-- 1. 引入vue库 -->
<script src="../js/vue.js"></script>
</head>
<body>
<!-- 2. 定义一个容器元素 -->
<div id="app">{{msg}}</div>
<script>
/* 3. 创建vue实例对象 */
new Vue({
// 页面根元素
el: '#app',
// 定义响应式数据
data: {
msg: 'Hello Vue!'
},
// 要解析显示的模板页面, 模板页面中可以读取data数据
// template会将页面挂载的根元素替换掉
template: `
<div>
<p>{{ msg }}</p>
</div>
`
})
</script>
</body>
插值语法
{{ js表达式 }}
- 作用:动态显示表达式的值
- 内部查找的是vue实例对象的属性或方法
- 支持的数据类型:
- 都支持(包括基本类型和对象类型
- 在显示时,会自动将数据转换为字符串值显示
- true / false => 显示对应名称字符串
- undefined / null => 不显示
- ⚠vue的插值语法只在vue实例上找数据
React中的JSX表达式
标签体文本和标签属性值上使用
<p name={表达式}>{表达式}</p>
作用: 动态显示表达式的值
Vue配置项
el-挂载
el: 用来指定模板的根元素
- 选择器字符串
- 元素对象
- 本质调用$mount
挂载vue对象
- 自动挂载——
el: 'selector'
- 手动挂载——
vue.$mount('selector')
挂载后,vue内部才会进行模板的解析/编译生成虚拟DOM,进而生成真实DOM显示
// 创建vue对象
const vm = new Vue({
// 1.自动挂载 => 内部会调用$mount进行挂载
// el: '#root',
data: {
content: 'abc'
}
})
// 2.手动挂载
vm.$mount('#root')
⚠vm的函数必须在挂载之前定义
const vm = new Vue({ data: { content: 'abc' } }) vm.fn = ()=> 'fn函数' vm.$mount('#root');//挂载
data-数据
data: 用来指定可变的数据,模板可以直接读取,对应react的state
- 包含n个数据的对象
- 返回数据对象的函数
响应式更新data数据
⚠vue3中所有数据类型直接修改都有响应式,无需注意以下
基本类型
直接赋值
vue能监听到数据发生的变化,并作出修改
对象类型❗
Vue内部会对data中所有层次属性都通过defineProperty添加getter/setter实现对data数据的深度劫持(监视)
但defineProperty不能监视添加和删除属性,只能监视读取和修改属性
-
修改对象已有属性 => 响应式 => 界面自动更新
-
添加新属性/删除属性 => 不是响应式 => 界面不会自动更新
修改已有属性
直接修改,为响应式
this.obj.yyy = value
添加新属性
Vue.set( target, propertyName, value )
vm.$set( target, propertyName, value )
删除属性
Vue.delete( target, propertyName )
vm.$delete( target, propertyName )
数组类型❗
Vue实现数组数据的响应式,不是通过defineProperty给元素添加getter/setter
而是对数组更新元素的7个方法进行包装:
- push/pop
- unshift/shift
- splice
- sort
- reverse
先调用原生方法对数组进行更新,再去更新DOM
对于其他未实现响应式的数组方法,可以用新数组替换旧数组
template-模板
template: 用来生成真实DOM的模板字符串
- 由: html + vue语法(插值语法 + 指令语法)组成的
- 模板最终会替换掉页面中的空容器元素
- 缺点: 编码没有提示, 代码没有高亮, 不利于编写和阅读
- 解决: 可以不写此配置, 将模板直接写在html标签中
methods-方法
methods:包含n个方法的对象
- methods的所有方法都会被自动添加到vm上
- 方法的this都被绑定指向vm
⚠methods中的方法不要用箭头函数
实现methods
function Vue(options){
this._data = options.data;
this._initData();
this._initMethods(options.methods)
}
Vue.prototype = {
_initData(){},
_initMethods(methods){
Object.keys(methods).forEach(key => {
this[key] = methods[key].bind(this)
})
}
}
computed-计算属性
计算属性监视内部使用的属性的变化,一旦发生变化,计算属性就要重新计算
- 计算属性调用情况
- 初始显示
- 依赖数据发生变化
⚠computed属性在使用时同data属性,无需带上小括号调用,{{ fullName }}
⚠什么时候使用计算属性
- 如果一个数据仅需直接展示,直接展示
- 如果一个数据要进行处理之后才能展示,可以考虑使用计算属性
- 如果模板页面中表达式较长,可以使用计算属性完成
计算属性两种写法
只读-value
computed: {
fullName () {
// setTimeout(() => 计算的结果)
// 进行特定的计算
return 同步计算的结果 => 专门用于显示
}
}
可读可写-get & set
computed: {
fullName () {
get () {
return 计算后的结果
},
// 监视计算属改变, 当fullName被指定了新的值时调用,
set (value) {
// 可以在其中更新其它数据/发请求/保存数据
}
}
}
computed VS. methods
computed | methods | |
---|---|---|
调用情况 | 1.初始显示 2.依赖数据发生变化 | 1.data中的数据发生变化 (只要data中的数据发生变化,vue需要重新解析模板,就会重新调用methods) |
多次读取 | 一次 计算属性有缓存,多次读取只计算一次(只在相关响应式依赖发生改变时才会重新求值) | methods多次读取需要执行多次 |
watch vs. computed
watch | computed | |
---|---|---|
是否可异步 | 可以执行异步操作 监视数据变化后,做一些特定工作, 比如:更新其它数据/保存数据/发请求提交数据 | 只能同步返回结果,不能异步返回计算结果 |
分别适用场景
- method: 事件的回调或者我们封装一些功能函数(更新数据/提示界面/发请求, 一般不用来返回一个要显示的数据)
- computed: 当要显示的数据是由已有n个数据来共同确定(需要有计算过程)的
- watch: 用于当某个数据发生改变时就需要做一些相应工作(比如: 更新其它数据/发请求/保存数据)
watch-侦听器
被监听的属性可以是属性(data)也可以是计算属性(computed)
使用
⚠监听属性首先需要保证有该属性(data)或计算属性(computed)
-
watch配置写法
watch: { firstName: { handler (newVal, oldVal) { console.log('watch2 firstName', newVal, oldVal) // 更新fullName4 setTimeout(() => { this.fullName4 = newVal + '-' + this.lastName }, 1000); } } }
-
$watch方法
vm.$watch('lastName', function (value) { this.fullName4 = this.firstName + '-' + value })
-
watch配置简写
watch: { firstName(newVal, oldVal){ console.log(oldVal) } }
监听配置
说明 | |
---|---|
立即监听 immediate: true | 初始化时就执行一次监听 |
深度监听 deep: true | watch默认不监测对象内部值的改变(监视一层) 配置deep:true可以监测对象内部值改变(监视多层) |
⚠要使用监听配置,必须用一般写法1
{ handler (value, oldValue) {} // 监视的回调 deep: true, // 深度监视 immediate: true, // 初始化执行一次, 后面数据时会再次执行 }
⚠**
immediate
&mounted
**如果某个功能既需要watch监听执行, 又需要初始化(mounted)时执行, 直接给watch设置immediate立即监听
监听对象属性
有两种写法:
["xx.xx"](){}
"xx.xx"(){}
watch:{"person.name"(){}}
data(){
return{
person:{
name:"asa",
age:22
}
}
}
监听多个属性
watch本身不能同时监听多个属性,只能利用computed实现
computed可以同时监听多个属性的变化,并重新计算值
- 让computed返回一个对象,对象包含所有监听的属性,一旦监听的属性发生改变,则computed就会重新计算得到新的属性值
- watch深度监听第1步的computed,只要computed内部有任何变化,watch都可以监听到
computed:{
nameAge(){
return{
name:this.name,
age:this.age
}
}
}
watch:{nameAge(){}}
Vue指令
vue指令:vue中自定义的一些标签属性,都以v-开头
-
作用:对所在标签进行特定操作
-
- 绑定动态属性值
- 绑定事件监听
- 显示/隐藏
- 复制产生多个
v-bind 强制绑定:
基本使用
v-bind可以给给任意标签属性,指定动态的属性值
- 内置属性: value / src / class / style
- 自定义属性: xxx
语法
v-bind:属性名="表达式"
//可简写为
:属性名="表达式"
实例
<body>
<div id="test">
<img :src="imgSrc" yyy="xxx" />
</div>
<script src="../js/vue.js"></script>
<script>
const vm = new Vue({
el: "#test",
data: {
imgSrc: "https://v2.cn.vuejs.org/images/logo.svg",
xxx: "123",
},
});
setTimeout(() => {
vm.imgSrc = "https://sponsors.vuejs.org/images/aircode.png";
}, 1000);
</script>
</body>
style和class动态绑定
style动态绑定
<p :style="{fontSize: '20px', 'backgroun-color': 'pink' color: val}">str</p>
v-bind:style
是一个包含样式属性的对象- 样式命名方式
- 驼峰式 (camelCase)
- 破折号方式 (kebab-case)
- 样式值的单位不能省略
React的style写法
<p style={{fontSize: 20, 'background-color': 'pink'}}>style动态绑定</p>
class动态绑定
:class="xxx"
,xxx可以是字符串、对象、数组
hasA、hasB: boolean,
myClass: 'classA',
myClass2: ['classA', 'classB']
-
类名确定,且只有一个,但不确定有没有 => 三元表达式写法
<p :class="hasA ? 'classA' : ''">情况1</p>
类名确定,且有多个,但不确定有没有 => 对象写法
<p :class="{classA: hasA, classB: hasB}">情况2</p> **类名只有一个,类名不确定时 => 字符串写法** ```js <p :class="myClass">情况3</p> **类名有多个,类名不确定时 => 数组写法** ```js <p :class="myClass2">情况4</p> **有固定/静态类名,也有动态类名 => 可分开写,会自动合并它们** ```js <p class="classC" :class="myClass">aaaa</p>
React的class写法
- 在react中,静态类名和动态类名不能分开,需写在一起
<p class={hasA ? 'classA' : ''}>情况1</p> <p class={hasA ? 'classC classA' : 'classC'}>情况1</p>
批量绑定多个
v-bind="包含多个属性的对象"
<div id="test">
<p v-bind="obj">aaaaaaa</p>
</div>
<script src="../js/vue.js"></script>
<script>
const vm = new Vue({
el: '#test',
data: {
obj: {
a: 1,
b: 2
}
}
})
</script>
v-model 数据双向绑定
- 数据单向绑定(v-bind):data => 页面,数据只能从data流向页面
- 数据双向绑定:data <=> 页面
- 页面能读取data数据进行动态显示,一旦更新数据页面会自动更新
- 当用户改变页面输入时,输入的数据会自动保存到data中
基本使用
<div id="test">
<input type="text" v-model:value="msg" />
<p>{{msg}}</p>
</div>
<script src="../js/vue.js"></script>
<script>
const vm = new Vue({ el: "#test", data: { msg: 123 } });
</script>
v-model本质
监听用户的输入事件,从而更新数据
- 动态value data => view的绑定
- input事件监听 view => data的绑定
也就是双向数据绑定:data数据 和 模板页面
- 页面能读取data数据进行动态显示,一旦更新数据页面会自动更新 data => view 绑定
- 当用户改变页面输入时,输入的数据会自动保存到data中 view => data 绑定
收集表单数据
对于checkbox、radio、option,需要设置value值,选中后会自动将绑定数据更新为选中项的value值
radio
<span>性别: </span>
<input type="radio" id="female" value="女" v-model="gender">
<label for="female">女</label>
<input type="radio" id="male" value="男" v-model="gender">
<label for="male">男</label><br>
checkbox
<span>爱好: </span>
<input type="checkbox" id="basket" value="basket" v-model="hobbies">
<label for="basket">篮球</label>
<input type="checkbox" id="foot" value="foot" v-model="hobbies">
<label for="foot">足球</label>
select & option
<select v-model="city">
<option value="">未选择</option>
<option value="芬兰">芬兰</option>
</select><br>
重置表单
将value值设置为初始值即可
<input type="button" value="重置" @click="reset">
reset () {
this.user = {
username: '',
gender: '女',
hobbies: ['foot'],
city: '',
}
修饰符
v-model.xxx="事件函数"
说明 | |
---|---|
.lazy | 转为change事件进行同步,失焦时触发同步 (v-model默认为input事件,输入即触发同步) |
.number | 将输入值转为数值类型 |
.trim | 过滤输入值的首尾空白字符 |
v-model父子组件绑定双向通信
v-on 事件绑定@
基本使用
一般写法:v-on:事件类型="事件函数"
简写:@:事件类型="事件函数"
- 事件回调函数需要写在methods中
- 事件函数中的this指向的是vue的实例对象vm
事件对象及传参
-
事件函数默认接收一个event对象
<button @click="fn">默认传递event</button> fn(e) { ...code }
-
传递自定义参数
<button @click="fn2('abc')">传递自定义参数</button> fn2(msg) {}
-
传递自定义参数和event对象
<button @click="fn3('abc', $event)">传递自定义参数+event</button> fn3(msg, e) {}
原理
vue在解析模板会自动在事件函数外层包裹一个函数
($event) => { 事件函数 }
修饰符
事件修饰符
在@事件名后, 加.xxx
来对事件监听进行进一步处理
@click.stop="fn"
说明 | |
---|---|
.stop | 停止事件传播 |
.self | 事件只有绑定事件的元素能触发,点击子元素不能触发 |
.prevent | 阻止事件默认行为 |
.once | 只执行一次 |
.capture | 默认绑定的事件都是在冒泡阶段执行,添加capture修饰符可以设置捕获阶段执行 |
按键修饰符
@按钮事件后, 加.xxx
来事件监听进行进一步处理
@keyup.enter="fn"
说明 | |
---|---|
.enter | 监视enter键 |
.keyCode | 监视keyCode对应的按钮,不推荐使用(enter键: 13 |
条件渲染
v-if
条件性地渲染内容
这块内容只会在表达式为true时才渲染
v-if="表达式1"
v-else-if="表达式2"
v-else
-
v-else-if 和 v-else 注意点
-
v-else-if必须用在紧跟在带有v-if的元素后面的元素上,中间不能有其他元素
-
v-else必须用在紧跟在带有v-if或v-else-if的元素后面的元素上,中间不能有其他元素
-
-
可在
<template>
上使用 v-if 条件渲染,最终渲染结果不包含<template>
<template v-if="表达式"></template>
v-show
v-show="表达式"
通过切换display样式来控制渲染内容的显示和隐藏
带有 v-show 的元素始终会被渲染并保留在 DOM 中
区分v-if & v-show
v-if | v-show | |
---|---|---|
false隐藏 | 删除标签 | 修改display为none |
true重新显示 | v-if重新创建 | 修改diplay的样式(不需要创建标签) |
更新显示效率 | 相较慢(需要创建标签) | 更快 |
占用内存 | 相较小 | 相较大 利用空间换时间:隐藏时占用更大的空间,来换取重新显示时更快 |
适用场景 | 1.界面切换频繁 2.DOM较大 | 在模板中初始读取空对象或空数组内部对象中的数据 {a:{}} -> a.b.c (空对象或空数组内部对象为undefined,再向下读取即为读取undefined的数据,会报错) |
v-for 列表渲染
⚠v-for 和 v-if/v-show不能同时用在同一个元素上
问题
- v-for的优先级大于v-if,在编译阶段先v-for生成多个虚拟DOM,任何在v-if去掉多个虚拟DOM,造成性能浪费
- v-for和v-if一起使用,可能结果预期不明确
解决方案
遍历时如果部分DOM条件渲染,使用计算属性解决
定义一个计算属性,返回过滤后的列表,再v-for遍历
遍历时如果全部DOM同时一个条件渲染,把v-if写在外层容器上
避免列表渲染和完全隐藏同步执行导致预期不明确
遍历数组
item:数组项
index:下标
v-for="item in 数组"
v-for="(item, index) in 数组"
遍历对象
item:键值
key:键名
v-for="item in 对象"
v-for="(item, key) in 对象"
遍历字符串
v-for="item in 字符串"
v-for="(item, index) in 字符串"
遍历数值
数值为多少,就遍历多少次
如10,即从item为从1到10,index为从0到9
v-for="item in 数值"
v-for="(item, index) in 数值"
其他指令
v-text & v-html
说明 | |
---|---|
v-text=“content” | 将content设置为标签的innerText |
v-html=“content” | 将content设置为标签的innerHTML |
插值语法相对于v-text更灵活:可以是标签体部分文本
v-once & v-pre
说明 | |
---|---|
v-once | 初始显示后,不再更新,用于不变的数据显示 => 优化内部解析的性能 (v-once显示后不会再更新,即使引用了动态值,动态值更新,也不会变) |
v-pre | 不解析指令/插值语法,直接按结构HTML显示 |
v-cloak
v-cloak存在于元素上直到关联实例结束编译
和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 插值 标签直到实例准备完毕
<div v-cloak>...</div>
<style>
div [v-cloak]{
display:
}
</style>
- 在简单项目中,使用v-cloak来解决屏幕闪动;
- 但在大型、工程化项目(webpack、vue-router)中不用:只有一个空的 div 元素,元素中的内容是通过路由挂载来实现的,这时我们就不需要用到 v-cloak 指令。
Vue响应式原理解析
核心方法
Object.create
ECMAScript5新增的一个静态方法,用来定义一个实例对象
该方法可以指定对象的原型和对象特性,使用现有对象来提供新创建对象的__proto__
语法
Object.create({ 对象原型 }, { 对象配置项... })
//对象配置项详写
{键名: {
value:键值/(get&set),
configurable: true,
//键值是否可以被配置,默认是false,不可被配置(删除操作);
enumerable: true,
//是否可枚举,是否可以被循环,默认值是false
}, 对象配置项2}
-
对象原型-必选——指定原型对象,可以为null
-
对象配置项-可选参数
包含一个或多个属性描述符的 JavaScript对象:
- value:指定属性值
- writable:默认为 false,设置属性值是否可写
- enumerable:默认为 false,设置属性是否可枚举( for/in)
- configurable:默认为false,设置是否可修改属性特性和删除属性
- get:作为该属性的 getter 函数,如果没有 getter 则为undefined。函数返回值将被用作属性的值。
- set:作为属性的 setter 函数,如果没有 setter 则为undefined。函数将仅接受参数赋值给该属性的新值。
具体使用
-
创建一个对象的原型
var obj = Object.create({ myname: "张三" }); console.log(obj); //两种创建方法结果相同 var obj = {}; obj.__proto__ = {myname:"张三"}; console.log(obj);
-
创建一个可控对象
相当于
{myname: '张三', age: 20}
var obj = Object.create( {}, { myname: { // 配置项 value: "张三", // 键值 ; configurable: true, //键值是否可以被配置,默认是false,不可被配置(删除操作); enumerable: true, //是否可枚举,是否可以被循环,默认值是false }, age: { value: 20 }, } ); console.log(obj); delete obj.myname for(var key in obj){ console.log(key); }
-
value值属性可以被拆分成get和set
⚠value 与 get&set两者不可同时存在,一个对象里只能有其中一个
get:当访问、获取属性时触发
set:当设置属性时触发
⚠get&set均为异步执行函数
var str = "张三"; var obj = Object.create( {}, { myname: { // value:"张三", // 不要和get set 一起写 ; get: function () { // 当获取myname属性的时候触发; // 访问 myname属性的时候就是get操作 console.log("get函数被触发了"); return str; }, set: function (newvalue) { // 当设置myname的时候会触发set函数; console.log("设置了属性:", newvalue); str = newvalue; }, }, } ); console.log(obj); console.log(obj.myname);//get操作,获取操作; obj.myname = "修改了"; //重新赋值就是一个set 操作,会触发set函数; console.log(obj);
Object.defineProperty
给对象添加属性,或者修改现有属性,并返回此对象
-
如果指定的属性名在对象中不存在,则执行添加操作
-
如果在对象中存在同名属性,则执行修改操作
-
Object.defineProperty()是vue 2.0 实现响应式的核心函数
语法
Object.defineProperty(obj, prop, { 对象配置项... })
- obj:要定义属性的对象
- prop:要定义或修改的属性键名
- 对象配置项:要定义或修改的属性的描述符,具体与Object.create的相同
具体使用
-
新创建一个可控对象
var obj = Object.defineProperty({},"myname",{ get:function(){ console.log("获取了get触发"); return "张三"; }, set:function(newvalue){ console.log("新值是",newvalue) }, configurable:true, enumerable:true })
-
修改一个单属性对象为可控对象
var obj = { myname: "张三", }; //修改对象为可控对象 var value = obj.myname; Object.defineProperty(obj, "myname", { get: function () { return value; console.log("get"); }, set: function (newvalue) { value = newvalue; }, configurable: true, enumerable: true, });
-
修改一个多属性多层级对象为可控对象
-
由于get&set是异步执行,在for循环修改对象属性时,key会指向最后一个
-
若对象属性也为对象,需递归修改属性
var obj = { myname: "张三", age: 20, obj1: { hobby: "篮球", }, }; function observe(obj){ var keys = Object.keys(obj);//将对象的所有key值提取放在一个数组里 keys.forEach(function (key) { var value = obj[key]; if (typeof value === "object") { observe(obj[key]); //深层次递归修改对象属性 } Object.defineProperty(obj, key, { configurable: true, enumerable: true, get: function () { return value; }, set: function (newvalue) { value = newvalue; }, }); }); }
-
ES6对象新语法
Object.keys(obj)
将对象的所有key值提取放在一个数组里,返回数组
Object.values(obj)
将对象的所有键值提取放在一个数组里,返回数组
-
可控对象实现修改数据时同步渲染页面
function observe(obj) { var keys = Object.keys(obj); keys.forEach(function (key) { var value = obj[key]; if (typeof value === "object") { observe(obj[key]); } Object.defineProperty(obj, key, { configurable: true, enumerable: true, get: function () { return value; }, set: function (newvalue) { if (value !== newvalue) value = newvalue; var appEle = document.querySelector("#app"); appEle.innerHTML = newvalue; //修改触发set后,再次进行渲染 }, }); }); } var obj = { myname: "张三", age: 20, obj1: { hobby: "篮球", }, }; // 初次渲染 var appEle = document.querySelector("#app"); appEle.innerHTML = obj["myname"];
数据代理
- 模板template中读取数据的来源——vm对象自身或原型链上的属性或方法
- Vue中的数据代理:通过vm对象来代理对_data中属性的操作
- 读取vm的属性,读取的是data对象中的属性
- 向vm保存属性,数据保存在data对象中
- Vue中数据代理的好处:模板中可以更加方便的操作_data中的数据
实现原理
遍历data对象中所有的属性,通过Object.defineProperty
给vm添加与data对象中对应的同名属性,由vm来代理内部data对象的属性操作(读/写)
- get方法:读取data对象中的同名属性返回
- set方法:将设置的最新值保存到data对象的同名属性上
实现数据代理
- 遍历data对象中的所有属性, 给vm添加相应的同名属性
- 利用defineProperty方法给vm添加属性
- get:读取data对象中对应的属性值返回
- set:将最新的属性值value保存到data对象的同名属性上
for (let key of Object.keys(vm._data)) {
Object.defineProperty(vm, key, {
get() {
return vm._data[key];
},
set(value) {
vm._data[key] = value;
},
});
}
数据劫持
为了捕获到数据的改变,进而重新解析模板
- _data身上的每一个属性不直接给值,而是都有对应的:reactiveSetter、reactiveGetter;
- 当修改_data上的属性时,该属性对应的reactiveSetter就会调用; 且在reactiveSetter中Vue会:更新数据、重新解析模板;
- 当读取_data上的属性时,该属性对应的reactiveGetter就会调用,返回对应的值;
实现原理
Object.defineProperty
- 遍历
_data
中的数据,得到属性和值 - 重写
_data
上的属性,并且书写为存取器数据 - 当取的时候直接返回当前属性的值,并收集当前的模板信息
- 当设置的时候修改当前属性的值,并通知所有的模板进行重新获取最新的值
实现数据劫持
const data = {
name: "laowang",
age: 18,
};
//变成响应式数据
observer(data);
function observer(target) {
if (typeof target !== "object" || target === null) {
return target;
}
for (let key in target) {
defineReactive(target, key, target[key]);
}
}
function defineReactive(target, key, value) {
//深层次监听
observer(value);
Object.defineProperty(target, key, {
get() {
return value;
},
set(newValue) {
//可能新改的值是一个对象
observer(value);
if (newValue !== value) {
value = newValue;
console.log("更新视图");
}
},
});
}
Vue数据响应式原理
响应性是 Vue的一个核心特性,用于监听视图中绑定的数据,当数据发生改变时视图自动更新。
只要状态发生改变,系统依赖部分发生自动更新就可以称为响应性。
Vue构造函数的创建
-
创建Vue构造函数,并实现数据代理
function Vue(options) { //将options中的内容 添加在vm实例的_data属性上 this._data = options.data; //开始进行数据代理 for (let key of Object.keys(this._data)) { Object.defineProperty(this, key, { get() { return this._data[key]; }, set(value) { this._data[key] = value; }, }); } }
-
实例化Vue得到vm,并传入数据
onst vm = new Vue({ data: { count: 123, user: { name: "xiaowang", age: 18, }, course: ["html", "js", "css"], }, });
数据劫持
在Vue构造函数中,把vm身上的_data属性变成响应式的,我们可以封装一个oberve函数去操作
//1.创建一个Vue构造函数
function Vue(options) {
....
//处理_data数据 把它变成响应式的
observe(this._data);
}
1.observe函数
observe函数首先对_data中的数据进行响应式
- observe函数判断数据是否为对象
- 如果不是对象则直接返回
- 反之进行响应式处理
observe函数主要是先判断,再响应式处理,因此把整个响应式处理提炼为一个Observe类更加方便
function observe(value) {
//如果dataObj不是一个对象,则直接返回
if (typeof value !== "object" || value === null) {
return value;
}
//把响应式处理逻辑 定义为一个类,然后对每一个需要响应式处理的数据操作
new Observer(value);
}
2.Observer类
- 由于进入Observer的对象类型可能是数组也可能是对象,需分类处理
- 如果是对象,则直接开始响应式操作
- 如果是数组,则先遍历数组的值,然后再次调用observe进行响应式处理
- 数据响应式的核心逻辑——使用defineReactive函数进行封装
class Observer {
constructor(obj) {
this.value = obj;
//判断当前的数据是数组还是对象
if (Array.isArray(obj)) {
//如果是数组,则调用observeArray方法,重新对数组操作
this.observeArray(obj);
} else {
//如果是对象,则直接进行响应式操作
this.walk();
}
}
//数组的响应式操作
observeArray() {
for (var i = 0; i < this.value.length; i++) {
observe(this.value[i]);
}
}
//对象的响应式操作
walk() {
//对当前内部的数据开始响应式
Object.keys(this.value).forEach((key) => {
//数据响应式的核心逻辑
defineReactive(this.value, key);
});
}
}
3.defineReactive核心逻辑
//函数中拿到当前需要响应式的属性名,及它所在的对象
function defineReactive(obj, key) {
//根据属性名和对象,得到当前的属性值
let value = obj[key];
//对属性值再次进行observe操作(观察)
observe(value);
console.log(obj, key, "defineReactive");
Object.defineProperty(obj, key, {
get() {
console.log("正在访问", key);
return value;
},
set(newValue) {
console.log("正在设置key", key, newValue);
value = newValue;
//对新的值继续进行observe
observe(value);
},
});
}
依赖收集(订阅发布模型)
以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的
需要再次执行依赖收集,在解析模板代码时,遇到 {{name}} 就会进行依赖收集
实现一个 Dep 类,用于依赖收集和派发更新操作
依赖收集类
class Dep {
constructor() {
//定义一个数组用来保存所有的订阅者(Watcher)
this.subs = [];
}
//收集订阅者方法
depend(watcher) {
this.subs.push(watcher);
}
//通知订阅者方法
notify() {
}
}
收集依赖
- 首先在defineReactive中实例化一个依赖收集类
- 在getter函数中,一旦有watcher(观察者)读取数据,则把这个watcher收集到依赖里
- 在setter函数中,一旦设置,则调用notify,通知所有观察者
function defineReactive(obj, key) {
......
//实例化一个订阅发布模型
let dep = new Dep();
.....
Object.defineProperty(obj, key, {
get() {
.....
//有一个观察者在访问这个数据,所以应该将这个观察者存入订阅发布中心
dep.depend(xxxxx);
.....
},
set(newValue) {
......
//找到订阅发布器 通知观察者
dep.notify();
},
});
}
观察者逻辑
只要在任意位置使用模板语法,就会实例化一个观察者,比如:
new Watcher(vm, "count");
观察者类
class Watcher {
constructor(data, key) {
this.value = data;
this.key = key;
//给Dep设置一个静态变量,其实就是为了全局共享这个Watcher实例,然后我们在defineReactive的getter函数中可以收集到这个watcher
Dep.target = this;
this.get();
//一旦收集完成,就把这个target设置为null
Dep.target = null;
}
//get方法主要是为了获取值,获取值的时候就会defineReactive的getter函数
get() {
return this.value[this.key];
}
//update方法,主要是当defineReactive的setter函数调用的时候,得到最新的值,然后更新视图
update() {
console.log("监听" + this.key + "的wathcer被触发了,新的值是" + this.get());
}
}
完成依赖收集
在defineReactive的getter中完成依赖收集
get() {
if (Dep.target) {
dep.depend(Dep.target);
}
return value;
},
通知订阅者更新
在Dep的notify通知订阅者方法中,开始通知订阅者执行update更新
notify() {
this.subs.forEach((watcher) => {
watcher.update();
});
}
$nextTick
在vue中,数据更新是同步的,用户界面Dom更新是异步的
- 数据改变:更新用户界面(数据类型改变也算改变,如“1”变为数字1
- 数据不变:不会触发更新用户界面
数据更新,Dom会出现更新渲染,而渲染Dom需要时间
若此时操作Dom,会操作无效,需要等Dom渲染完后,再进行操作
$nextTick(()=>{})
$nextTick
会等待用户界面渲染mounted完成后,再触发参数中的回调函数
实例
操作Dom
等待用户界面渲染mounted完成后,在操作Dom
const inputRef = ref();
const toEdit = (row: attrValueItemType) => {
row.isEdit = true;
nextTick(() => {
inputRef.value.focus();
});
};
父子组件
父组件在mounted中请求接收数据后传给子组件;子组件在mounted中进行渲染
而由于父组件的mounted在子组件的mounted之后运行,故子组件在渲染时数据为undefined
解决办法:使用watch观察需渲染数据的变化,并在$nextTick中再创建轮播图渲染
watch: {
// 等 carouselList 数据发生变化时触发
carouselList() {
$nextTick(()=>{
new Swiper(".swiper", { });
})
},
},
Vue生命周期
render配置
Vue内部解析tempalte模块自动生成render函数
该函数执行返回虚拟dom,后期调用该函数来更新(初始化执行一次 + 每次更新都执行)
render(createElement: () => VNode) 返回VNode
-
接收的参数
内部定义好的用来创建虚拟DOM/Node的函数
-
返回值:虚拟DOM/Node
用来生成真实DOM显示界面 / 与旧的虚拟DOM进行diff比较
实现最小化DOM更新
-
调用时机
- 初始化:调用render, 得到虚拟DOM => 生成真实DOM,替换el元素显示
- 更新数据:调用render, 得到新的虚拟DOM => 与旧的虚拟DOM进行diff比较, 实现最小化DOM更新
1.init 初始化
-
做一些关于事件和生命周期的初始化准备工作 (不重要)
-
执行 beforeCreate()
- 不能访问data中的属性
- 不能调用methods中的方法
-
初始化 data/methods/computed/watch /props配置
- 实现data的数据代理
- 实现data的数据劫持
- 将methods的方法挂载到vm上,并指定this为vm
-
执行 created()
- 能访问data中的属性
- 能调用methods中的方法
(⚠最早可以访问data、methods的钩子)
2.mount 挂载
-
[编译模板]( 如果有render函数, 不需要此过程
将template字符串编译生成render函数,此时render还不执行
-
执行 beforeMount()
-
调用render函数生成虚拟DOM => 生成真实DOM,并替换页面中的el元素显示 (使用patch函数)
-
执行 mounted()
⚠最早能获取DOM元素的钩子
3.update 更新
update阶段多次执行
-
当data发生更改后,执行 beforeUpdate()
- 此时读取界面,是旧界面
- 读取数据,是新数据
-
调用 render函数产生新虚拟DOM => 执行patch函数(进行新旧虚拟DOM Diff,进行最小化DOM更新)
-
执行 updated()
此时读取界面,是新界面
4.destroy 销毁
- 执行 beforeDestroy()
- 将vm内部的数据代理/数据劫持/重新render/DOM Diff都干掉
- 执行 destroyed()
- 此时vm已销毁,不再具有管理页面的能力
生命周期触发顺序
父组件需等待子组件挂载好后才能挂载好
- 父组件beforeCreate
- 父组件created
- 父组件beforeMount
- 子组件beforeCreate
- 子组件created
- 子组件beforeMount
- 子组件mounted
- 父组件mounted
常用钩子
- created/mounted——【初始化异步操作】
- 发送ajax请求
- 启动定时器
- 绑定自定义事件
- 订阅消息等
- beforeDestroy——【收尾工作】
- 清除定时器
- 解绑自定义事件
- 取消订阅消息等
常见问法
- 哪个钩子中不能访问data中的数据、methods中的方法? —— beforeCreate
- 想给vm上追加一些属性,最早可以在哪个钩子中操作? —— beforeCreate
- data中的数据、methods中的方法,最早可以在哪个钩子中获取? —— created
- 哪个钩子中数据和页面其实是不同步的?—— beforeUpdate、created、beforeMount
- 最早可以获取DOM元素的钩子——mounted
Vue-Cli脚手架
在线文档: https://cli.vuejs.org/zh/
使用vue脚手架创建项目
下载vue-cli
npm install -g @vue/cli
创建vue项目
vue create vue-demo
运行项目
npm run serve
不同版本的Vue
- vue.esm.js——带编译器的版本
- 完整版的Vue,包含:核心功能 + 模板解析器
- 可进行运行时的模板编译,即main.js文件中可以写template
- 缺点:项目的打包文件变大, 一般不用
- vue.runtime.esm.js——不带编译器的版本
- 不能编译main.js中的template,只能通过提供render来渲染App组件
- 优点:打包文件较小, 一般都用此版本
vue单文件组件中的模板是在运行前,由webpack的vue-loader提供的模板编译器来解析的template
scoped(作用域)样式
- 不加scoped的style中的样式是全局样式,可以应用到当前组件外的组件
- 加scoped之后,只能影响当前组件的样式
关闭语法检查
-
忽略后边一行的检查
/* eslint-disable-next-line */
-
忽略之后的检查
/* eslint-disable */
-
关闭某个规则的检查
package.json 或者.eslintrc.js: 全局规则配置文件修改eslint配置
'rules': {'no-new': 'off'}
-
关闭所有检查
vue.confog.js中配置 关闭所有eslint检查
lintOnSave: false
Vue组件化编程
前言&理解
Vue与VueComponent
VueComponent
组件的本质是一个构造函数,名为VueComponent
- VueComponent不是程序员定义的,是Vue.extend()生成的
- Vue.extend()每次生成的都是一个全新的VueComponent构造函数
- 每次使用组件时
<Student></Student>或<Student/>
Vue就会帮我们执行: new VueComponent(options) - 关于this
- 组件配置中
- data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】
- new Vue(options)配置中
- data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】
- 组件配置中
- VueComponent的实例对象,可称之为:组件实例对象
- 组件实例对象就是一个小型的vm(组件实例对象也有:数据劫持、数据代理、生命周期…)
Vue与VueComponent的关系
VueComponent.prototype.proto === Vue.prototype
-
关系:组件对象的原型对象的原型对象是Vue的原型对象
-
原理:VueComponent.prototype = Object.create(Vue.prototype);
-
目的:组件实例对象(vc)可以访问到 Vue原型上的属性、方法
vue模板
输入vue2
或vue3
可快捷获取vue模板
- 打开设置->用户代码片段
-
搜索打开vue.json并配置
{ "Printtovue": { "prefix": "vue2", "body": [ "<template>", " <div></div>", "</template>", "", "<script>", "export default {", " name: '',", "}", "</script>", "", "<style scoped>", "", "</style>", "" ], "description": "快速创建vue单文件组件" }, "Printtovue3": { "prefix": "vue3", "body": [ "<template>", " <div></div>", "</template>", "", "<script lang=\"ts\">", "export default {", " name: '',", "}", "</script>", "", "<script lang=\"ts\" setup>", "</script>", "", "<style scoped>", "", "</style>", "" ], "description": "快速创建vue单文件组件" } }
基本使用
Vue组件分类
- 非单文件组件:一个文件中可以定义n个组件,文件后缀可以是:.html
- 单文件组件:一个文件中只定义一个组件,文件后缀是:.vue
创建组件
基本使用
data❗
组件的data配置项必须是一个返回对象的函数
- 如果data配置是对象 => 这个组件多个实例共享同一个data对象,一旦一个组件对象的数据更新,多个界面都会更新
- 如果data配置是函数 => 这个组件的多个实例都有自己data对象,各自更新各自的
vue的官方回答:
- 当一个组件被定义,data必须声明为返回一个初始数据对象的函数
- 因为组件可能被用来创建多个实例,如果data仍是一个纯粹的对象,则所有的实例将共享引用同一个数据对象
- 通过提供data函数,每次创建一个新实例后,能够调用data函数,从而返回初始数据的一个全新副本数据对象
组件名
- 一个单词组成——School
- 命名(首字母大写):School
- 使用:
<School> 或 <school>
- 多个单词组成——MySchool
- 命名(CamelCase命名):MySchool
- 使用:
<MySchool> 或 <my-school>
注册组件
-
局部注册
只能在对应的模板中进行使用
components: {组件标签名: 组件}
-
全局注册
任意模板中都能使用
Vue.component('组件标签名', 组件)
1.非单文件创建组件
定义组件
- Vue.extends
const School = Vue.extend({
template:`
<div>
<h2>学校名称:{{name}}</h2>
</div>`,
data(){
return {
name:'尚硅谷',
}
}
})
- Vue.extends简写
const App = {
name:'App',
components:{School},
template:`
<div>
<School/>
</div>
`,
}
2.单文件创建组件(工程化方式)
组件单文件组成
- template
- script
- style
main.js-入口文件
/*
1.该文件是脚手架的入口文件
2.一般我们在该文件中:去创建vm,并接管容器
*/
//引入Vue
import Vue from 'vue'
import App from './App'
new Vue({
el:'#demo',//指定当前的vm接管哪个容器
components:{App}, //注册App组件
template:'<App/>'
})
组件.vue
<template>
<!-- 组件的结构 -->
<div class="school">
<h2>学校名称:{{name}}</h2>
</div>
</template>
<script>
//此处编写:组件的配置对象并暴露出去
export default {
name:'School',
data() {
return {
name:'尚硅谷',
}
},
}
</script>
<style scoped>
/* 组件的样式 */
</style>
App.vue-根组件
<template>
<div class="app">
<School/>
</div>
</template>
<script>
//引入School组件
import School from './components/School'
export default {
name:'App',
components:{School},
}
</script>
<style>
/* 组件的样式 */
</style>
ref属性
给元素或子组件注册标识,便于获取组件中的标签对象或子组件对象(id的替代者)
- 应用在html标签上获取的是真实DOM元素
- 应用在组件标签上是组件实例对象
使用
-
给html标签打标识
<h1 ref="xxx">.....</h1>
-
给标签组件打标识
<School ref="xxx"></School>
获取
获取标签对象或组件对象
this.$refs.xxx
操作dom的方式
ref(获取模板
自定义指令(操作模板
ui组件库不太建议用,不好获取dom
⚠操作DOM必须等待页面渲染mounted完之后再进行
组件通信❗
组件间通信
重要通信方案 props 父 -> 子 自定义事件 子 -> 父 v-model 父 <-> 子 v-bind:xxx.sync 父 <-> 子 插槽 v-slot 父 <-> 子 (通信的是标签数据) vuex / pinia 兄弟或祖孙 次要通信方案 全局事件总线 兄弟或祖孙 provide / inject 祖孙 $attrs
/$listeners
父 <-> 子 $children
/$refs
/$parent
父 <-> 子 $root
共享数据,将数据放置在vm上
通过$root
获取
props
- 父向子: 非函数属性
- 子向父: 函数属性
- 祖孙之间: 需要进行多次逐层传递
使用
传递的分别为字符串、数值、data数据
<MyComponent name='tom' :setName='setName' :set-age="1"></MyComponent>
声明接收
接收的所有属性都会成为组件对象的属性
prop名格式为kebab-case,接收时可以使用小驼峰接收
set-age => setAge
-
数组 [属性名]
props: ['name', 'setName']
-
对象: {属性名: 属性值类型}
props: { name: String, setName: Function }
-
对象: {属性名: {type: 属性值类型, required: true}}
props: { name: { type: String, required: true, default:xxx } }
⚠props对象写法注意点:
无required,最好有default值
使用default
基本数据类型直接写值
default: xxx
,引用数据类型需使用函数返回值
default: () => []
原理同组件data使用函数返回对象一样
props只读⚠
-
基本数据类型的props一旦修改,直接就会报错!
-
对于对象类型的props:
-
- 若修改的是整个对象(地址值变化),会报错;
- 若修改的是对象中的属性,则不会报错(有时候程序员会利用这个小bug)
自定义事件emit
实现子向父通信
基本使用
-
父组件——绑定自定义事件
给子组件标签绑定自定义事件, 指定事件回调函数, 用于接收数据并处理
-
常用方式
<Child @事件名="回调函数fn">
-
其他方式
this.$refs.footer.$on('事件名', 回调函数fn) $on前面为指定要绑定的组件
⚠**$event的理解**
-
简单理解:代表event对象
-
真正理解:代表事件回调函数的第一个参数
- 原生DOM事件:事件回调函数参数event,所以$event就是event
- Vue自定义事件:事件回调函数参数要看触发事件所传入的参数,$event就是传入的第一个参数
//触发名为setCurrentImgIndex的事件时,会执行右侧的表达式 //$event表示事件对象,这里将事件对象的值赋给currentImgIndex,从而更新currentImgIndex的值 @setCurrentImgIndex="currentImgIndex = $event" //上下两种写法一样 @setCurrentImgIndex="handleSetCurrentImgIndex" handleSetCurrentImgIndex(newIndex) { this.currentImgIndex = newIndex; }
⚠**.native的使用**
<child-comp @click.native="fn"/>
- 原生标签可直接@绑定DOM原生事件
- 而组件绑定的事件都被认为是自定义事件(即使名字和DOM原生事件一样),需要自己触发
- 要被组件绑定DOM原生事件,需要使用
.native
修饰符
-
-
子组件——分发触发自定义事件(事件名, 数据)
this.$emit(事件名, 数据)
事件处理方法
-
绑定事件
$on(事件名, 回调函数)
-
分发触发事件
$emit(事件名, 数据)
-
解绑事件
$off() / $off(事件名) / $off(事件名, 回调函数)
-
绑定事件,触发一次后解绑
$once(事件名, 回调函数)
v-model
表单项使用,每个组件只能用一次
⚠v-model 原理:双向数据绑定
- 普通input、textarea元素:绑定value属性和input事件
- radio、checkbox元素:绑定checked属性和change事件
- select元素:绑定的value属性和change事件
- 给组件绑定,绑定value属性和input事件
- value属性的值就是表达式的值
- input事件用来修改值的,触发事件传入的第一个参数就是要改的值
本质:动态value属性与自定义input监听(接收子组件分发的数据更新父组件数据)
-
父组件
<CustomInput v-model="name"/> <!-- 等价于 --> <CustomInput :value="name" @input="name=$event"/>
-
子组件
<input type="text" :value="value" @input="$emit('input', $event.target.value)"> props: ['value']
.sync
非表单项使用
在原本父向子的基础上增加子向父
.sync本质:通过事件监听来接收子组件分发过来的数据并更新父组件的数据
使用
-
父组件使用v-bind绑定数据向子组件传递prop
v-bind:xxx.sync = "xxx2"
-
子组件接收prop数据
props:["xxx"]
-
子组件绑定自定义事件
触发自定义事件,将父组件的数据更新为自定义事件传入的第一个参数
@click="$emit('update:xxx', arg)"
实例
//父组件
<ImageList
:skuImageList="goodsDetail.skuImageList"
:currentImgIndex.sync="currentImgIndex"
/>
//子组件
<img
:class="{
active: currentImgIndex === index,
}"
:src="img.imgUrl"
@mouseenter="$emit('update:currentImgIndex', index)"
/>
:currentImgIndex.sync="currentImgIndex"
-
绑定props数据:
currentImgIndex
-
绑定自定义事件:
update:currentImgIndex
当触发自定义事件传入的第一个参数,就会将
currentImgIndex
数据更新
插槽slot
当需要传入子组件的内容不是单纯的数据,而是标签结构 => 使用插槽
slot可以实现组件的动态内容
- 插槽的默认内容:当没有向插槽传入内容时, 显示默认内容
- 插槽的分类
- 默认插槽: 默认name为default
- 具名插槽: 指定自定义name
v-slot:xxx
可以简写为#xxx
默认插槽
slot无name属性,默认名称为default
-
设置插槽
<child-com>组件
<template> <div> <slot></slot> </div> </template>
-
使用插槽
只有一个slot时,可以不指定slot名字
<template> <div> <child-com>写入slot的内容,可传入html</child-com> </div> </template>
具名插槽
-
设置插槽
<child-com>组件
<template> <div> <slot></slot> <slot name="content"> <h4>插槽的默认内容</h4> </slot> </div> </template>
-
使用插槽
- 有多个slot时,使用必须指定slot名字,不指定则视为使用默认插槽
- 具名插槽不能省略template(
v-slot
只能添加在<template>
上)
<template> <div> <child-com> <h3>默认插槽的内容</h3> <hr/> <!-- template v-slot:content --> <template #content> <h3>content插槽的内容</h3> </template> </child-com> </div> </template>
作用域插槽
当父组件传入的插槽内容需要使用子组件的数据时使用
-
子组件传递插槽属性
将要传递的数据使用v-bind绑定在
<slot>
上,这些attribute 被称为插槽 prop<slot :row="todo" :$index="index"></slot>
-
父组件接收
父组件通过v-slot接收传过来的数据,可以解构
<template v-slot:content="scope">{{scope.row}}</template> //解构数据使用 <template v-slot:content="{row, $index}">{{row}}</template>
vuex
放置共享数据
全局事件总线
- 类似于PubSub
- 可以实现任意组件间通信
1.自定义事件实现
能用内部的尽量用内部自定义实现,避免引入第三方库
-
main.js——确定事件总线
new Vue({ // 最早能访问vm对象的勾子 beforeCreate () { // 将vm对象指定为事件总线对象, 并挂载到Vue原型对象上 => 让所有组件都直接可见 Vue.prototype.$bus = this }, render: h => h(App), }).$mount('#app')
-
监听绑定事件组件
-
绑定事件监听——mounted
在mounted中绑定
this.$bus.$on('事件名', 事件函数fn) fn (data){}
-
解绑事件——beforeCreate
在beforeCreate中解绑
this.$bus.$off('事件名')
-
-
分发触发事件组件
this.$bus.$emit('事件名', 数据)
2.事件处理库 mitt
vue3推荐使用,vue3废弃了$on语法
https://www.npmjs.com/package/mitt
-
下载
npm i mitt
-
引入
import mitt from 'mitt'
-
在main.js主入口文件创建事件发射器并绑定到Vue原型对象
Vue.prototype.$emitter = mitt()
-
事件操作
-
分发事件
this.$emitter.emit(事件名, 数据)
-
绑定事件
this.$emitter.on(事件名, fn) fn (data){}
-
解绑事件
this.$emitter.off(事件名)
-
3.消息订阅发布库 pubsub-js
https://www.npmjs.com/package/pubsub-js
-
下载
npm i pubsub-js
-
引入
import PubSub from 'pubsub-js'
-
在main.js主入口文件绑定到Vue原型对象
Vue.prototype.$PubSub = PubSub
-
消息操作
-
发布消息
this.$PubSub.publish(消息名, 数据)
-
订阅消息
this.$PubSub.subscribe(消息名, (msgName, data) => {})
-
取消订阅
this.$PubSub.unsubscribe(消息名)
-
$attrs
/ $listeners
祖孙通信
多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用这个,比如父组件向孙子组件传递数据时
$attrs
-
包含父作用域里除 class 和 style 除外的 props 属性集合
-
实现当前组件的父组件向当前组件的子组件通信(祖孙间通信)
-
通过 this.$attrs 获取父作用域中所有符合条件的属性集合
-
通过 v-bind=“$attrs” 将父组件传入的n个属性数据传递给当前组件的子组件
$listeners
-
包含父作用域里 .native 除外的监听事件集合
-
实现当前组件的子组件向当前组件的父组件通信 (孙向祖通信)
-
通过v-on=“$listeners” 将父组件绑定给当前组件的事件监听绑定给当前组件的子组件
$children
/ $refs
/ $parent
不推荐使用
$refs
- 实现父组件向指定子组件通信
- $refs是包含所有有ref属性的标签对象或组件对象的容器对象
- 使用: 通过 this.$refs.child 得到子组件对象, 从而可以直接更新其数据或调用其方法更新数据
//父组件
<button @click="updateChild1">累加Child1的count</button>
<Child1 ref="child1"></Child1>
updateChild1() {
console.log(this);
this.$refs.child1.count += 1;
},
$children
- 实现父组件向多个子组件通信
- $children是所有直接子组件对象的数组(不包含孙子组件)
- 使用: 通过this.$children 遍历子组件对象, 从而可以更新多个子组件的数据
$parent
- 实现子组件向父组件通信
- $parent是当前组件的父组件对象
- 使用: 通过this.$parent 得到父组件对象, 从而可以更新父组件的数据
provide / inject
实现祖孙组件间直接通信
-
在祖组件中通过provide配置向后代组件提供数据
provide() { return { count: this.count, }; },
-
在后代组件中通过inject配置来声明接收数据
inject: ["count"]
注意:
- 不太建议在应用开发中使用, 一般用来封装vue插件
- provide提供的数据本身不是响应式的 ==> 父组件更新了数据, 后代组件不会变化
- provide提供的数据对象内部是响应式的 ==> 父组件更新了数据, 后代组件也会变化
provide / inject的响应式写法
-
方法1:传递的参数用一个方法返回
provide: function() { return { newName: () => this.name } }
-
方法2:把需要传递的参数定义成一个对象
provide: function() { return { // 传递一个对象 obj: this.obj } }
$root
- vue所有组件实例身上都有一个$root属性,它们指向整个项目的根组件,一般也就是App组件
- 可以借助$root做事件总线类似的传值方式
//发送数据组件
this.$root.$emit("hello", "HelloWorld")
//接受数据组件
this.$root.$on("hello",name => {
console.log("hello",name);
})
scoped(作用域)样式❗
scoped样式
scoped让样式只能影响当前组件的标签及子组件的根标签
**原理:**scoped的2个变化
- 标签:给当前组件的所有标签和子组件的根标签添加一个data自定义属性
-
样式:选择器的右侧添加一个data自定义属性的属性选择器
-
=> 目标标签必须要有这个自定义属性 =>
-
只有当前组件和子组件根标签才有这个对应的属性
-
样式穿透-深度作用域选择器❗
用于设置影响子组件(UI组件)的子标签的样式
>>> 选择器
原理
- 将data自定义属性移动到
>>>
的位置 - 将最右侧的属性选择器移到左侧, 对目标元素就没有自定义属性的要求 => 就可 以匹配子组件的子标签了
.title {
color:aqua;
}
.box >>> .title {
color:aqua;
}
>>> .title {
color:aqua;
}
Vue ajax
项目开发中一般使用axios
使用位置
- 初始化请求: mounted 或 created
- 用户操作界面元素(如: 点击按钮): 在事件回调中
- 数据改变后请求: 监视的回调中
跨域代理配置
vue.config.js
devServer: {
proxy: { // 代理服务器的作用: 转发请求
// 前缀路径, 请求的路径中必须包含此路径
'/dev-api': {
target: 'https://api.github.com', // 目标接口地址
changeOrigin: true, // 让接口服务器端读取的请求源基础地址是接口服务器的地址
pathRewrite: { // 去掉/dev-api
'^/dev-api': ''
}
},
}
}
mixin混入
用来复用多个组件间相同的js代码
-
将重复的代码(带配置)抽取到单独的模块中, 并暴露
export const myMixin = { data () { return { m: 3 } }, methods: { fn () { console.log('fn') } } }
-
在组件中引入mixin对象, 并通过mixins来配置
import {myMixin} from '@/mixins' mixins: [myMixin] //mixins的配置项为数组
-
使用
this.代码名
即可调用this.fn()
-
如果组件的代码与mixin的重名了,会优先使用自己的
自定义功能
自定义过滤器
对要显示的数据进行特定格式后再显示
- 过滤器也可以接收额外参数、多个过滤器也可以串联;
- 不修改原本的数据, 而是产生新的对应的数据;
- 过滤器中的this永远都是undefined
基本使用
注册过滤器
-
全局过滤器
Vue.filter('过滤器名称', (value, 任意多个参数) => { //value即需要过滤格式化的值 //根据value和其它参数进行计算产一个新的值 return 新值 })
-
局部过滤器
{ name:'School', filters:{ fn(){ 局部过滤器处理函数 } } }
使用过滤器
{{表达式 | 过滤器1(任意多个数据) | 过滤器2(任意多个数据)}}
//表达式即指value,要过滤格式化的值
步骤
-
注册过滤器
-
引入过滤器——main.js
import './filters'
-
使用过滤器
自定义指令
对普通 DOM 元素进行底层操作
基本使用
-
自定义指令
-
全局自定义指令
Vue.directive()
// 注册一个全局自定义指令 `指令名` Vue.directive('指令名', (el, [binding]) => { 根据binding中value去操作el元素 })
-
局部自定义指令
通过配置项的 directives属性配置自定义指令函数
- 参数
- el:当前指令作用的元素
- binding:
binding.value
属性代表当前指令的值
-
-
使用组件
<div v-指令名[="表达式"]>
- 处理指令的函数默认调用的时机:初始化 & 更新
- ⚠注意点
- 指令定义时不加v-,但使用时必须要加v-;
- 指令名如果是多个单词,要使用kebab-case命名方式;
- 指令的回调函数中this是undefined;
自定义指令的钩子函数
自定义指令的第二个参数可配置所有钩子函数
Vue.directive('指令名',{
bind(){},
inserted(){},
update(){},
componentUpdate(){},
unbind(){}
})
钩子函数
bind
:初始化指令的时候执行inserted
:被绑定元素插入父节点时调用- 要删除元素,必须有父元素
- 固在bind钩子函数中无法删除,再早只能在inserted中删除
update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前componentUpdate
:指令所在组件的 VNode 及其子 VNode 全部更新后调用unbind
:只调用一次,指令与元素解绑时调用
自定义指令的简写:自定义指令的第二个参数不书写配置对象,而是直接写为一个函数,则这个函数默认是钩子函数中的bind函数(即基本使用的写法
插件
插件通常用来为 Vue 添加全局功能
在Vue语法的基本上,提供一些新的语法
插件的功能范围没有严格的限制——一般有下面几种:实例方法/静态方法/组件/指令/过滤器
- 添加全局方法或者 property。
- 添加全局资源:指令/过滤器/过渡等。
- 通过全局混入来添加一些组件选项。
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。
自定义插件
对象插件
对象必须要有一个intall方法
const myPlugin = {
//Vue.use会自动调用install
install (Vue) {
通过Vue来扩展语法
}
}
函数插件
函数本身就是install方法
function myPlugin (Vue) {
通过Vue来扩展语法
}
使用插件
-
引用并安装
use会调用插件对象的install或插件函数
Vue.use(引入的插件)
-
使用插件扩展的语法
实例
-
main.js——在
new Vue
之前引入并安装插件// 引入对象插件 import myPlugin from './myPlugin' // 安装插件 Vue.use(myPlugin)
-
myPlugin/index.js
import Msg from './Msg.vue' import dayjs from 'dayjs' const myPlugin = { // 当安装插件时自动调用, 在此方法扩展vue的语法 install (Vue) { console.log('install') // 添加静态方法 Vue.staticFn = function () { console.log('staticFn') } // 添加实例方法 Vue.prototype.$fn = function () { console.log('$fn') } // 注册全局指令 Vue.directive('upper-text', (el, binding) => { console.log('upper-text', binding) // 给元素指定文本内容(需要转成大写) el.innerText = binding.value.toUpperCase() }) // 注册全局过滤器 Vue.filter('DateFormat', function DateFormat(value, formatStr='YY-MM-DD HH:mm:ss') { console.log('DateFormat', value, formatStr) // return 'abc' // return moment(value).format('YYYY-MM-DD HH:mm:ss') return dayjs(value).format(formatStr) }) // 注册全局的组件 Vue.component('Msg', Msg) } } export default myPlugin
-
App.vue
<template> <div> <p>{{startTime|DateFormat}}</p> <p v-upper-text="msg"></p> <Msg/> </div> </template> <script> import Vue from 'vue' export default { name: 'App', data () { return { startTime: Date.now()-1000, msg: 'What You See!' } }, mounted () { Vue.staticFn() this.$fn() }, } </script>
Vuex
Vuex官网
- github站点: https://github.com/vuejs/vuex
- 在线文档: https://vuex.vuejs.org/zh-cn/
Vuex五大核心
- state
- getter
- mutation
- action
- module 模块化
Vuex概述
作用:管理多个组件共享状态 / 实现任意组件间通信
组成部分
- state:集中式管理的数据(多个组件共享的数据)
- getters:只读计算属性数据(一定会依赖state)
- mutations:如果更新数据需要进行异步操作,定义使用action
- actions:如果更新数据直接更新即可,直接定义mutation
- modules:模块
state
存储store的状态数据(多个组件共享的数据
定义
state: {
xxx: value值,
}
使用
$store.state.xxx
getters
根据state数据计算产生数据的方法的对象,类似计算属性computed
定义
getters: {
yyy (state) {
return 根据state计算产生一个新值
}
}
使用
$store.getters.yyy
mutations
直接更新state的方法(回调函数)
mutation调用完后,vuex devtool会直接同步更新state数据
定义
- 参数1:state对象
- 参数2(可选):接收的参数,形参(由action或组件提供)
mutations: {
aaa (state, data) { // data由action或组件提供 }
}
使用
$store.commit("mutation名称", [传入的参数(可选)])
⚠vuex中的mutations可以写异步操作,但不建议,因为工具中的数据没有响应式变化,不会响应式更改
actions
间接同步/异步更新状态数据的方法(定时器, ajax)
定义
-
参数1:context对象,一般直接解构出
{commit, state}
使用(commit用于触发执行mutation)- context就是一个小型的store对象:commit、dispatch、state、getters等内容(一般只需要用commit和state)
-
参数2(可选):接收的参数,形参(由组件提供)
actions: {
bbb ({commit, state}, data) { //data由组件提供
// 异步操作/逻辑操作
commit('mutation名称', data)
}
}
⚠mutation的命名
action调用的mutaion,名称和action一样,但大写
action:
login
mutation:
LOGIN
使用
$store.dispatch("action名称", [传入的参数(可选,只能传入一个参数)])
⚠dispatch的返回值为promise对象,promise的状态由所调用的action决定
基本使用
-
下载vuex
npm i vuex@3
-
创建store
//1.引入 import Vue from 'vue' import Vuex from 'vuex' //2.安装vuex Vue.use(Vuex) //3.创建store const store = new Vuex.Store({ state: {}, getters: {}, mutations: {}, actions: {}, }) //4.暴露store export default store
-
主入口文件配置/挂载store
挂载后,在组件中可通过this.$store得到store对象
import store from './store' new Vue({ render: h => h(App), store, // 注册store => 将store对象添加到Vue原型对象上的$store => 让所有组件通过$store得到store对象 }).$mount('#app')
-
组件中读取或更新状态
//读取 this.$store.state.xxx 读state数据 this.$store.getters.yyy 读取getters计算属性数据 //更新 this.$store.dispatch('action名称', 数据) 触发action调用 this.$store.commit('mutaion名称', 数据) 触发mutation调用
Vuex模块化
按照功能将数据分为多个模块进行管理
-
定义vuex模块
-
将不同功能模块相关的state/getters/mutations/actions的配置单独定义一个vuex的子模块文件 store/modules下
-
开启vuex模块的namespaced(命名空间)
开启命名空间,隔离不同模块的actions和mutations
-
state、getters变为了多模块的结构(不开启时不是)
$store.getters( '模块名/getter名称') $store.getters.模块名('getter名称') $store.state.模块名('state名称') $store.getters.模块名.getter名称 $store.state.模块名.state名称
-
在组件中dispatch或commit时, 要指定模块的名称:
$store.commit( '模块名/mutaion名称') $store.dispatch( '模块名/action名称')
-
在vuex子模块中
- mutaion中的state不是总状态,是当前模块的state
- actioin中执行commit时,不需要指定模块名
-
export default { namespaced: true, // 开启命名空间 => 触发mutation或action调用时必须先指定模块名称 state: {}, getters: {}, mutations: {}, actions: {}, }
-
-
整合注册vuex模块
new Vuex.Store({ ..., modules: {moduleA, moduleB} })
映射函数简化编码
频繁使用vuex中的某项,使用map映射更简洁
- ⚠使用前需先引入
import {mapState} from 'vuex' 2. **mapState、mapGetters在computed中映射** 3. **mapMutations、mapActions在methods中映射**
mapState
使用:$store.state.模块名.count
-
对象写法
...mapState({ count2: 'count' }) // 返回值对象: {count2 () {return this.$store.state['count']}}
-
数组写法
...mapState(['count']) // 返回值对象: {'count' () {return this.$store.state['count']}}
-
模块化写法
...mapState('模块名', ['count'])
mapGetters
-
对象写法
...mapGetters({ evenOrOdd2: 'evenOrOdd' }) // 返回值对象: {evenOrOdd2 () {return this.$store.getters.evenOrOdd}}
-
数组写法
...mapGetters(['evenOrOdd']) // 返回值对象: {evenOrOdd(){returnthis.$store.getters.evenOrOdd}}
-
模块化写法
...mapGetters('模块名', ['evenOrOdd'])
mapMutations
-
对象写法
...mapMutations({ handleIncrement2: 'increment', handleDecrement2: 'decrement', })
-
数组写法
...mapMutations(['increment', 'decrement'])
-
模块化写法
...mapMutations('模块名', ['increment', 'decrement'])
mapActions
-
对象写法
...mapActions({ handleIncrementIfOdd2: 'incrementIfOdd', handleIncrementAsync2: 'incrementAsync' })
-
数组写法
...mapActions(['incrementIfOdd', 'incrementAsync'])
-
模块化写法
...mapActions('模块名', ['incrementIfOdd', 'incrementAsync'])
Vuex数据持久化
-
vuex的状态数据是保存内存中的,浏览器刷新或关闭后就释放了
-
故保存在vuex实例store里的数据是会丢失的
-
解决:将vuex的状态数据保存到 local中
-
可使用vuex-persistedstate插件实现状态数据持久化
-
下载插件
npm install vuex-persistedstate --save
-
使用
const store = new Vuex.Store({ ..., //1.保存所有模块的状态 plugins: [createPersistedState()] //2.通过path指定只存储counter模块的state plugins: [ createPersistedState({ paths: ['counter'] }) ] })
-
Vue-Router
http://router.vuejs.org/zh-cn/
前言
vue-router
- vue的一个插件库,
- 用于路由页面管理和实现SPA
- 基于vue的项目基本都会用到此库
路由
- 一个路由就是一个映射(对应)关系(key:value)
- key为路由路径path, value可能是function/component
- 路由又分为后台路由和前台路由2种
后台路由
node服务器端路由, value是function
用来处理客户端提交的请求并返回一个响应数据
- 注册路由:app.get(path, function(req, res))router.get(path, function(req, res))
- 当node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
前台路由
浏览器端路由,value是component
当请求的是路由path时, 浏览器端没有发送http请求, 但界面会更新显示对应的组件
当浏览器的path变为/about时, 当前路由组件就会变为About组件
路由器&路由
- 路由: 一组key-value的对应关系route
- 路由器: 管理多个路由router
⚠注意区分vue中的router和route
- router 包含路由导航的方法
- route 包含参数数据
路由原生实现
读懂,会讲即可
代码不用记
hash路由原生实现
利用location对象的hash语法来实现的
- 使用location.hash进行hash地址的变更
- 使用onhashchange事件监听hash的变更,在回调中根据最新hash路径找到对应的路由组件显示
<script>
// 路由表
const routes = [
{ path: "/home", component: "<h2>欢迎来到首页</h2>" },
{ path: "/center", component: "<h2>我真的不是大冤种</h2>" },
{ path: "/hot", component: "<h2>武汉尚硅谷XXX拿到30k offer</h2>" },
];
//给所有的button绑定点击事件
for (var i = 0; i < oBtns.length; i++) {
oBtns[i].onclick = function () {
// 更新location对象的hash值
location.hash = this.dataset.path;
};
}
//js提供了一个hashchange事件,可以监听hash地址的改变
//并且历史记录改变的时候,也是hash的改变,也会触发hashchange事件
window.onhashchange = function (e) {
//拿到当前的hash location.hash #/home => /home
const hashPath = location.hash.slice(1)
//去路由表中找到对应的hash地址,加载内容
routes.forEach((route) => {
if (route.path === hashPath) {
oView.innerHTML = route.component;
}
});
};
</script>
history路由原生实现
利用history对象的pushState/replaceState语法和onpopstate事件 来实现的
- 在点击切换路由时:
- 调用history的pushState来添加一个新的路由路径
- 根据这个路径查找对应的路由组件更新显示
- 绑定popState事件, 根据当前路径查找对应的路由组件更新显示
- 注意: popState事件只能监视浏览的前进和后退
<script>
// 路由表
const routes = [
{ path: '/home', component: '欢迎来到首页' },
{ path: '/center', component: '我真的不是大冤种' },
{ path: '/hot', component: '武汉尚硅谷XXX拿到30k offer' },
];
//给所有的button绑定点击事件
for (var i = 0; i < oBtns.length; i++) {
oBtns[i].onclick = function () {
// 调用history对象的pushState方法来添加一个state (路由路径),可以无刷新的改变地址
history.pushState(null, null, this.dataset.path);
// 得到手动遍历查找对应的路由组件去更新显示
routes.forEach((route) => {
if (route.path === this.dataset.path) {
oView.innerHTML = route.component;
}
});
};
}
// 监视浏览器的前进和回退的, 不能监视pushState导致的变化
// 监听历史记录改变的事件 popstate
window.onpopstate = function () {
//当历史记录改变后,获取最新的地址 location.pathname
console.log(location.pathname);
//点击完按钮之后,直接遍历routes 改变content内容
routes.forEach((route) => {
if (route.path === location.pathname) {
oView.innerHTML = route.component;
}
});
};
</script>
hash模式&history模式区别❗
-
hash模式
- hash模式把前端路由的路径用#拼接在真实URL后面
- 当用户切换路由的时候使用
location.hash
的方式切换hash值 - #后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发hashchange事件
- hash模式的浏览器兼容性较好,但看起来不够优雅
-
history模式
- history模式用到了HTML5中的history API,可直接更新浏览器URL地址而不用发请求,不带#
- 用到了history API:replaceState、pushState、back、forward和go 5个方法。
- 使用window的
popState
事件可以监听历史记录的改动,并加载一个组件 - history兼容性不如hash模式,而且浏览器在刷新的时候会按照路径发送真实的资源请求,因此在线上部署基于historyAPI的单页面应用的时候,一定要后端配合支持才行
- 后端:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面
基本使用
-
安装 vue-router
npm i vue-router@3
-
定义路由组件
和以前创建组件一样创建
-
创建路由器, 注册路由
//引入Vue import Vue from "vue"; //引入VueRouter import VueRouter from "vue-router"; //引入组件 import Login from "../components/Login"; import Home from "../components/Home"; //应用路由器 Vue.use(VueRouter); //创建router实例对象,去管理一组一组的路由规则 const router = new VueRouter({ routes: [ { path: "/login", //路径 component: Login, //组件 }, { path: "/home", component: Home, }, ], }); //暴露router export default router;
-
创建 vm 时注册路由器配置
import Vue from "vue"; import App from "./App"; //引入路由器 import router from "./router"; Vue.config.productionTip = false; new Vue({ el: "#root", render: (h) => h(App), router, // 此处传入注册路由器 });
-
通过
<router-view>
指定路由展示位置<template> <div> <!-- 指定在此显示当前路由组件 --> <router-view></router-view> </div> </template>
-
通过手动修改地址来测试切换路由
⚠注意点
路由组件通常存放在views或pages文件夹,一般组件或公共组件通常存放在components文件夹
通过切换“隐藏” 了的路由组件,默认是被销毁掉的,需要的时候再去创建
组件实例的$route属性:存储着当前路由信息(当前路径、路由参数)
组件实例的$router属性:整个应用的路由器,包含跳转路由, 动态注册路由等方法
不可直接修改$route.query、params的值
修改后相当于请求参数不变,导航到同一个地方,会出问题
多级嵌套路由
通过路由实现组件的嵌套展示,叫做嵌套路由
-
创建子路由组件
- views/Home/Course/index.vue
- views/Home/Game/index.vue
- views/Home/News/index.vue
-
配置路由规则,使用 children 配置项注册子路由
routes: [ // 路由 path component {path: '/login', component: Login}, { path: '/home', component: Home, // 注册二级子路由 children: [ {path: '/home/news', component: News}, {path: 'game', component: Game}, {path: 'course', component: Course}, ] }, // 当请求项目根路径时, 自动跳转/login路由路径 { path: '/', redirect: '/home' } ]
-
在父路由组件中指定
<router-view>
来显示子路由组件<template> <div> <h1>Home</h1> <router-view></router-view> </div> </template>
路由重定向
将指定路径重新定向到已有路由
通过路由规则的redirect属性指定一个新的路由地址,设置路由重定向
-
在自身路由中定义redirect
{ path: "/home", component: Home, // 写法一: 当访问/home时, 自动跳转到/home/course redirect:"/home/course", children: [ ... ] }
-
在子路由中定义redirect
children: [ // 写法二: 当访问/home时, 自动跳转到/home/course // ''空串相对路径即指当前的父路由路径 {path: '', redirect: '/home/course'}, ],
声明式&编程式路由导航
声明式导航
利用**<router-link>
**实现路由跳转
功能 | 值 | |
---|---|---|
to | 指定要跳转的目标位置location | 1. string类型:path字符串,可以用相对路径值,且匹配忽略大小写 2.object类型:{name, path} |
replace | 指定是否为replace模式 | 默认为false,即push模式 |
active-class | 指定当前路由链接的类名 | 默认为router-link-active |
tag | 指定生成的html标签 | 默认为a,已弃用 |
to
-
string类型
<router-link to="/home/course">course</router-link> <router-link to="game">game</router-link>
-
object类型
<router-link :to="{ path: '/home/course' }">course</router-link> //设置为replace模式 <router-link :to="{ path: 'game' }" replace>game</router-link> //通过路由的name指定跳转的路由,每个路由的name为唯一标识,相当于id <router-link :to="{ name: 'News' }">news</router-link>
active-class
1.active-class
只能修改单个路由链接的类名
<router-link to="/home/news" active-class="active">News</router-link>
//只有路由指向为/home/news的导航链接,激活后添加类名为active
2.linkActiveClass
全局修改当前路由链接的类名
const router = new VueRouter({
// 路由表
routes: [ ... ],
// 全局修改当前路由链接的类名
linkActiveClass: 'active2'
})
tag
tag指定生成的html标签,目前已弃用
<router-link>slot</router-link>
中自带插槽- a为默认插槽值,如需使用其他html标签,只需在其中写明即可
<router-link>
<button>导航到首页</button>
</router-link>
编程式导航
利用router对象的push或replace方法实现路由跳转
获取方法
组件中通过 this.$router得到router对象
$router.push(location)
$router.replace(location)
location的值
-
string类型
path字符串, 可以用相对路径值, 且是忽略大小写匹配的
<button @click="$router.push('/home/course')">course</button> <button @click="$router.push('game')">game</button>
-
object类型
{name, path}
<button @click="$router.replace({ path: '/home/course' })">course</button> <button @click="$router.replace({ path: 'game' })">game</button> //通过name指定跳转路由 <button @click="$router.replace({ name: 'News' })">news</button>
router其他跳转方法
- back(): 回退到上一个路由
- forward(): 前进到下一个路由
- go(n): 前进或后退指定个数的路由
编程式导航重复跳转路由报错❗
适用于面试题
- 开发时遇到过什么印象深刻的问题?
场景: 手里有2个项目(一个老的, 一个新的), 老的没这个问题, 但新的就有这个问题
解决: 百度/查看文档 => 最后在vue-router官方仓库的Issue(提交的仓库问题)
编程式导航跳转同一个路由,且携带参数不变时,会报错
而声明式不会报错
方式一
原因
vue-router版本问题
-
3.1.0之前,push方法没有返回值
- router.push(location) void
- router.push(location, onResolved, onRejected) void
-
3.1.0之后,push方法如果没有指定回调函数, 返回值为promise对象
- router.push(location).then(onResolved, onRejected) => 此语法重复跳转到当前路由就会抛出失败的promise错误
注意:旧的语法(传了回调)没有问题,只有新的语法(没有传入回调)才有问题
解决
在路由表中配置
通过重写VueRouter原型对象上的push/replace方法, 来解决重复跳转报错的问题
const originPush = VueRouter.prototype.push
VueRouter.prototype.push = function (location, onResolved, onRejected) {
console.log('push', onResolved, onRejected)
debugger
// 如果传入了回调, 用的旧语法, 不会有错误的, 调用原函数进行处理
if (onResolved || onRejected) {
originPush.call(this, location, onResolved, onRejected)
} else { // 没有传回调, 用的新语法, 可能会抛出失败的promise错误
return originPush.call(this, location).catch(() => {
console.log('error')
})
// originPush.call(this, () => {})
}
}
为什么声明式导航不报错?
默认传入一个成功的回调, 用的是旧的语法
方式二
原因
vue对window.history做了二次封装,判断新路径和旧路径是否相等,相等就报错
解决
使用原生history来push,就不会报错了
$router.history.push()
路由组件传参
1.params
-
路由表配置params路径参数
:xxx
{ name: 'Course', path: 'course', component: Course, children: [ { name: 'CourseItem', path: 'item/:id/:name', // 必须要有冒号占位 component: CourseItem } ] },
-
路由跳转,携带params参数
⚠传递params参数时,to的对象写法只能用name,不能用path
<!-- to字符串写法 --> <router-link :to="`/home/course/item/${c.id}/${c.name}`">{{c.name}}</router-link> <!-- to对象写法 --> <router-link :to="{name: 'CourseItem', params: {id: c.id, name: c.name}}"> {{c.name}} </router-link>
-
目标路由组件中读取params参数
$route.params.xxx
<li>课程ID: {{ $route.params.id }}</li> <li>课程名称: {{ $route.params.name }}</li>
2.query
-
路由跳转,携带params参数
<!-- to的字符串写法 --> <router-link :to="`/home/course/item?id=${c.id}&name=${c.name}`"> {{c.name}} </router-link> <!-- to的对象写法 --> <router-link :to="{ name: 'CourseItem', query: {id: c.id, name: c.name} }"> {{c.name}} </router-link> //传递query参数,可以用path <router-link :to="{ path: '/home/course/item', query: {id: c.id, name: c.name} }"> {{c.name}} </router-link>
-
目标路由组件中读取query参数
$route.query.xxx
<li>课程ID: {{ $route.query.id }}</li> <li>课程名称: {{ $route.query.name }}</li>
3.props❗
面试问得多
1.设置props参数
在路由的props配置项中配置参数
-
对象:专门指定自定义数据
//对象: props: {a: 1, b: 2} => 专门指定自定义数据的 { name: 'CourseItem', // 一定指定name path: 'item/:id/:title', // params参数占位 component: CourseItem, props: {a: 1, b: 2} }
-
布尔值(true):专门将params参数映射成props
//布尔值(true): props: true => 专门将params参数映射成props //对象: props: {a: 1, b: 2} => 专门指定自定义数据的 { name: 'CourseItem', // 一定指定name path: 'item/:id/:title', // params参数占位 component: CourseItem, props: true }
-
函数:传递自定义,params参数和query参数
//函数: props: route => ({自定义/params/query}) => 传递自定义, params参数和query参数 //对象: props: {a: 1, b: 2} => 专门指定自定义数据的 { name: 'CourseItem', // 一定指定name path: 'item/:id/:title', // params参数占位 component: CourseItem, props: route => ({c: 2, id2: route.params.id, title2: route.query.title2}) }
2.获取props参数
目标路由组件通过props配置项接收props参数,读取同读取props一样
props: ['a', 'b', 'id', 'title', 'c', 'id2', 'title2']
4.meta元信息
路由配置项meta,可以用来给特定路由组件指定需要的数据
1.注册路由时配置meta
{
...
meta: {
isHideFooter: true, // 标识隐藏footer
}
},
2.读取meta参数
$route.meta.xxx
实例
设置除login外,其他路由均显示footer部分
-
注册路由时,配置meta
{ name: 'Login', path: '/login', component: Login, meta: { isHideFooter: true, // 标识隐藏footer } },
-
在App组件中根据$route.meta.isHideFooter来判断是否显示Footer界面
<template> <div> <!-- 指定在此显示当前路由组件 --> <router-view></router-view> <div class="footer" v-if="!$route.meta.isHideFooter">这是页面的Footer</div> </div> </template>
监视路由参数变化
使用watch监听$route,只要路由变化就调用执行
路由变化,路由中的参数也会变化
watch: {
$route: {
handler (value) {
console.log(`根据${value.params.id}发请求获取详情信息`)
},
immediate: true, // 初始化就执行一次
}
}
动态组件
vue内容,非vue-router内容
vue的动态组件
- Vue提供了一个
<component>
组件,用来做动态组件 <component>
是一个占位符,接受一个is属性
,is属性
的值是一个 字符串类型 的已经被引入和注册的组件的名称- 通过改变is强制绑定的值,来切换替换
<component>
的组件
<template>
<button @click="comp = News">新闻</button>
<button @click="comp = Play">娱乐</button>
<component :is="comp"></component>
</template>
缓存组件keep-alive
vue提供的,不是vue-router提供的
- 路由切换时,隐藏组件实质是销毁组件,显示组件实质是挂载组件
- 缓存路由组件让不展示的路由组件保持挂载,不被销毁(DOM 结构会移除,但支撑带结构的组件不被销毁)
keep-alive基本使用
keep-alive 缓存路由组件
用<keep-alive>
将<router-view>
包裹,隐藏组件时不会将组件销毁,组件依然挂载着
功能 | 值 | |
---|---|---|
include | 指定缓存路由 | 组件名(组件配置中的name属性) |
exclude | 指定不缓存路由 | 组件名(组件配置中的name属性) |
max | 指定缓存路由的最大缓存数 | ⚠1.当前激活路由也算在缓存数中 2.按照最新使用的排序来缓存 |
<!-- 缓存所有对应的路由组件对象 -->
<keep-alive>
<router-view></router-view>
</keep-alive>
<!-- 缓存指定的一个 -->
<keep-alive include="Login">
<router-view></router-view>
</keep-alive>
<!-- 缓存指定的多个 -->
<keep-alive :include="['Login','Home']">
<router-view></router-view>
</keep-alive>
keep-alive钩子函数
使用keep-alive组件缓存后,多了两个钩子函数
名称 | 调用时间 | |
---|---|---|
activated | 激活时 | 1.mounted之后调用第一次 2.导航来到该路由时调用 |
deactivated | 失活时 | 离开路由调用 |
路由懒加载
路由懒加载的原理即异步组件
异步组件
将应用分割成小一些的代码块,并且只在需要时才从服务器加载一个模块
组件导入分类
组件的导入分为两种
-
静态导入模块
import utils from './utils'
- 静态导入将所有模块打包在一起
- 主要为了提高首页的访问检验(更快), 访问首页时, 需要加载的打包文件更小了
-
动态导入
import('./utils')
-
单独打包 code split
-
在注册组件的时候,可以进行异步导入,即异步组件
components: { Hello: () => import("./components/Hello.vue"), },
-
vue异步组件❗
使用import模块化静态引入vue组件,webpack在打包构建时,会把所有的js都打包到一起,里面包含许多暂时没有使用的模块,这就会导致包的体积过大,造成进入首页时需要加载的内容过多,出现长时间白屏现象
- vue允许以一个函数的方式定义组件,函数内部使用import方法动态引入某个模块,并将结果返回
- webpack在打包时,遇到import动态引入,会将import动态引入的资源进行单独打包
- vue只有在这个组件需要被渲染的时候才会触发该函数,且把结果缓存起来以便未来重渲染
- 函数内import方法返回promise实例,当import引入成功,promise变为成功状态,就可以渲染当前组件了
路由懒加载
更高效
在路由表中,使用异步导入组件即可
- 外层函数开始不执行,请求对应的路径时才会执行
- 执行函数进才会请求加载对应的打包文件
const Home = () => import('@/views/Home')
const Login = () => import('@/views/Login')
const Course = () => import('@/views/Home/components/Course')
- 懒加载的缺点:访问其它路由更慢了 => 需要发请求加载对应的打包文件
- 解决:预加载 => 提前加载后面需要其它的打包文件
路由导航守卫
官网
- https://v3.router.vuejs.org/zh/guide/advanced/navigation-guards.html
⚠注意点
刷新页面也是路由跳转,相当于从根路由跳转到当前路由
(如当前处于路由
/home
处,刷新页面,from为/
,to为/home
)参数或查询的改变并不会触发进入/离开的导航守卫
- 可以通过观察
$route
对象来应对这些变化- 或使用
beforeRouteUpdate
的组件内守卫
路由导航守卫:当路由跳转时生效
基本使用
以 router.beforeEach
全局前置守卫为例
const router = new VueRouter({ ... })
// 注册一个全局前置守卫
router.beforeEach((to, from, next) => {
// ...
})
每个守卫方法接收三个参数:
-
to: Route
:即将要进入的目标路由对象 -
from: Route
:当前导航正要离开的路由 -
next: Function
:一定要调用该方法来 resolve 这个钩子执行效果依赖
next
方法的调用参数:-
next()
:跳转到to指定的目标路由 -
next('/')
或者next({ path: '/' })
:跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航
可以向
next
传递任意位置对象,且允许设置诸如replace: true
、name: 'home'
之类的选项以及任何用在router-link
的to
prop 或router.push
中的选项。
-
确保
next
函数在任何给定的导航守卫中都被严格调用一次它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错
全局导航守卫
(最常用):对所有路由生效
- beforeEach 全局路由前置守卫(路由跳转之前触发)
- beforeResolve 全局路由解析守卫
- afterEach 全局路由后置守卫(路由跳转之后触发)
在路由配置中配置全局导航守卫:router/index.ts
import routes from "./routes";
const router = createRouter({
routes,
});
router.beforeEach((to, from, next) => {
console.log("全局前置守卫");
next();
});
router.beforeResolve((to, from, next) => {
console.log("全局解析守卫");
next();
});
router.afterEach((to, from) => {
console.log("全局后置钩子");
});
export default router;
路由独享守卫
只对某个路由生效
- beforeEnter
在路由表中配置路由独享守卫:router/routes.ts
export default [
{
path: "/home",
name: "Home",
component: () => import("../views/Home/index.vue"),
beforeEnter(to: any, from: any, next: any) {
console.log("item路由独享守卫");
next();
},
},
];
组件内守卫
只对某个组件生效
- beforeRouteEnter
- beforeRouteUpdate (2.2 新增)
- beforeRouteLeave
在组件中配置组件内守卫:
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Item",
beforeRouteEnter(to, from, next) {
console.log("item组件被进入了");
next();
},
beforeRouteUpdate(to, from, next) {
console.log("item组件更新时守卫");
next();
},
beforeRouteLeave(to, from, next) {
console.log("Item组件被离开");
next();
},
});
</script>
路由守卫导航解析流程❗
只要导航跳转,全局守卫就会调用(即使导航到同一个组件,全局守卫也会调用
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
切换路由表模式
一共有3种模式:
- hash
- history
- abstract:渲染后台的路由时使用
const router = new VueRouter({
//配置路由表模式
mode: 'history', // hash abstract
// 路由表
routes: [ ... ],
})
Scroll Behavior
https://v3.router.vuejs.org/zh/guide/advanced/scroll-behavior.html
-
vue-router单页应用,只是切换了路由组件,滚动条还在原来的位置
-
针对这种情况,vue使用Scroll Behavior来指定跳转路由后滚动条的位置
-
每次路由跳转时,scrollBehavior函数的返回值决定滚动条的位置
基本使用
在路由表中进行配置即可
const router = new VueRouter({
mode: "history", // 路由模式
routes, // 路由配置
// 滚动行为:每次路由跳转时,函数的返回值决定滚动条的位置
scrollBehavior() {
// return 期望滚动到哪个的位置,此处为返回最顶部
return {
x: 0,
y: 0,
};
},
});
Vue动画/过渡
Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果
transition
-
transition
在进入/离开的过渡中,会有 6 个 class 切换:-
当子组件刚进入时,同时添加
v-enter-from
和v-enter-active
,然后删除v-enter-from
-
当子组件进入动画结束后添加
v-enter-to
-
-
transition
可指定name属性,指定后6个class的v替换为name属性值- 如name为xxx =>
xxx-enter-from
- 如name为xxx =>
-
中只能有一个直接子元素
过渡动画
-
在目标元素外包裹
-
要实现动画的元素必须使用v-if或v-show来控制
使用v-if或v-show才能实现消失和显示
-
给xxx-enter-active指定进入的过渡
-
给xxx-leave-active指定离开的过渡
-
给xxx-enter 和 xxx-leave-to 指定隐藏时的样式
<template>
<div>
<button @click="isShow=!isShow">切换</button>
<transition>
<div v-show="isShow" class="box"></div>
</transition>
</div>
</template>
<style scoped>
.box {
width: 100px;
height: 100px;
background: pink;
}
/* 显示和隐藏的过渡样式 */
.v-enter-active, .v-leave-active {
transition: opacity 2s;
}
/* 隐藏时的样式 */
.v-enter, .v-leave-to {
opacity: 0;
}
</style>
<script>
export default {
name: 'App',
data () {
return {
isShow: true
}
}
}
</script>
关键帧动画
- 在目标元素外包裹
- 编写样式
-
- 进入时样式:xxxx-enter-active
- 离开时样式:xxxx-leave-active
<template>
<div>
<button @click="show = !show">Toggle show</button>
<br>
<transition name="bounce">
<p v-if="show">Lorem ipsum</p>
</transition>
</div>
</template>
<style scoped>
p {
display: inline-block;
}
/* 显示动画 */
.bounce-enter-active {
animation: bounce-in .5s;
}
/* 隐藏动画 */
.bounce-leave-active {
animation: bounce-in reverse .5s;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
</style>
<script>
export default {
name: 'App',
data () {
return {
show: true
}
}
}
</script>
多元素过渡
多元素之间的切换很生硬
vue为此提供了过渡模式即mode属性:
in-out
:新元素先进行过渡,完成之后当前元素过渡离开out-in
:当前元素先进行过渡,完成之后新元素过渡进入
<transition name="fade" mode="out-in">
<!-- ... the buttons ... -->
</transition>
动画库Animate.css
-
安装
$ npm install animate.css --save
-
main.js中引入
import 'animate.css';
-
添加类名来使用动画样式
- animate__animated为固定添加类名
- 另一个为指定的动画样式
<h1 class="animate__animated animate__bounce">An animated element</h1>
半场动画
重排重绘
重绘(Repaint)和重排(Reflow)是浏览器渲染页面时的两个关键步骤。
-
重绘是指当元素的样式发生改变,但不影响其布局的情况下,浏览器会重新绘制(repaint)该元素,使其呈现新的样式。
-
重排是指当元素的布局发生改变,浏览器需要重新计算并重新布局(reflow)整个页面或部分页面的情况。重排会导致其他元素的位置和尺寸发生变化。
触发重绘重排
-
修改元素的样式属性:例如修改元素的颜色、背景、字体大小等。
-
修改元素的尺寸属性:例如修改元素的宽度、高度、边距等。
-
修改元素的位置属性:例如修改元素的定位方式、左右上下偏移等。
-
添加或删除元素:例如在页面中添加或删除元素。
-
修改页面的布局结构:例如修改页面的结构或布局方式。
-
获取布局信息数据也可触发
优化策略
触发重绘和重排的操作会导致浏览器重新计算和渲染页面,这些操作可能会影响页面的性能。
为了提高页面的渲染性能,可以采取以下几点优化策略:
- 减少重绘和重排的次数:尽量避免频繁地修改元素的样式和布局属性,可以通过批量修改、使用 CSS 动画等方式减少重绘和重排的次数。
- 使用 CSS3 动画:CSS3 动画使用 GPU 加速,可以减少重绘和重排的开销。
- 使用
transform
和opacity
属性:transform
和opacity
属性可以在不影响布局的情况下改变元素的样式,从而避免重排。 - 使用文档片段(Document Fragment):将多个 DOM 操作封装在文档片段中,然后一次性插入到页面中,可以减少重排的次数。
半场动画实例
利用动画的生命周期勾子来实现半场动画
<template>
<div>
<div class="box">
<button @click="isShow=!isShow">+</button>
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter">
<div class="ball" v-show="isShow"></div>
</transition>
<img class="cart" src="https://img1.baidu.com/it/u=3761831681,4005524739&fm=253&fmt=auto&app=138&f=PNG?w=500&h=500" alt="">
</div>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
isShow: false,
}
},
methods: {
// 进入动画开始前调用
beforeEnter: function (el) {
console.log('beforeEnter')
// 让小球回到初始位置
el.style.transform = 'translate(0, 0)'
},
/*
进入动画开始时调用
在此给el添加过渡相关的样式
*/
enter: function (el, done) {
console.log('enter')
el.offsetLeft // 只要执行触发重排代码, 过渡才会有效果 读取一属性也会触发重排
// 指定过渡样式
el.style.transition = 'all .5s'
el.style.transform = 'translate(450px, 430px)'
done() // 标识动画完成, 触发afterEnter调用
},
// 进入动画结束时调用
afterEnter: function (el) {
console.log('afterEnter')
// 隐藏小球
this.isShow = false
},
}
}
</script>
<style scoped>
.box {
width: 500px;
height: 500px;
border: 1px solid #ccc;
position: relative;
}
.cart {
position: absolute;
right: 0;
bottom: 0;
width: 50px;
height: 50px;
}
.ball {
width: 20px;
height: 20px;
background: red;
border-radius: 50%;
}
</style>
列表动画
在v-for这种场景中,使用 组件
<transition-group>
- 不同于
<transition>
,它会以一个真实元素呈现:默认为一个<span>
(可以通过tag
属性 更换为其他元素) - 过渡模式不可用,因为我们不再相互切换特有的元素
- 内部元素总是需要提供唯一的
key
attribute 值 - CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身
实例
<template>
<div id="list-demo" class="demo">
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>
</template>
<style>
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
</style>
路由跳转动画
https://v3.router.vuejs.org/zh/guide/advanced/transitions.html
可以用 <transition>
组件给<router-view>
添加过渡效果
<transition>
的所有功能在此处都适用
<template>
<div>
<div>
<router-link to="/login">Login</router-link>
<router-link to="/home">Home</router-link>
</div>
<hr>
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style scoped>
/* 显示/隐藏的过渡样式 */
.fade-enter-active, .fade-leave-active {
transition: all .5s;
}
/* 隐藏时的样式 */
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
拓展
手写代码
call & bind
区别
- 同:都是用来改变函数中的this的
- 异
- call立即调用
- bind只有当调用bind返回的函数时才会调用
- 如何选择:主要看函数是否是需要立即调用, 如果不是,只能用bind
实现bind
Function.prototype.bind = function (thisObj, ...args) {
console.log('bind')
// bind中的this是原函数
return (...args2) => {
return this.call(thisObj, ...args, ...args2)
}
}
实现call
Function.prototype.call = function (obj,...args){
if(obj===undefined || obj===null){
obj = window;
}
obj = Object(obj);
// 将原函数添加为obj的方法
obj.fn = this;
// 通过obj来调用方法
const result = obj.fn(...args);
// 删除这个方法
delete obj.fn;
return result;
}
throttle & debounce
在项目中直接使用lodash来防抖节流
Lodash——js库
https://www.lodashjs.com/
防抖、节流应用:
轮播图
触发频繁发送请求
两者关系
-
共同点
解决同一类问题:事件高频触发处理的问题(请求或更新界面的次数太多 )
-
区别——在一段较长的时间内事件高频触发
- 节流:每隔一定时间执行一次 => 执行少量几次
- 防抖:只执行最后一次
节流throttle
判断时间差(当前时间与上一次处理的时间)大于指定时间才调用处理函数
function throttle(callback, time) {
let pre = 0;
return function (e) {
const now = Date.now();
if (now - pre > time) {
callback.call(this, e);
pre = now;
}
};
}
document.querySelector("#btn3").onclick = throttle(fn, 1000);
防抖debounce
事件发生后, 启动延迟定时器来执行处理函数, 但在启动前先清除未执行的定时器
function debounce(callback, time) {
let timer = null;
return function (e) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
callback.call(this, e);
timer = null;
}, time);
};
}
document.querySelector("#btn2").onclick = debounce(fn, 1000);
new
-
创建空obj
-
将obj的
__proto__
指向构造函数的prototype -
call调用构造函数创建实例对象
-
判断构造函数的返回值,若为复杂数据类型返回,简单数据类型返回实例对象
function Person(name, age) {
this.name = name;
this.age = age;
// return 1;
// return [1];
// return {m:1}
// return ()=>{}
}
function newFn(type, ...args) {
const obj = {};
obj.__proto__ = type.prototype;
const result = type.call(obj, ...args);
return result instanceof Object ? result : obj;
}
const person1 = new Person("asa", 12);
const person2 = newFn(Person, "ooo", 13);
console.log(person1);
console.log(person2);
深拷贝
区别深拷贝与浅拷贝
被拷贝的对象/数组的结构:内部要包含了对象/数组
- 浅拷贝不会拷贝包含对象/数组,只会拷贝它的地址值 => 只拷贝一层
- 深拷贝会拷贝包含的所有层级的对象/数组 => 拷贝多层
浅拷贝
对象{...obj}
数组[...arr]
⚠**
obj1 = obj
和obj1 = {..obj}
不同**
深拷贝
项目中一般使用lodash的cloneDeep
json深拷贝
⚠json无法保存函数,函数会丢失,若深拷贝目标中有函数不要使用json深拷贝
json深拷贝适用于请求获取的数据
function cloneDeep1(target) {
return JSON.parse(JSON.stringify(target));
}
递归深拷贝
Object的toString会返回一个字符串"[object 数据类型]"
如Object.prototype.toString.call([1]) =>'[object Array]'
//设置判断数据类型的函数
function targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1);
}
//递归深拷贝函数
function cloneDeep2(target) {
const type = targetType(target);
if(type==="Object"||type==="Array"){
let cloneTarget;
if(type==="Object"){
cloneTarget={}
Object.keys(target).forEach(key=>{
cloneTarget[key] = cloneDeep2(target[key])
})
}else if(type==="Array"){
cloneTarget=[];
target.forEach(item=>{
cloneTarget.push(cloneDeep2(item))
})
}
return cloneTarget;
}
return target
}
生成唯一Id
Date.now() => 不能连续调用 / 并发访问时都有可能产生重复的值
uuid
uuid: 36位的唯一字符串
https://www.npmjs.com/package/uuid
下载
npm i uuid
使用
//1.ES6
import { v4 as uuidv4 } from 'uuid';
uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
//2.commonjs
const { v4: uuidv4 } = require('uuid');
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
nanoid
nanoid: 21位的唯一字符串
https://www.npmjs.com/package/nanoid
一般用这个,相对短些
下载
npm i nanoid
使用
import { nanoid } from 'nanoid'
model.id = nanoid() //=> "V1StGXR8_Z5jdHi6B-myT"
日期格式化库
一般和过滤器结合使用
moment
https://momentjs.com/
import moment from 'moment';
moment(value).format('YYYY-MM-DD HH:mm:ss')
dayjs
https://dayjs.gitee.io/docs/zh-CN/installation/installation
dayjs与moment的语法基本一样, 但体积更小
import dayjs from 'dayjs'
dayjs(value).format('YYYY-MM-DD HH:mm:ss')
Element库
常见的Vue UI组件库
(最流行的Vue UI组件库为Element)
移动端UI组件库
- Vant https://youzan.github.io/vant/#/zh-CN
- Cube UI https://didi.github.io/cube-ui/#/zh-CN
PC端UI组件库
- Element UI https://element.eleme.cn/#/zh-CN
- IView UI https://www.iviewui.com
推荐vscode插件:vue-helper
一般使用
1. 下载
npm i element-ui
2.引入
完整引入
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
按需引入
-
按需引入还需下载功能包
npm i babel-plugin-component -D
-
添加babel相关配置(babel.config.js)
plugins: [ [ "component", // 为babel-plugin-components配置 { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ]
-
引入并只注册需要使用的组件
import Vue from 'vue' import { Button, MessageBox, Message } from 'element-ui' // 注册Button组件, 本质: Vue.component(Button.name, Button) Vue.use(Button) // 将MessageBox的显示确认框的方法confirm搭载到Vue原型对象上 => 所有组件都可以调用显示确认框 Vue.prototype.$confirm = MessageBox.confirm Vue.prototype.$message = Message
3.使用UI组件
根据官网实例使用即可
快捷使用
https://github.com/ElementUI/vue-cli-plugin-element
Element为vue-cli专门提供了Element插件
可以快速地搭建一个基于 Element 的项目,无需自己配置
-
下载
-
指令只要输一遍
vue add element
-
指定完整引入还是按需引入
-
- 指定语言
-
还原App组件的内容
⚠Element插件会同时配置App.vue的内容,需要改回原来写的代码
-
在plugins/element.js执行引入操作
-
使用UI组件
-
重启服务器
由于改了配置,需要重启服务器,避免ui组件样式刷新不出来
Mock/模拟数据接口
mock数据:后端接口还未开发完成,所以需要mock数据
⚠注意:
- 数据结构要找后端要(mock数据结构要和之后后端提供的数据结构一致)
- 后端接口将来会开发好,记得将requestMock改回request,修改请求地址等
mockjs使用
官网
- http://mockjs.com/
- https://github.com/nuysoft/Mock
-
安装mockjs
npm i mockjs
-
定义mock
import Mock from "mockjs"; Mock.mock("url", "请求方法" ,{ 响应数据即response.data })
url:当请求该url时,mock拦截请求,返回第三个参数指定的响应数据
import Mock from "mockjs"; Mock.mock("/mock/getHomeBannerList", "get", { code: 200, message: "", ok: true, data: [ { id: "1", imgUrl: "/images/banner1.jpg", }, { id: "2", imgUrl: "/images/banner2.jpg", }, { id: "3", imgUrl: "/images/banner3.jpg", }, { id: "4", imgUrl: "/images/banner4.jpg", }, ], });
-
主入口main.js中引入,让mock生效
import "./mock/index";
-
准备一个新的请求文件 - requestMock
目的:防止和正常请求request冲突
// utils/requestMock.js const request = axios.create({ baseURL: "/mock", // 只要修改请求前缀,不和/api一致即可 timeout: 20000, });
-
定义请求函数
import requestMock from "@/utils/requestMock"; //获取首页轮播图列表 export const reqGetHomeBannerList = () => { return requestMock({ method: "GET", url: "/getHomeBannerList", }); };
-
组件发送请求,获取数据
Swiper轮播图
- 官网:https://swiperjs.com/
- api文档:https://swiperjs.com/swiper-api
vue2使用
Vue2只能使用这种方法
https://swiperjs.com/get-started
-
安装swiper
$ npm i swiper
-
主入口main.js引入
-
全部引入(加载慢
// import Swiper JS import Swiper from 'swiper'; // import Swiper styles import 'swiper/css'; //在目标组件中创建swiper实例对象 const swiper = new Swiper(...);
-
按需引入
// core version + navigation, pagination modules: import Swiper from 'swiper'; import { Navigation, Pagination, Autoplay } from 'swiper/modules'; // import Swiper and modules styles import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; //在目标组件中创建swiper实例对象 //按需引入时,需要指明引入的模块 const swiper = new Swiper('.swiper', { // configure Swiper to use modules modules: [Navigation, Pagination], ... });
-
-
目标组件中
-
设置swiper布局
<div class="swiper"> <!-- Additional required wrapper --> <div class="swiper-wrapper"> <!-- 轮播内容 --> <div class="swiper-slide">轮播内容 1</div> <div class="swiper-slide">轮播内容 2</div> ... </div> <!-- 下方页面小圆点 --> <div class="swiper-pagination"></div> <!-- 前后翻页 --> <div class="swiper-button-prev"></div> <div class="swiper-button-next"></div> <!-- 滚动条 --> <div class="swiper-scrollbar"></div> </div>
-
创建swiper实例,并设置swiper配置
new Swiper(".swiper", { // 按需引入时需指明引入的模块 modules: [Navigation, Pagination, Autoplay], // 无限轮播 loop: true, // 自动轮播:https://swiperjs.com/swiper-api#autoplay autoplay: { // 操作完后禁止自动轮播 disableOnInteraction: false, // 鼠标移入停止轮播 pauseOnMouseEnter: true, }, // 小圆点: https://swiperjs.com/swiper-api#pagination pagination: { el: ".swiper-pagination", clickable: true, }, // 左右翻页按钮 navigation: { nextEl: ".swiper-button-next", prevEl: ".swiper-button-prev", }, });
-
vue3使用
Vue3可以使用Swiper组件,更简单快捷
https://swiperjs.com/element
使用常见错误
-
使用本地照片404
- 原因
编译打包时没有打包所需的本地图片,所以后面访问时访问不了
- 解决方法
将图片数据放入public目录中即可
public中的文件,脚手架不会处理,会原封不动地打包
-
轮播内容无内容(undefined)时,new Swiper会导致功能错乱
- 原因
父组件在mounted中请求接收轮播图的数据后传给子组件(轮播图所在位置);子组件在mounted中进行渲染
而由于父组件的mounted在子组件的mounted之后运行,故子组件在渲染时数据为undefined
- 解决方法
watch: { // 等 carouselList 数据发生变化时触发 carouselList() { new Swiper(".swiper", { }); }, },
使用watch观察需渲染数据的变化,并在$nextTick中再创建轮播图渲染
-
同时选中多个swiper组件运行,会导致功能错乱
- 原因
new Swiper('.swiper', {})
每个轮播图只能new swiper一次
复用swiper组件,如果使用
.swiper
选择器,那创建的轮播图类名都是.swiper
,就会使同时操作多个轮播图运行,导致轮播功能错误- 解决方法
new Swiper(this.$refs.swiperRef, {})
为避免这种情况,将通过选择器获取swiper容器,改为通过refs获取容器(能使用选择器的地方就能用ref),ref可以获取所在的真实dom元素,故只会操作一个轮播图,不会导致错乱
放大镜
计算比例:剩余距离即比例
<template>
<div class="spec-preview">
<img src="../images/s1.png" />
<div class="event" @mousemove="handleMouseMove"></div>
<div class="big">
<img
src="../images/s1.png"
:style="{
top: -scale * y + 'px',
left: -scale * x + 'px',
}"
/>
</div>
<div
class="mask"
:style="{
top: y + 'px',
left: x + 'px',
}"
></div>
</div>
</template>
<script>
/*
小绿:200 * 200
左图:400 * 400
右图(大图):800 * 800
*/
const MASK_SIZE = 200; // 小绿大小
const SMALL_IMG_SIZE = 400; // 左图大小
const BIG_IMG_SIZE = 800; // 右图大小
export default {
name: "ZoomComp",
data() {
return {
x: 0,
y: 0,
scale: BIG_IMG_SIZE / SMALL_IMG_SIZE,
};
},
methods: {
handleMouseMove(e) {
// 计算小绿的位置:x y
let x = e.offsetX - MASK_SIZE / 2;
let y = e.offsetY - MASK_SIZE / 2;
if (x < 0) x = 0;
if (x > SMALL_IMG_SIZE - MASK_SIZE) x = SMALL_IMG_SIZE - MASK_SIZE;
if (y < 0) y = 0;
if (y > SMALL_IMG_SIZE - MASK_SIZE) y = SMALL_IMG_SIZE - MASK_SIZE;
this.x = x;
this.y = y;
},
},
};
</script>
vee-validate
用于表单验证
官网:https://www.npmjs.com/package/vee-validate
vue2和vue3的使用版本不同,以下为vue2使用
-
安装
npm i vee-validate@3
-
表单校验目标组件引入&注册组件
import { ValidationObserver, ValidationProvider, extend } from "vee-validate"; components: { ValidationObserver, ValidationProvider, },
-
定义表单校验规则
import { required } from "vee-validate/dist/rules"; //校验手机号 extend("phoneRequired", { ...required, message: "手机号是必填项", }); const phoneReg = /^1[3-9][0-9]{9}$/; extend("phone", { // val就是校验表单项的数据 validate(val) { // 返回true通过,返回false就失败 return phoneReg.test(val); }, message: "请填写正确的手机号", }); //校验确认密码 extend("rePasswordRequired", { ...required, message: "确认密码是必填项", }); extend("rePassword", { // val就是校验表单项的数据 validate(val, { password }) { // 返回true通过,返回false就失败 return val === password; }, message: "两次密码输入不一致", params: ["password"], });
-
配置表单
-
使用 ValidateObserver 组件包裹整个表单,并定义提交表单事件和回调
<ValidationObserver v-slot="{ handleSubmit }"> <form @submit.prevent="handleSubmit(register)"></form> </ValidationObserver> methods: { register() { console.log("表单校验通过了"); }, },
-
使用 ValidateProvider 包裹单个表单项,用来定义表单校验规则等
//手机 <ValidationProvider //phoneRequired|phone中的|表示同时校验2个rule rules="phoneRequired|phone" v-slot="{ errors }" tag="div" mode="lazy" > <label>手机号:</label> <input type="text" placeholder="请输入你的手机号" v-model="user.phone" /> <span class="error-msg">{{ errors[0] }}</span> </ValidationProvider> //确认密码 <ValidationProvider :rules="`rePasswordRequired|rePassword:${user.password}`" v-slot="{ errors }" tag="div" mode="lazy" > <label>确认密码:</label> <input type="text" placeholder="请输入确认密码" v-model="user.rePassword" /> <span class="error-msg">{{ errors[0] }}</span> </ValidationProvider> //提交按钮 <div class="btn"> <button>完成注册</button> </div>
-
二维码qrcode
- 在线生成二维码:草料二维码
-
安装
npm install --save qrcode
-
使用
返回的结果url即可用在img的src,会显示二维码图片
import QRCode from "qrcode"; const url = await QRCode.toDataURL(codeUrl);
图片懒加载
vue-lazyload
只适用于vue1和vue2
官网:https://www.npmjs.com/package/vue-lazyload
-
安装
npm i vue-lazyload@1 -S
-
main.js中配置
import VueLazyload from "vue-lazyload"; // 图片需要准备好 const loadImage = require("./assets/imgs/loading.gif"); const errorImage = require("./assets/imgs/error.jpg"); Vue.use(VueLazyload, { preLoad: 1.3, //提前多少预加载 error: errorImage, loading: loadImage, attempt: 1, //请求几次(此处为第1次请求失败后不再请求 });
-
使用
xxx为图片的url,即一般src中写的内容
<img v-lazy="xxx" />
封装v-lazy
https://blog.csdn.net/weixin_43288600/article/details/132239842
-
响应式监听目标元素的可见性
https://www.vueusejs.com/core/useIntersectionObserver/
Web Socket
https://www.yuque.com/xpromise/fontend/sy7xax007n3s7pdg
概念
WebSocket 是 HTML5 提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。
它基于 TCP 传输协议,并复用 HTTP 的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。
特点
服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。
API
构造函数
new WebSocket(url[, protocols])
- url:连接WebSocket服务器地址
- protocols:: 一个协议字符串或者一个包含协议字符串的数组
实例方法
close()
:关闭WebSocket连接send(data)
:发送消息给WebSocket服务器
事件
close
:监听WebSocket关闭的事件error
:监听WebSocket错误的事件message
:监听客户端接收消息的事件open
:监听WebSocket连接好的事件
基本使用
客户端代码
<template>
<div>
<h1>聊天室</h1>
<ul>
<li v-for="(msg, index) in list" :key="index">{{ msg }}</li>
</ul>
<input type="text" v-model.trim="message" />
<button @click="sendMessage">发送消息</button>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
message: "",
list: [],
};
},
mounted() {
// 文档:https://www.yuque.com/xpromise/fontend/sy7xax007n3s7pdg
// 1. 链接上webSocket服务器
this.ws = new WebSocket("ws://192.168.14.81:5000");
// 3. 接受消息
this.ws.onmessage = (e) => {
this.list.push(e.data);
};
},
methods: {
sendMessage() {
if (!this.message) return;
// 2. 点击按钮,向服务器发送消息
this.ws.send(this.message);
this.list.push(this.message);
this.message = "";
},
},
};
</script>
服务端代码
-
下载
npm i ws
-
使用
// 如果想在nodejs平台使用ES6模块化语法,只需在package.json写上"type": "module", import { WebSocketServer } from "ws"; // 创建ws服务器 // ws://localhost:5000 const wss = new WebSocketServer({ port: 5000 }); // connection 事件:监听客户端的链接事件 wss.on("connection", function connection(ws) { // ws 链接上的客户端对象 // 监听客户端向服务器发送消息的事件 ws.on("message", function message(data) { // data 就是消息的内容 console.log("客户端向服务器发送的消息", data.toString()); // 将消息转发其他所有人 // wss.clients.forEach(ws => ws.send(data.toString())) wss.clients.forEach((client) => { if (client === ws) return; client.send(data.toString()); }); }); });