基础知识部分
生命周期函数
概念:在生命周期中的某一时刻自动执行的函数
beforeCreate:在实例生成之前会自动执行的函数
created:在实例生成之后会自动执行
beforeMount:在组件内容被渲染到页面之前自动执行的函数
mounted:在组件内容被渲染到页面之后自动执行的函数
beforeUpdate:当 data 内的数据发生变化时会立即自动执行的函数
updated:当 data 内的数据发生变化,且已经在页面渲染后,会立即自动执行的函数
beforeUnmount:当页面中存在 dom 对象失效时,自动执行的函数
unmounted:当页面中存在 dom 对象失效时,且 dom 完全销毁后,自动执行的函数
逻辑图
1、在 beforeCreate 生命周期函数执行前,会进行事件和生命周期的初始化(init events & lifecycle);
2、在 beforeCreate 生命周期函数执行结束后,会去初始化一些数据双向绑定、依赖注入等信息(init injections & reactivity);
3、完成上述操作后,会执行 created 生命周期函数;
4、对 template 模板转换成 render 函数,如果没有定义模板,而是直接写在 html 中,也会加载转换成 render 函数;
5、执行 beforeMount 生命周期函数;
6、对 render 函数中,对节点上数据表达式引用的数据进行渲染(节点相当于 dom 节点);
7、上述的渲染结束后,会执行 mounted 生命周期函数。
Vue3 中的生命周期函数
在 Vue2 中,生命周期函数是定义在应用上的,也就是通过 Vue.createApp
创建的应用上,直接用对应mounted(){}
方法即可实现,但是在 Vue3 中,使用的 ComponentAPI,没有了 Vue.createApp
的步骤,转而使用的 setup 函数,这个是有在使用生命周期函数,就需要用如下代码格式来实现。
<script setup>
import { onMounted } from "vue";
onMounted(() => {
// 逻辑代码
});
</script>
Vue 设计模式
Vue 使用的 MVVM 设计模式:
- M:表示 model,数据
- V:表示 view,视图
- VM:表示 viewModel 视图数据连接层,在代码层面,表示的是当前根组件对象,可以通过根组件对象来对其中的数据进行修改
// 创建vue应用
const app = Vue.createApp({
data() {
return {
message: "hello",
};
},
template: `<div>{{message}}</div>`,
});
// 这里的vm就表示id为root应用的根组件
const vm = app.mount("#root");
// 可以通过下面的表达式修改data里面的message值
// 注意这里的data前面需要加上$符号
vm.$data.message = "world";
常用指令
- v-model:双向绑定指令
可用修饰符:
- lazy:表示懒加载,当输入框输入值后,并且失去焦点的时候触发
- number:限定输入框是数字类型,非数值字符会被自动过滤
- trim:将前后的空格自动去除(中间存在空格会正常保留)
<!--当输入框的值发生变化后,div标签对应的值也会发生变化-->
<input v-model="inputValue" />
<div>{{inputValue}}</div>
- v-bind:属性的绑定操作,简写
:
。
<!--将变量赋值给div的title属性,如果直接写title="userName"是无法实现的,
userName只会被识别为字符串,而不是变量-->
<div v-bind:title="userName"></div>
<!--简写如下-->
<div :title="userName"></div>
当在标签上将标签属性绑定动态的值时,需要使用v-bind
,否则不会生效,也不能使用差值表达式。
上述代码绑定的方式,会固定属性的名称为 title,如果想属性名也是动态的,可以定义一个变量来指定属性名,如下代码:
<div id="root"></div>
<script>
const app = Vue.createApp({
data() {
return {
attrName: "title", // 指定属性名
message: "title info", // 指定属性值
};
},
template: `<div :[attrName]="message">hello world</div>`,
});
app.mount("#root");
</script>
注意:这种书写方式不止适用与 v-bind,同时也适用于 v-on,书写方式是一样的,但是这种方式不常用,作为冷知识了解一下。
- v-for:循环遍历列表数据(数组数据或者对象数据)
遍历数组时对应的item
是数组内的每个元素,遍历对象时,value
对应的是对象内属性的值,key
对应的是对象内属性名。
注意:这里涉及到在标签中添加一个:key
,通过这个方式优化循环,当出现 key 相同的时候,之前遍历的标签可以复用,提高性能,key 值要保证唯一性,否则会出现覆盖的问题
const list = ["张三","李四","王五"]
const listObject = {
"id":1,
"name":"张三",
"age":18
}
/*循环数组*/
<ul>
<li v-for="(item,index) of list" :key="item">{{item}}</li>
</ul>
/*循环对象value对应字段值,key对应字段名*/
<ul>
<li v-for="(value,key,index) of listObject" :key="value">{{value}}</li>
</ul>
- v-show:用于判断当前标签是否显示,通过样式的 display 属性来实现,标签并没有销毁,也不会触发
beforeUnmount
和unmounted
生命周期函数。
data(){
return{
show: true
}
}
/*当show为true的时候显示,false的时候不显示*/
<div v-show="show"></div>
- v-if:用于判断当前标签是否显示,通过删除和添加当前标签实现(支持
v-else-if
,v-else
),会触发beforeUnmount
和Unmounted
生命周期函数。
data(){
return{
show: true,
condition: false
}
}
/*当show为true的时候显示,false的时候不显示*/
<div v-if="show"></div>
<div v-else-if="condition"></div>
<div v-else></div>
注意:这里需要v-if
、v-else-if
、else
三种标签放在一起,如果中间插入其他标签会出现报错的问题。
- v-html:将含有
html
标签的变量渲染到指定的元素内
const html = "<strong>hell world</strong>"
/*此方式会直接将html作为文本内容显示在div标签内,不会渲染strong标签的效果*/
<div>{{html}}</div>
/*可以正常解析strong标签,并将hello world加粗*/
<div v-html="html"></div>
- v-on:用来定义事件,简写
@
。
<div v-on:click="clickBtn"></div>
<!--简写-->
<div @click="clickBtn"></div>
- v-once:表示当前标签变量只渲染一次,即使此变量在不断的变化,也不会再重新渲染。
<input v-model="inputValue" />
<!--此时input标签内的值变化并不会引起div内的数据变化-->
<div v-once>{{inputValue}}</div>
- v-for和v-if混合使用细节
/*这种写法在Vue里面是不正确的,v-if不会生效,因为v-for优先级要高于v-if*/
template: `
<div v-for="item in 10" :key="item" v-if="key !== 5">
{{item}}
</div>
`;
/*改进第一步:这种方法可以显示,但是会出现外面多包了一层div*/
template: `
<div v-for="item in 10" :key="item">
<div v-if="key !== 5">
{{item}}
</div>
</div>
`;
/*终版:使用占位符标签:template*/
template: `
<template v-for="item in 10" :key="item">
<div v-if="key !== 5">
{{item}}
</div>
</template>
`;
差值表达式
差值表达式中的内容只能是表达式,不能是语句。
- 运算
{{ count * price }}
- 调用方法
/*count是变量*/ {{ doSomething(count); }}
methods 方法
methods 里面定义方法的时候,经常会使用到 data 里面的数据,直接使用this.
的方式就可以获取到 data 内数据,因为这个 this
代表的是当前的 vue 应用。但是当方法使用箭头函数的时候,this
就是指向上层结构的对象,非 vue 应用,此时使用this.
就无法获取到 data 内的数据啦。
const app = Vue.createApp({
data() {
return {
message: "hello world",
};
},
methods: {
// 推荐使用!!!
handleClick() {
// this表示当前vue应用
console.log(this.message);
},
handleClick2: () => {
// 此时这个this就是undefined
console.log(this.message);
//表示当前的window(窗口应用)
console.log(this);
},
},
});
在 Vue3 中,不需要将方法都包裹在 methods 方法,而是直接写在 setup 中函数中。示例代码如下:
<script setup>
// 此方法可以直接用方法名调用
const handleClickMethod = () => {
// 逻辑代码
};
</script>
计算属性 computed
在变量发生变化的时候,computed
内的计算属性会自动进行重新计算并渲染到页面。(不仅限于数值的运算,可以是对象、字符串、数组等)
data(){
return {
count: 2,
price: 10,
message: '123'
}
},
computed: {
// 当计算属性依赖的内容发生变更时,才会重新计算
total(){
return this.price * this.count;
}
},
methods:{
// 当页面重新渲染,才会重新计算(注意:任意标签出现重新渲染,都会重新计算)
getTotal(){
return this.price * this.count;
}
}
/*当动态改变message的时候,这个total值是不会变化的 */
<div>{{total}}</div>
/* 当动态改变message的时候,此div会重新渲染,重新调用getTotal方法*/
<div>{{message}} {{getTotal()}}</div>
在 Vue3 中计算属性的使用,首先需要导入 computed
才可以使用。示例如下:
<script setup>
import { computed } from "vue";
computed(() => {
// 业务逻辑代码
});
</script>
侦听器 watcher
监听属性的变化,在侦听器的函数上有两个参数,分别是修改后和修改前的值。在实际应用中,可以通过侦听属性的变化,做异步的操作。
data(){
return {
count:2,
price:5,
total:10
}
},
watch: {
// current表示当前修改后的值(新值),prev表示历史值
price(current,prev) {
return this.count * current;
}
}
<div>{{total}}</div>
注意:
- 在
computed
和method
都可以实现的时候,优先使用computed
,因为computed
具有缓存 - 在
computed
和watcher
都可以实现的时候,优先使用computed
,因为computed
更加的简洁
样式绑定
样式绑定的三种方式:
- 字符串方式:适用于只绑定一个 class;
- 数组方式:适用于绑定多个 class;
- 对象方式:适用于绑定多个 class。
<style>
.classOne {
color :red;
}
.classTwo {
font-size:100px;
}
</style>
data(){
return {
classString:'classOne',
// 对象方式
classObject:{ classOne:true,classTwo:false },
// 数组方式(不支持使用布尔类型动态展示样式,写定离手)
classArray:['classOne','classTwo'],
// 对象和数组混合模式
classMix:['classOne',{classTwo:true}]
}
}
template:`<div :class="classArray">hello world!</div>`
这里如果使用的是 style 属性,不是使用 class,style 对应的属性值,定义在变量里面,应该怎么去做。如下代码:
data() {
return{
// 字符串形式书写
styleString:"color:red;font-size:100px;",
// 对象形式书写(推荐写法)
styleObject:{
color :'red',
font-size:100px
}
}
}
template:`<div :style="styleString">hello world!</div>`
template:`<div :style="styleObject">hello world!</div>`
函数的方法
数组操作函数
- push:从尾部新增元素,可以新增多个,返回值是添加后数组元素个数,修改原数组;
var arr = [1, 2, 3];
arr.push(4, 5, 6); //结果arr为:[1,2,3,4,5,6]
- pop:从尾部删除元素,返回值是对应删除的元素,修改原数组;
- shift:从头部删除元素,返回值是对应删除的元素,修改原数组;
- unshift:从头部添加元素,可以添加多个,返回值是添加后数组元素个数,修改原数组;
- reverse:取反,返回的是取反后的数组,修改原数组;
var arr = [1, 2, 3];
arr.reverse(); // arr将变为:[3,2,1]
- sort:排序,通过函数的方式规定排序方式,修改的是原数组;(默认是升序排列)
- splice:对应参数有 index、howmany 和 item1,item2……itemX,此操作是对原数据进行修改,具体含义如下:
参数名 | 含义 | 是否必须 |
---|---|---|
index | 整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置 | 是 |
howmany | 要删除的项目数量,如果设置为 0,则不会删除项目 | 是 |
item1,……,itemX | 向数组添加的新项目 | 否 |
var arr = [1, 2, 3, 4, 5];
// 结果arr:[1,3,4,5],arr1:[2]
var arr1 = arr.splice(1, 1);
// 结果arr:[1,4,5],arr2:[2,3]
var arr2 = arr.splice(1, 2);
// 此时会将index为1的元素删除,从1开始新增两个元素6,7结果arr是:[1,6,7,3,4,5]
// 当howmany和item参数都有的时候,可以理解为替换(并非一换一,可能是一换多或者多换一)
var arr3 = arr.splice(1, 1, 6, 7);
//表示不删除元素,而是新增,最后arr为:[1,6,7,2,3,4,5]
arr.splice(1, 0, 6, 7);
- slice:从某个已有的数组返回选定的元素,请注意,该方法并不会修改数组,而是返回一个子数组,可以通过使用负值从数组的尾部选取元素。
var arr = [1, 2, 3, 4, 5, 6];
var arr2 = arr.slice(-4, -2); //返回的arr2结果是:[3,4]
// 说明:负值从-1开始计算,slice的结果是含头不含尾
- join:把数组的所有元素放入一个字符串中,元素通过指定的分隔符进行分割,此方法有一个参数,即指定分隔符,形成新的字符串,不修改原数组;
var arr = ["a", "b", "c"];
var arrStr = arr.join("-"); // 结果是a-b-c
var arrStr = arr.join(""); // 结果是abc
- concat:连接两个或多个数组,并返回连接后的新数组,原数组不会发生变化,此方法有一个参数,即指定需要连接的数组或数据;
var arr1 = [1, 2, 3];
var arr2 = ["a", "b", "c"];
// 对应的结果是 [1,2,3,'a','b','c']
var arr3 = arr1.concat(arr2);
在 Vue 中,实际两个数据的连接,可以简写成如下方式:
var arr1 = [1, 2, 3];
var arr2 = ["a", "b", "c"];
// 对应的结果是 [1,2,3,'a','b','c']
var arr3 = [...arr1, ...arr2];
- filter:按照指定条件,从数组中筛选出需要的元素,筛选出后,作为一个新的数组返回。
var arr1 = ["abc", "bdc", "erd"];
//arr2的结果是:["abc"]
var arr2 = arr1.filter((item) => item === "abc");
- find:从数组中查找指定的元素,以新的数组返回。和
filter
函数是类似的。
const arr = ["abc", "bcd", "erd"];
const result = arr.find((item) => item === "abc");
if (result) {
console.log("abc");
} else {
console.log("error");
}
- includes:查看数组中是否包含指定的元素,包含的返回
true
,不包含则返回false
。
const whiteList = ["/login", "/home", "/404"];
const currentPath = "/home";
console.log(whiteList.includes(currentPath));
对象操作
在循环遍历对象的时候,需要向对象中添加内容,此时就要用到如下的语法:
const app = Vue.createApp({
data() {
return {
user: {
name: "张三",
age: "23",
weight: "120kg",
},
addAttr: {
height: "177cm",
addr: "合肥",
},
attrName: "height",
};
},
methods: {
changeUser() {
// 方式一:
this.user.height = "174cm";
// 方式二:
this.user[this.attrName] = "173cm";
},
},
template: `<div>
<div v-for="(value,key,index) in user" :key="key">
{{index}}-{{key}}-{{value}}
</div>
</div>`,
});
const vm = app.mount("#root");
如果遇到动态添加,怎么办,就是想一个对象的所有属性添加到另一个对象中,如上代码,将 addAttr 内的属性全部添加到 user 里面,代码如下:
methods:{
changeUser(){
for(attr in this.addAttr){
this.user[attr] = this.addAttr[attr];
}
}
}
事件绑定及修饰符
- 在调用事件的时候需要传入参数
method:{
clickHandle(event){
console.log(event.target)
}
},
// 在没有传入任何参数的时候,存在一个默认参数event,表示当前标签对象
template:`<div @click="clickHandle">hello world!</div>`
//-----------------------------
method:{
clickHandle(num,event){
console.log(num)
console.log(event.target)
}
},
// 当传入参数的时候,如果不显示写出event,则方法中无法接收,可以使用$event方式获取并作为参数传入
template:`<div @click="clickHandle(num,$event)">hello world!</div>`
- 绑定多个事件
method:{
clickHandle(){
console.log(2)
},
clickHandle2(){
console.log(1)
},
},
// 错误写法
template:`<div @click="clickHandle,clickHandle2">hello world!</div>`
// 正确写法
template:`<div @click="clickHandle(),clickHandle2()">hello world!</div>`
- 事件冒泡规则(默认)
method:{
clickHandle(event){
console.log(2)
},
divClickHandle(event){
console.log(1)
}
},
/*此时点击button,同时会触发clickHandle、divClickHandle两个事件,从内到外依次触发*/
template:`
<div @click="divClickHandle">
<button @click="clickHandle">点击这里</button>
</div>
`
事件修饰符
- prevent:阻止默认事件冒泡行为,如在
form
表单在action
中的地址,默认会发生跳转,为了阻止跳转,可以在点击事件上增加prevent
修饰符。
method:{
clickHandle(event){
console.log(2)
}
},
template:`
<div>
<button @click.prevent="clickHandle">点击这里</button>
</div>
`
- stop:停止事件冒泡,当父标签和子标签都定义了事件,在点击子标签的时候,父标签的点击事件也会被触发,此时为了让父标签事件不触发,可用
stop
修饰符。
method:{
clickHandle(event){
console.log(2)
},
divClickHandle(event){
console.log(1)
}
},
template:`
<div @click="divClickHandle">
<button @click.stop="clickHandle">点击这里</button>
</div>
`
- self:父子标签都有事件,点击子标签触发子标签点击事件,点击父标签触发父标签点击事件,可以在父标签上增加
self
修饰符。
method:{
clickHandle(event){
console.log(2)
},
divClickHandle(event){
console.log(1)
}
},
template:`
<div @click.self="divClickHandle">
<button @click="clickHandle">点击这里</button>
</div>
`
按键修饰符
- enter:在点击
enter
的时候,触发事件。
template: `
<div>
<button @keydown.enter="clickHandle">点击这里</button>
</div>
`;
- tab:同理 enter,点击 tab 键触发
- delete:同理 enter,点击 delete 键触发
- esc:同理 enter,点击 esc 键触发
- up:同理 enter,点击向上箭头键触发
- down:同理 enter,点击向下箭头键触发
鼠标修饰符
- left:点击左键,触发事件
template: `
<div>
<button @click.left="clickHandle">点击这里</button>
</div>
`;
- right:同理 left,点击右键,触发事件
- middle:同理 left,点击中键,触发事件
精确修饰符
**exact:**比如定义了按住 ctrl,单击鼠标时触发事件,如果不用精确修饰符,就会出现按住 ctrl 及组合键,再去点击鼠标也可以触发,需要精确,就需要使用 exact 修饰符。
// 此时按住ctrl键点击鼠标即可触发,但是安装ctrl加其他组合键,在点击鼠标,也会触发
template: `<div @click.ctrl="counter+=1"></div>`;
// 只按住ctrl一个键,在点击鼠标触发,其他情况无法触发
template: `<div @click.ctrl.exact="counter+=1"></div>`;
双向绑定(表单)
- input(text):文本输入框绑定
const app = Vue.createApp({
data() {
return {
message: "",
};
},
template: `<input v-model="message" />`,
});
- textarea:文本域绑定
const app = Vue.createApp({
data() {
return {
message: "",
};
},
template: `<textarea v-model="message" />`,
});
- input(checkbox):checkbox 多选框绑定
// 在多选框被选中后,value值会自动赋值给message数组,取消选中后,也会从message中去除,message是个数组
<input type="checkbox" value="id1" v-model="message"/>
<input type="checkbox" value="id2" v-model="message"/>
<input type="checkbox" value="id3" v-model="message"/>
<input type="checkbox" value="id4" v-model="message"/>
data(){
return{
message:[]
}
}
checkbox 如果没有指定 value 值,则返回的值是 true 和 false,如果指定 value 值,如果选中,返回对应的 value 值,如果未选中,返回的是空值。但是有时候会遇到在 true、false 不同情况下,返回不同的值,这个时候就可以使用下面的编写方式:
<input
type="checkbox"
true-value="this is true value"
false-value="this is false value"
v-model="message"
/>
这种方式存在一个小 bug,如果在默认情况下,指定true-value
值,会出现这个选择框没有自动选择。
input(radio):radio 单选框绑定,和 checkbox 相同,不同在于 radio 是单选,message 使用字符串即可,无需数组
select-option:选择框绑定
<select v-model="message">
<option style="color:aliceblue;" disabled value="">请选择内容</option>
<!-- 这里的value需要用v-bind修饰,另外这个value不仅限于字符串,可以是对象等 -->
<option v-for="item of options" :value="item.value">{{item.text}}</option>
</select>
data(){ return{ message:"", options:[{ value:"A", text:"A" },{ value:"B",
text:"B" },{ value:"C", text:"C" }] } }
☆☆☆ 组件
将一个页面拆分成多个组件,在渲染的时候,将组件拼装成一个页面,对于公用的组件,可以被多个页面使用。
- 定义公用组件,一次定义多处使用
- 组件内的数据是独有的,非共享的
组件可以分为局部组件和全局组件,定义方式如下:
// 局部组件定义方式,需要根组件上通过声明components来引用conter局部组件
const Conter = {};
const app = Vue.createApp({
//通过声明components来引用conter局部组件
components: { conter: Conter },
});
// 全局组件定义方式,无论在根组件中是否使用,都会被挂载在根组件上
// 全局组件有使用简单,性能不高的特点
app.component("conter", {});
组件命名规则:
全局组件在定义组件名称的时候,采用小写字母,如果涉及多个单词,使用-
来连接;
局部组件在定义名称的时候,采用首字母大写的方式,如果涉及多个单词,采用驼峰式命名。
在引用局部组件的时候,可以直接使用原名称,也可直接指定名称,如下:
components: {
Counter, HelloWorld;
}
// 在使用的时候Vue会自动检测名称,并将Counter首字母改为小写,最终为counter
// 当时驼峰命名的时候,Vue也会自动将字母转为小写,多字母之间用中划线连接
template: `<div><conter/><hello-world/></div>`;
在 Vue3 中,组件都是以一个单独的 vue 文件存在,通过 import 方式导入即可直接使用,无需通过 components 来指定组件的名称。示例代码如下:
<template>
<!-- 使用组件 -->
<Counter />
</template>
<script setup>
// 导入组件
import Counter from "./components/Counter.vue";
</script>
父组件传值到子组件
- 静态传参:通过非动态绑定的属性进行传值,传入的值提前写入,传到子组件统一转为 String 类型
const app = Vue.createApp({
template: `<counter content="contentvalue"></counter>`,
});
- 动态传参:通过
v-bind
来动态绑定属性传值,传入的值可动态改变,子组件接收的数据类型与传入的数据类型相同
const app = Vue.createApp({
data(){
return {
content: "contentValue123"
}
}
template:`<counter :content="content"></counter>`
});
子组件在接收值的时候可以做数据类型判断、限定等操作。如下:
// 直接接收数据
app.component("counter", {
props: ["content"],
});
// 加入类型判断
app.component("counter", {
props: {
content: {
type: Number, // 指定类型是数值类型
required: true, // 是否必须传入
default: 123, // 当不满足需求的时候,采用此默认值
validator: function (value) {
// value表示传入的数据
// 参数校验逻辑,最终返回一个boolean类型的结果
},
},
},
});
type
可取的类型有:String
、Boolean
、Array
、Object
、Function
、Symbol
。
传入 Function 的方式:
const app = Vue.createApp({
data() {
return {
content: () => {
alert(123);
},
};
},
template: `<counter :content="content"></counter>`,
});
app.component("counter", {
props: {
content: Function,
},
methods: {
funClick() {
this.content(); // 直接调用传过来的方法
},
},
template: `{{typeof content}}<button @click="funClick()">点击操作内容</button>`,
});
在传多个数据的时候,可以使用以下两种方式:
- 方式一:
data(){
return {
params:{
a:"1",
b:"2",
c:"3",
}
}
},
template:`<div><counter v-bind="params"/></div>`
// 此时,Vue会自动将params内a、b、c赋值到props中对应的属性
app.component('counter',{
props:['a','b','c'],
template:`<div>{{a}}-{{b}}-{{c}}</div>`
})
- 方式二:
data(){
return {
params:{
a:"1",
b:"2",
c:"3"
}
}
},
template:`<div ><counter :params="params"/></div>`
app.component('counter',{
props:['params'],
template:`<div>{{params.a}}-{{params.b}}-{{params.c}}</div>`
})
由于 html 不支持驼峰规则,所有在定义标签属性的时候,涉及多个单词,需要用-
连接,此时传值就存在一个问题,需要规避,如下:
data(){
return {
params:1234
}
},
template:`<div ><counter :counter-params="params"/></div>` //定义采用短横线连接的方式,不支持驼峰规则
app.component('counter',{
props:['counter-params'],
template:`<div>{{counter-params}}</div>`
})
// 如果采用上面的方式来接收传入的值就会出现counter-params是NaN
// 规避方法:将接收值改成驼峰规则。如下:
app.component('counter',{
props:['counterParams'],
template:`<div>{{counterParams}}</div>`
})
在 Vue3 中,如果需要父组件传给子组件值,传入方式基本一样,使用v-bind
来实现,但是在接受数据的时候存在一定的差异。示例代码如下:
<script setup>
// 导入接受参数参数的defineProps函数
import { defineProps } from "vue";
// 通过使用defineProps函数,获取所有数据,并赋值给props
const props = defineProps({
user: {
type: Object,
required: true,
},
count: {
type: Number,
required: true,
},
});
// 使用props
const countValue = props.count;
</script>
单向数据流
概念:子组件可以使用父组件传递的数据,但是不能修改传递过来的数据。
如果子组件的确需要修改传过来的数据,这个时候可以将传过来的数据复制到新的变量内,通过修改新的变量来实现。如下:
data(){
return {
param:1234
}
},
template:`<div ><counter :param="param"/></div>`
app.component('counter',{
props:['param'],
data(){
return{
myCount:this.param // 复制数据
}
}
template:`<div @click="myCount += 1">{{myCount}}</div>`
})
Non-props 特性
- 在父组件给子组件传值的时候,如果子组件没有通过
props
接收时,Vue 会自动将传入的属性和值添加到子组件template
的顶层 dom 对象中。如下:
data(){
return {
param:1234
}
},
template:`<div ><counter :param="param"/></div>`
app.component('counter',{
template:`<div>Counter</div>`
})
// 浏览器渲染后的效果如下:
<div param="1234">Counter</div>
- 如果此时不想 Vue 自动给顶层 dom 添加属性,可以用
inheritAttrs:false
来关闭此特性。如下:
data(){
return {
param:1234
}
},
template:`<div ><counter :param="param"/></div>`
app.component('counter',{
inheritAttrs:false,
template:`<div>Counter</div>`
})
- 如果子组件存在多个并列的顶层 dom 对象,Vue 是无法自动实现添加属性的机制,可以手动的添加。如下:
const app = Vue.createApp({
data() {
return {
param: 1234,
};
},
template: `<div ><counter :param="param" message="111"/></div>`,
});
app.component("counter", {
template: `
<div :param="$attrs.param">Counter1</div> //param属性
<div :message="$attrs.message">Counter2</div> //message属性
<div v-bind="$attrs">Counter2</div>`, // 所有属性
});
- 不仅如此,还可以通过生命周期函数来获取属性和属性值,如下:
app.component("counter", {
mounted() {
console.log(this.$attrs.param);
},
template: `
<div :param="$attrs.param">Counter1</div> //param属性
<div :message="$attrs.message">Counter2</div> //message属性
<div v-bind="$attrs">Counter2</div>`, // 所有属性
});
补充 Vue3 中关于 props 和 attrs 接收父组件传值的那些事
在父组件传值给子组件的时候,一般都会用 props 进行接收,但是当传入的这些值只做静态数据展示,不做任何数据相关的操作,可以不用 props 接收,直接用 attrs 在 template 中使用即可。使用的方式是$attrs.name
,这种更为的简洁。但是往往应用场景较为复杂,有可能存在部分属性需要进行 js 操作,部分属性可以直接在 template 中显示。那就可以采用 props 和 attrs 混合使用的方式。示例代码如下:
<!-- 父组件 -->
<template>
<!--父组件给子组件传值-->
<Child :user="user" :count="count" :desc="desc" />
</template>
<script setup>
import Child from "./components/Child.vue";
const user = {
name: "张三",
age: 18,
};
const count = 1;
const desc = "描述信息";
</script>
<!-- 子组件 -->
<template>
<!-- 子组件 -->
<div>{{ $attrs.desc }}</div>
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
user: {
type: Object,
},
count: {
type: Number,
},
});
</script>
父子组件通过事件传递数据
正常的数据流是父组件将数据传给子组件,而子组件是没有修改父组件属性值的权限,只能将属性值复制到子组件内进行操作,但是可以通过事件的方式将子组件改变的数据传给父组件,由父组件进行修改操作。也就是在父组件中定义一个回调的函数,然后在子组件中调用此回调函数,最终由父组件完成修改动作。如下:
const app = Vue.createApp({
data(){
return {
count:123
}
},
methods:{
addOneHandle(num){ // num是接收传递来的参数,可多个,与传入对应即可
this.count = this.count + num;
}
}
// 定义回调函数:addOneHandle
template:`<counter :count="count" @add-one="addOneHandle"/>`
});
app.component("counter",{
props:['count'],
methods:{
addOneHandle(){
// 调用父节点的add-one事件,传入一个参数2
this.$emit('addOne',2);
}
},
template:`<div @click="addOneHandle">{{count}}</div>`
})
注意点:
- 在使用
this.$emit('addOne',2)
的时候,调用事件的名称采用驼峰命名规则,@add-one
父标签中命名使用中划线连接,因为 html 不支持驼峰规则规则命名 - 在调用父组件的事件时,传入的参数个数无限制,多个参数依次向后添加即可
- 可以通过
emits
属性来统一管理记录调用父组件的事件集合,如下:
app.component("counter",{
props:['count'],
// 这里既可以使用数组的方式,也可使用对象的方式,对象的方式可以引入函数做逻辑判断
emits:['addOne']
methods:{
addOneHandle(){
// 调用父节点的add-one事件,传入一个参数2
this.$emit('addOne',2);
}
},
template:`<div @click="addOneHandle">{{count}}</div>`
})
父子组件的双向绑定
在同一个组件里面,数据可以通过v-model
进行双向绑定,在父子组件之间也能通过这种方式实现双向绑定。如下:
const app = Vue.createApp({
data() {
return {
count: 123,
other: 234,
};
},
template: `<counter v-model:count="count" v-model:other="other"/>`,
});
app.component("counter", {
props: ["count", "other"],
methods: {
addOneHandle() {
this.$emit("update:count", this.count + 1);
this.$emit("update:other", this.other + 1);
},
},
template: `<div @click="addOneHandle">{{count}}-{{other}}</div>`,
});
上面这种方式可以适用于多个参数,即多个v-model:xxx="xxx"
,如果是单个参数,可采用下面的方式简化写成v-model="xxx"
,但是这种写法会影响代码的可读性,在实际开发中不建议这么写。
const app = Vue.createApp({
data() {
return {
count: 123,
};
},
template: `<counter v-model="count" />`,
});
app.component("counter", {
props: ["modelValue"], // 固定的名字,必须使用modelValue
methods: {
addOneHandle() {
this.$emit("update:modelValue", this.modelValue + 1); // update:modelValue也是固定格式
},
},
template: `<div @click="addOneHandle">{{modelValue}}</div>`,
});
在 Vue3 中使用的是 Compostion API,在 setup 中是不能使用this
这个关键字的,那带来的问题就不能使用this.$emits
方式回调父组件的方法。给出的解决方式如下示例代码:
<script setup>
// 导入defineEmits函数
import { defineEmits } from "vue";
// 获取回调方法
const emits = defineEmits(["update:modelValue"]);
// 使用emits回调
const handleClick = () => {
emits("update:modelValue", "this is update value");
};
</script>
双向绑定自定义修饰符
在双向绑定的时候,如果需要子组件做一些固定的操作,就可以使用此方式,比如大小写转换。
const app = Vue.createApp({
data() {
return {
content: "abc",
};
},
template: `<counter v-model.uppercase="content"/>`,
});
app.component("counter", {
props: {
modelValue: String,
modelModifiers: {
// 固定写法,名称必须是modelModifiers
default: () => ({}),
},
},
methods: {
modifier() {
let newValue = this.modelValue + "def";
if (this.modelModifiers.uppercase) {
// 检查修饰符是否存在,做相应的操作
newValue = newValue.toUpperCase();
}
this.$emit("update:modelValue", newValue);
},
},
template: `<div>{{modelValue}}<div>
<button @click="modifier">change</button>`,
});
app.mount("#root");
插槽分类讲解
- 基本插槽:通过在引用子组件的时候,传递 dom 对象给子组件的一种方式,如下:
const app = Vue.createApp({
template: `<counter>
<div>dom</div>
</counter>`,
});
app.component("counter", {
template: `<div>
<slot></slot> // 通过slot标签,将父组件counter内的所有dom引入到此位置
</div>`,
});
- 具名插槽:如果父组件传递多个 dom,这些 dom 需要放在子组件中的不同位置,直接采用基本插槽的全部引用,就会存在问题,这里就引入了具名组件。
const app = Vue.createApp({
template: `<counter>
<template v-slot:header> // 注意之而立使用冒号连接,名称无需引号包裹
<div>header</div>
</template>
<template v-slot:footer> // 使用template标签作为占位符,这里的v-slot:footer可简化为#footer
<div>footer</div>
</template>
</counter>`,
});
app.component("counter", {
template: `<div>
<slot name="header"></slot> //将header的标签放到body前
<div>
body content
</div>
<slot name="footer"></slot> //将footer的标签放到body后
</div>`,
});
- 作用域插槽:直接上代码如下:
const app = Vue.createApp({
// 第一种写法,使用slotProps接收子组件所有的属性数据,并封装成对象赋值给slotProps
template:`<counter v-slot="slotProps"> // 接收子组件绑定的所有属性数据
<div>{{slotProps.index}}-{{slotProps.item}}</div>// 渲染接收到的数据
</counter>`
// 第二种写法,使用ES6的解构,对第一种方法的slotProps进行解构,书写格式如下:
template:`<counter v-slot="{index,item}">
<div>{{index}}-{{item}}</div>
</counter>`
});
app.component("counter",{
data(){
return {
list:[1,2,3],
}
},
template:`<div>
<slot v-for="(item,index) in list" :item="item" :index="index"/> //将遍历出来的数据绑定到slot上
</div>`
})
注意:
- 不管是何种类型的插槽,都没办法在
slot
标签上绑定事件,此时可以考虑在slot
标签外层添加一个span
标签; - 插槽内可以是 dom 对象,也可以是字符串或者其他组件;
- 插槽中取数的作用域,父模板调用的数据属性,使用的都是父模板里的数据,子模板调用的数据属性,使用的都是子模板里的数据;
- 在定义查找的时候,可以指定默认值,在父模板没有传入内容时默认显示,格式:
<slot>default value</slot>
。
动态组件
通component
标签及其固定属性is
来控制子组件的加载情况,实现动态组件的效果,代码如下:
const app = Vue.createApp({
data() {
return {
showItem: "one-item",
};
},
methods: {
changeItem() {
// 通过点击事件,修改需要展示的组件
this.showItem = this.showItem === "one-item" ? "two-item" : "one-item";
},
},
template: `
<component :is="showItem"/> //通过is属性,切换显示one或者two组件
<button @click="changeItem">切换</button>
`,
});
app.component("one-item", {
template: `<div>this is one-item</div>`,
});
app.component("two-item", {
template: `<div>this is two-item</div>`,
});
app.mount("#root");
注意:如果动态组件内有类似数据框的标签,当切换的时候,原本输入框内的数据会丢失,这个时候就需要通过keep-alive
标签对动态组件进行包裹。如下:(动态组件和keep-alive
标签经常混合使用,以达到最好的效果)
<keep-alive>
<component :is="showItem" />
</keep-alive>
异步组件
组件异步显示,详细见示例代码:
const app = Vue.createApp({
template: `
<div>this is static div</div>
<async-item /> // 加载子组件
`,
});
app.component(
"async-item",
Vue.defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
template: `<div>this is async template</div>`,
});
}, 4000); // 4秒后加载
});
})
);
app.mount("#root");
ref 引用和跨子组件传值
ref 引用
- 在 dom 对象上定义
ref
属性,通过this.$refs
来获取 dom 对象,如下:
const app = Vue.createApp({
mounted() {
console.log(this.$refs.count.innerHTML()); // this.$refs.count获取count对应的dom对象
},
template: `
<div ref="count">
<div>this is count value<div>
</div>
`,
});
- 在子组件引用时使用,可以得到子组件对象,操作子组件内的方法
const app = Vue.createApp({
mounted() {
this.$refs.child.sayHello(); // this.$refs.child获取子组件child对象,通过child对象调用sayHello方法
},
template: `
<child ref="child" />
`,
});
app.component("child", {
methods: {
sayHello() {
alert("你好");
},
},
});
在 Vue3 中,通过 ref 获取 dom 对象和 Vue2 存在区别,主要还是体现在 Vue3 中不能使用this
关键字导致的,但是在 Vue3 中获取 ref 对象更为的简洁。示例代码如下:
<template>
<div ref="divNodeRef"></div>
</template>
<script setup>
import { ref } from "vue";
// 直接采用这种方式即可,名称和ref指定的名称保持一致
// Vue3会自动将div对应的dom对象赋值给divNodeRef
const divNodeRef = ref(null);
</script>
provide 和 inject
在组件之间值传递的时候,可能会遇到顶级组件传递数据给孙子组件,这个时候需要先传给儿子组件,再传给孙子组件,就会很冗余很麻烦,可以采用provide
和inject
来实现,代码如下:
const app = Vue.createApp({
data(){
return {
count:123
}
},
provide:{
count:123
},
template:`<child />`
})
app.component('child',{
template:`<child-child />`
})
app.component('child-child',{
inject:['count']
template:`<div>{{count}}</div>`
})
/*
这里可以看出来一个问题,在provide里面也定义了一个count属性,
如果直接通过count=this.count能不能引用到data内的count值呢,
实际是不可以的,需要对provide进行改写,改写如下:*/
provide(){
count:this.count
}
注意点:
- 通过这样的传递,可以不用经过层层组件进行传递
- 在
provide
和inject
配合的时候,值只会传入一次,即使后续顶层组件中count
值发生变化,也不会再做同步(后续可以用 Vue3 一些新的特性实现,待学习后更新)
过渡和动画效果
在 Vue 中,使用<transition>
标签,能够实现过渡和动画效果。
定义过渡效果
<style>
.v-enter-from {
/*定义进入前的透明度 */
opacity: 0;
}
.v-enter-active {
/*定义进入过程的过渡效果 */
transition: 2s opacity ease-out;
}
.v-enter-to {
/*定义进入后的透明度 */
opacity: 1;
}
.v-leave-from {
/*定义离开前的透明度 */
opacity: 1;
}
.v-leave-active {
/*定义离开过程的过渡效果 */
transition: 2s opacity ease-in;
}
.v-leave-to {
/*定义离开后的透明度 */
opacity: 0;
}
</style>
<script>
const app = Vue.createApp({
data() {
return {
show: false,
};
},
methods: {
change() {
this.show = !this.show;
},
},
template: `<transition>
<div v-show="show">Hello World</div>
</transition>
<button @click="change">点击切换</button>
`,
});
</script>
定义动画效果
<style>
@keyframs shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
,
.v-enter-active {
animation: shake 3s;
}
.v-leave-active {
animation: shake 3s;
}
</style>
总结注意点
- 在 Vue 中,定义的的
v-enter-active
、v-leave-active
等这些 class 样式,会直接应用到transition
标签上,此时必须transition
标签没有指定name
属性; - 如果在页面中多处使用到
transition
标签,且对应的过渡和动画效果都不同,此时就需要给不同的transition
标签定义名称,然后将 class 名称与其对应即可,如下示例:
<style>
.my-enter-active {
/**这里的my对应的是transition标签上的name值 */
animation: shake 3s;
}
</style>
<script>
template: `<transition name="my">……</transition>`;
</script>
- 除第二点的自定名称方式,也可以通过在
transition
标签中指定 class 样式的名称,如下示例:(这种方式很实用,特别是在应用第三方动画效果时使用)
<style>
.hello {
animation: shake 3s;
}
.bye {
animation: shake 3s;
}
</style>
<script>
template: `<transition
enter-active-class="hello"
leave-active-class="bye"
>……</transition>`;
</script>
其中对应的v-enter-from
、v-enter-to
、v-leave-from
、v-leave-to
等都可以通过这种方式指定。
推荐一个第三方动态效果官网https://animate.style/
transition 标签的其他特性
transition
标签可以同时添加过渡和动画效果,但是存在过渡效果和动画效果的时长是不一样的,这个时候如果要以其中一个时间为准,可以使用type
属性来解决,对应的值有transition
和animation
,分别表示以过渡效果时间为准和动画效果时间为准。transition
标签上可以通过duration
直接指定过渡和动画效果的时长,即使对应的效果上的 CSS 已经设置时长,也会失效。示例代码如下:
const app = Vue.createApp({
template: `
<transition :duration="1000"></transition><!-- 表示所有的过渡和动画都是1000ms执行结束 -->
<transition :duration="{enter:1000,leave:2000}"></transition><!-- 表示所有的过渡和动画都是进场为1000ms,出场为2000ms -->
`,
});
- 在
transition
标签里面有多个元素,这多个元素使用v-if
或者v-show
来切换元素显示,但是实际操作中,两个标签消失效果和展示效果是同时执行的,也就是说这个时候,页面上会同时展示两个元素,但是实际需求不需要,而是想隐藏的元素根据指定效果退出后,再按指定效果将展示的元素显示出来。这个时候就可以使用model
属性,指定其值为out-in
,表示先消失隐藏的,再展示要显示的。
<script>
const app = Vue.createApp({
data() {
return { show: false };
},
methods: {
change() {
this.show = !this.show;
},
},
// 对两个div进行切换
template: `
<transition model="out-in" appear>
<div v-if="show">hello world</div>
<div v-else>bye world</div>
</transition>
<button @click="chage">切换</button>
`,
});
</script>
但是这样做了以后,在页面刷新初次加载bye world
的时候,没有进场动画,这个时候可以在transition
标签上添加appear
属性即可。
- 第三点中指定的是多个单元素之间的切换,但是实际使用过程中,也可以实现多个组件之间的切换,只需将单元素换成组件即可。示例代码如下:
<script>
const ComponentA = {
template:`<div>hello world</div>`
};
const ComponentB = {
template:`<div>bye world</div>`
};
const app = Vue.createApp({
data(){
return {show: false}
},
methods: {
change(){
this.show = !this.show;
}
},
components :{
"component-a":ComponentA,
"component-b":ComponentB
}
// 两个组件之间的切换,同时这里也可以用动态组件的方式,将transition内的内容改写成如下
// <component :is="component"/>,然后对change方法改写,并定义一个data参数component即可
template:`
<transition model="out-in" appear>
<component-a v-if="show" />
<component-b v-else/>
</transition>
<button @click="chage">切换</button>
`
});
</script>
transition-group 实现列表动画
在列表渲染的时候,在添加元素和减少元素,都可以有一个过渡和动画效果,具体代码如下:
<style>
.list-item {
display: inline-block;
margin-right: 10px;
}
.v-move {
transition: all 0.5s ease-in;
}
.v-enter-from {
opacity: 0;
transform: translateY(30px);
}
.v-enter-active {
transition: all 0.5s ease-in;
}
.v-enter-to {
opacity: 1;
transform: translateY(0px);
}
.v-leave-to {
opacity: 0;
transform: translateY(-20px);
}
.v-leave-active {
transition: all 0.5s ease-in;
}
</style>
<script>
const app = Vue.createApp({
data() {
return {
list: [3, 2, 1],
};
},
methods: {
addItem() {
this.list.unshift(this.list[0] + 1);
},
removeItem() {
this.list.shift();
},
},
template: `
<div>
<transition-group>
<span class="list-item" v-for="item in list" :key="item">{{item}}</span>
</transition-group>
<button @click="addItem">添加元素</button>
<button @click="removeItem">删除元素</button>
</div>
`,
});
app.mount("#root");
</script>
Mixin 基础语法
Mixin 语法在 Vue3 以后,不再推荐使用,可以考虑使用 Composition API 进行替代。
Mixin 分类
- 局部 Mixin:只在引入的组件里面生效,子组件中是无法使用的,写法如下:
const mymixin = {
data() {
return {
message: "mixin message",
};
},
};
const app = Vue.createApp({
data() {
return {
message: "app message",
};
},
mixins: [mymixin],
template: `<div>{{message}}</div>`,
});
- 全局 Mixin:对当前 app 下的所有组件都生效,写法如下:
const app = Vue.createApp({
template: `
<div>{{message}}</div>
<child></child>
`,
});
app.mixin({
// 全局Mixin
data() {
return {
message: "mixin message",
};
},
});
app.component("child", {
template: `<div>{{message}}</div>`,
});
优先级关系
- 当在 mixin 和组件中都定义了相同名称的 data 数据,组件中的 data 比 mixin 中的 data 优先级高
- 当在 mixin 和组件中都定义了相同名称的 methods 数据,组件中的 methods 比 mixin 中的 methods 优先级高
- 当在 mixin 和组件中都定义了生命周期函数,两者都会生效,且mixin 中的生命周期函数优先执行
- 自定义属性,组件中的属性优先级高于 Mixin 属性,使用
this.属性名
的方式调用
const mymixin = {
data(){
return {
message: "mixin message"
}
}
}
const app = Vue.createApp({
data(){
return {
message: "app message"
}
},
mixins:[mymixin],
template:`<div>{{message}}</div>`
})
// 最终渲染效果
<div>app message</div>
const mymixin = {
message: "mixin message",
};
const app = Vue.createApp({
message: "app message",
mixins: [mymixin],
template: `<div>{{this.$options.message}}</div>`,
});
注意点:这里引入一个$options
指令,在 Vue 里面,组件里面的属性都会挂载到options
上面
优先级策略修改
仅支持自定义属性的优先级修改,写法如下:
const mymixin = {
message: "mixin message",
};
const app = Vue.createApp({
message: "app message",
mixins: [mymixin],
template: `<div>{{this.$options.message}}</div>`,
});
// 修改优先级策略,针对属性number
// 当mixin中有message优先展示,否则就展示组件中的message
app.config.optionMergeStrategies.number = (mixinValue, appValue) => {
return mixinValue || appValue;
};
自定义指令
自定义指令方式
通过directive
自定义指令,分为两种类型,分别是局部指令和全局指令,定义如下:
// 定义自定义局部指令
const directives = {
focus: {
mounted(el) {
// 生命周期函数:表示在el标签渲染完成后执行,el表示使用此指令的标签,这里是input标签对象
el.focus();
},
// 不仅支持mounted周期函数,beforeMounted、afterMounted、beforeUpdate、updated、beforeUnmount、unmounted生命周期函数都可以使用
},
};
const app = Vue.createApp({
directives: directives, // 引入自定义局部指令,如果名称是相同的,可以直接写:directives即可
template: `<input v-focus />`,
});
// 在app上挂载自定义全局指令
app.directive("focus", {
mounted(el) {
// 生命周期函数:表示在el标签渲染完成后执行,el表示使用此指令的标签,这里是input标签对象
el.focus();
},
});
注意点:自定义指令里面可以实现所有的生命周期函数,针对当前使用自定义指令的 dom 对象
自定义指令传参
<style>
.demo {
position: absolute;
}
</style>
<script>
const app = Vue.createApp({
data() {
return {
distance: "100px",
};
},
// 这里在v-pos后增加一个left和赋值为distance,会被封装到binding参数内
// left对应的参数名是arg,distance对应的参数名是value
template: `<input class="demo" v-pos:left="distance" />`,
});
app.directive("pos", {
// 初始加载的时候渲染
mounted(el, binding) {
el.style[binding.arg] = binding.value; // 通过binding获取arg和value值
},
// 如果在修改的时候,也需要重新渲染,则需要使用updated生命周期函数
updated(el, binding) {
el.style[binding.arg] = binding.value;
},
});
const vm = app.mount("#root");
</script>
注意:如果这个时候在 directive 中,只有 mounted 和 updated 生命周期函数,则可以实现一下的简写方式。
app.directive("pos", (el, binding) => {
el.style[binding.arg] = binding.value;
});
传送门 teleport
当在template
内定义一个div
的时候,需要将这个div
放到指定的标签内,这个时候就可以使用teleport
将这个div
传送到指定位置。代码如下:
// 正常如果没有teleport,其内的div会按照正常顺序渲染在root标签下id为first的div标签内
// 但是如果现在需要将这个div放到body标签下,就可以使用teleport标签的to属性,将其传送到body下
//
const app = Vue.createApp({
template: `<div id="first">
<teleport to="body"> // 可以指定标签名,也可指定其他标签的id,如:to="#target"
<div>需要将此div放到body标签下</div>
</teleport>
</div>`,
});
const vm = app.mount("#root");
拓转 CSS 知识点:
- 将一个盒子水平居中放到整个页面的中央位置。
.center {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
background: green;
}
- 制作整个页面背景为透明灰色。
.opacity-back {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #000;
opacity: 0.3;
}
render 函数
render
函数在某种意义上来说就是对应template
,在 Vue 底层编译的时候,是将template
内容编译成render
函数,形成 js 描述的 dom 对象(虚拟 dom),通过一系列的操作对虚拟 dom 设置属性、动态值等,然后转为真实 dom 渲染到页面。
render
函数的应用:
// 需要根据传递参数level来确定使用h1、h2、h3、h4标签
const app = Vue.createApp({
data() {
return {
level: 1,
};
},
template: `<demo :level="level">slot message</demo>`,
});
app.component("demo", {
props: ["level"],
render() {
const { h } = Vue; // 从Vue中获取虚拟dom对象
// 第一个参数是标签的名称
// 第二个参数是标签的属性,如:id="demo" name="demoName"
// 第三个参数对应的是标签内的内容,相当于innerHTML
return h("h" + this.level, {}, this.$slots.default()); // default表示所有的slot插槽内容
},
});
app.mount("#root");
核心点:在上面的代码中,第三个参数可以实现多层嵌套,示例代码如下:
// 仅写第三个参数的嵌套示例
[
this.$slots.default(),
h("div", {}, ["innerdiv", h("span"), {}, "content"]), //这里还可以继续嵌套下去
];
核心点:从这种嵌套关系可以看出来整个页面的布局关系,不是吗?
自定义插件
- 首先自定义插件的规则
- 然后通过自定义插件,使用规则
- 通过
app.use
方法挂载应用插件
const app = Vue.createApp({
data() {
return {
age: 30,
name: "joker",
};
},
rules: {
// 自定义校验规则
age: {
validate: (age) => {
return age < 28;
},
message: "too young too simple",
},
name: {
validate: (name) => {
return name.length < 4;
},
// validate: name => name.length < 4; validate的简写方式
message: "name too short",
},
},
template: `<div>name:{{name}}-age:{{age}}</div>`,
});
const validatorPlugin = {
// 定义插件
install(app, options) {
app.mixin({
created() {
for (let element in this.$options.rules) {
const item = this.$options.rules[element];
this.$watch(element, (value) => {
// 监听器,监听element的变化,这里的element是age和name
const result = item.validate(value);
if (result) console.log(item.message);
});
}
},
});
},
};
// 定义插件的另一种简写方式(推荐写法)
// const validatorPlugin1 = (app, options) => {
// app.mixin({
// created() {
// for (let element in this.$options.rules) {
// const item = this.$options.rules[element];
// this.$watch(element, (value) => { // 监听器
// const result = item.validate(value);
// if (result) console.log(item.message);
// })
// }
// }
// })
// }
app.use(validatorPlugin); //使用插件
const vm = app.mount("#root");
注意点:在使用 app.use 方法的时候,是可以传入参数的,传入方式如下:
app.use(validatorPlugin, { name: "joker", age: 20 });
Composition API 语法
Composition API 是 Vue 的新特性,目前在 Vue2.7 版本及以上是可以使用的,低的版本不兼容。
setup
在 Composition API 语法中,将内容都定义在setup
内,setup
是在 Vue 实例被完全初始化之前调用,因此在 setup 内不能使用this
关键字,因为这个时候this
内容还未形成。setup 结构如下:
const app = Vue.createApp({
// created 实例被完全初始化之前,this还没有生成,因此在setup中不能使用this
setup(props, context) {
return {
name: "joker",
};
},
template: `<div>{{name}}</div>`,
});
响应式引用-ref
在setup
里面创建变量后,在template
内使用,当此变量发生变化,template
内使用的数据并不会发生改变,这是因为在setup
内定义的普通变量不满足响应式引用要求,Composition API 给出了一个解决方案使用ref
,但是ref 只能对基础类型数据做响应引用,如下代码:
const app = Vue.createApp({
setup(props, context) {
let name = "dell";
setTimeout(() => {
name = "joker";
}, 2000);
return { name };
},
template: `<div>{{name}}</div>`,
});
// 上述的代码在2秒后修改name为joker,但是渲染出来div内的内容依然是dell。不会随之变化。要想变化,修改如下:
const app = Vue.createApp({
setup(props, context) {
const { ref } = Vue; // 从Vue中引入ref函数
let name = ref("dell");
setTimeout(() => {
name.value = "joker";
}, 2000);
return { name };
},
template: `<div>{{name}}</div>`,
});
总结点:在使用ref
关键字后,在 Vue 底层就相当于proxy({value='dell'})
,对name
进行了数据封装,此时就可以实现响应式引用,在setTimeout
内修改name
值的时候,使用name.value
也是这层封装的原因,但是在template
中使用的时候不需要用name.value
,直接使用name
即可,因为 Vue 底层会自动将其转为name.value
响应式引用-reactive
ref
只能对基础类型数据做响应式引用,如果需要对对象、数组这些数据做响应式引用,就需要使用reactive
。如下代码:
const app = Vue.createApp({
setup(props,context){
const { reactive } = Vue; // 从Vue中引入reactive函数
let user = reactive({name:"joker",age:20});
setTimeout(()=>{
user.name = 'dell';
},2000);
return { user };
}
template:`<div>{{user.name}}-{{user.age}}</div>`
})
总结点一:这里基本和ref
相同,Vue 底层相当于proxy({name:'joker',age:20})
,实现响应式引用
总结点二:根据官方给出的结论,在实际应用中ref
带来的性能提升是远高于reactive
的,因此在开发过程中,尽量使用ref
但是如果现在需要设置只读权限,此时引入了readonly
关键字。代码如下:
const app = Vue.createApp({
setup(props,context){
const { reactive,readonly } = Vue;
let user = reactive({name:"joker",age:20});
const copyUser = readonly(user);
setTimeout(()=>{
user.name = 'dell';
copyUser.name = 'dell'; // 此时就会报错,因为copyUser设置了只读
},2000);
return { user };
}
template:`<div>{{user.name}}-{{user.age}}</div>`
})
响应式引用-toRefs
如下代码:
const app = Vue.createApp({
setup(props,context){
const { reactive } = Vue;
const user = reactive({name:"joker",age:20});
setTimeout(()=>{
user.name = 'dell';
},2000);
const { name } = user; //如果这里只要将name返回,则可以从user中取出name,解构思维
return { name };
}
template:`<div>{{name}}</div>`
})
问题点:template
可以正常渲染,但是在 name 改动的时候,不会重新渲染,也就是响应式引用失效,为什么呢?因为reactive
代理的响应式引用是针对其内的整个对象,对对象内的属性没有单独做响应式引用。此时可以使用toRefs
来解决。
const app = Vue.createApp({
setup(props,context){
const { reactive,toRefs } = Vue;
const user = reactive({name:"joker",age:20});
setTimeout(()=>{
user.name = 'dell';
},2000);
const { name } = toRefs(user);
return { name };
}
template:`<div>{{name}}</div>`
})
总结点:上面说过使用reactive
就相当于proxy({name:'joker',age:20})
,当使用toRefs
后,就相当于{name:proxy({value:'joker'}),age:proxy({value:20})}
,将代理细化到内部的每一个属性,实现每一个属性都有单独的响应式引用。当细化到每个属性的时候,变相的看就是将对象内的基本属性转换成响应式引用。
响应式引用-toRef(一般不建议使用)
当解构的时候,如果解构对象没有对应属性,使用toRefs
会出现报错,因为解构出来的这个属性不会被赋予默认值,这个使用可以使用toRef
实现增加默认值。代码如下:
const app = Vue.createApp({
setup(props,context){
const { reactive,toRefs } = Vue;
const user = reactive({name:"joker",age:20});
const { phone } = toRefs(user);
setTimeout(()=>{
phone.value = 'dell'; // 这里会出现报错,因为phone在user里面不存在
},2000);
return { phone };
}
template:`<div>{{phone}}</div>`
})
// 修改后如下
const { reactive,toRef } = Vue;
const user = reactive({name:"joker",age:20});
// 使用toRef,就会给phone一个默认值,在对phone进行操作的时候不会报错
// 相对于toRefs来说,toRef只能解构一个属性
const phone = toRef(user,'phone');
setup 之 context
在定义setup
的时候,能够定义两个参数分别是props
和context
,针对context
内包含哪些数据呢。见代码如下:
const app = Vue.createApp({
methods :{
fatherChange(){
alert("fatherChange")
}
},
template:`<child @click="fatherChange"></child>`
})
app.component('child',{
template:`<div @click="change">change</div>`
setup(props,context){
const { attrs,slots,emit } = context;
function change(){
emit("fatherChange");
}
return { change };
}
})
- attrs:接收的是父组件传入的 Non-Props 属性,正常子组件接收传入的参数使用
props
接收,如果无props
,父组件传入的数据就会被称为 Non-Props 属性,这个属性就会被attrs
接收; - slots:接收所有的插槽集合
- emit:触发父组件的事件,之前都是使用
this.$emit
来触发,在setup
内,直接使用此emit
即可触发(另外在setup
内也是不能直接使用this
关键字的,因为setup
优先于组件的created
生命周期函数前就创建,此时this
还没有形成)
Composition API 的封装思路
直接将所有的逻辑写在setup
内会导致代码很拥挤,因此可以考虑采用封装的思想(类似 Java 的封装),将封装好的逻辑引入到setup
内。示例代码如下:
<body>
<div id="root">
<div>
<input type="text" @input="inputChange" />
<button @click="itemChange">提交</button>
</div>
<div>
<ul>
<li v-for="(item,index) in list">{{item}}</li>
</ul>
</div>
</div>
</body>
<script>
// 封装list处理逻辑
const listRelativeEffect = () => {
const { reactive } = Vue;
const list = reactive([]);
const addItem = (item) => {
list.push(item);
};
return {
list,
addItem,
};
};
// 封装input标签处理逻辑
const inputRelativeEffect = () => {
const { ref } = Vue;
const inputValue = ref("");
const inputChange = (event) => {
inputValue.value = event.target.value;
};
return {
inputValue,
inputChange,
};
};
const app = Vue.createApp({
setup() {
// 将list处理逻辑引入
const { list, addItem } = listRelativeEffect();
// 将input标签处理逻辑引入
const { inputValue, inputChange } = inputRelativeEffect();
// 在点击提交的时候,将input标签内的值添加到list中
const itemChange = () => {
addItem(inputValue.value);
};
return {
list,
inputValue,
inputChange,
addItem,
itemChange,
};
},
});
const vm = app.mount("#root");
</script>
优化点:在点击提交的时候,需要将input
标签的值添加到list
集合中,这里定义了一个方法作为处理,但是这里还可以采用下面的缩减方式
<!--将click事件内容写为箭头函数-->
<button @click="()=>addItem(inputValue)">提交</button>
<!--将原来itemChange相关的内容删除即可-->
Composition API 中计算属性 computed 应用
<body>
<div id="root">
<div @click="add">{{count}}---{{computedValue}}</div>
</div>
</body>
<script>
const app = Vue.createApp({
setup() {
const {
ref,
computed, //在这里引入computed
} = Vue;
const count = ref(0);
const add = () => {
count.value += 1;
};
// 书写方式一:在没有修改computedValue需求的时候
// const computedValue = computed(()=>{
// return count.value + 10;
// })
// 书写方式二:在需要修改computedValue时,需要写成如下格式
let computedValue = computed({
get: () => {
return count.value + 10;
},
// 接收其他业务逻辑代码给computedValue赋的值
set: (param) => {
count.value = param - 8;
},
});
setTimeout(() => {
computedValue.value = 10; // computedValue赋值
}, 3000);
return {
count,
add,
computedValue,
};
},
});
const vm = app.mount("#root");
</script>
Composition API 内的 watch
watch
具有的特性:
- 监听的参数必须是方法、
ref
属性、reactive
对象、数组 - 惰性特性,在页面刚加载的时候
watch
是不会监听和执行其内业务代码的,只有在监听的数据发生变化才会触发 - 可以同时监听多个数据,用数组方式书写
<body>
<div id="root">
<input type="text" v-model="name" />
<input type="text" v-model="englishName" />
</div>
</body>
<script>
const app = Vue.createApp({
setup() {
const {
reactive,
watch, // 引入watch
toRefs,
} = Vue;
const user = reactive({
name: "张三",
englishName: "joker",
});
// watch(user.name,(current,prev) => {})
// 当我们直接用上面的方式,watch监听user.name属性的时候,是会报如下的错误信息
// 错误信息:watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types
// 提示监听的数据只能是function、ref属性、reactive对象、数组,因此这里user.name不能被监听
// 需要修改成watch(() => user.name,(current,prev) => {})
// 监听多个写法如下:
watch(
[() => user.name, () => user.englishName],
([curName, curEnglishName], [prevName, prevEnglishName]) => {
console.log(curName, prevName);
console.log(curEnglishName, prevEnglishName);
}
);
const { name, englishName } = toRefs(user);
return {
name,
englishName,
};
},
});
const vm = app.mount("#root");
</script>
知识点:watch
是有惰性的,但是可以通过配置修改其为即时性的,修改如下:
watch(
[() => user.name, () => user.englishName],
([curName, curEnglishName], [prevName, prevEnglishName]) => {
console.log(curName, prevName);
console.log(curEnglishName, prevEnglishName);
},
{
immediate: true, // 设置其为即时性,在页面加载的时候,就会执行监听的业务逻辑
}
);
Composition API 内的 watchEffect
watchEffect
也是一种监听器,其监听的是回调函数中的所有可监听的数据(也就是watchEffect
内包含响应式变量发生变化,就会触发监听操作),且是即时性的,一般在表单提交的时候,监控当前表单属性的变化,效果很好,因为这个时候不需要提供历史值,用不到watch
,监听的代码也会很简洁。代码如下:
<body>
<div id="root">
<input type="text" v-model="name" />
<input type="text" v-model="englishName" />
</div>
</body>
<script>
const app = Vue.createApp({
setup() {
const {
reactive,
watchEffect, //引入watchEffect
toRefs,
} = Vue;
const user = reactive({
name: "张三",
englishName: "joker",
});
watchEffect(() => {
console.log(user.name); // 可监听user.name的变动
});
const { name, englishName } = toRefs(user);
return {
name,
englishName,
};
},
});
const vm = app.mount("#root");
</script>
注意点:watchEffect
对数据进行监听,只能获取最新的数据,不能获取原数据,这个是和watch
的一个重要的区别。
如果需要监听器在一定时间后自动结束监听,可用如下方式实现:(此方式对watch
和watchEffect
都是可以适用的)
const demoWatch = watchEffect(() => {
console.log(user.name);
});
// 这个方法可以写在监听器的回调方法内部和外部都可以
setTimeout(() => {
demoWatch();
}, 5000);
composition 内的生命周期函数
在所有的生命周期方法前加上on
,从 Vue 中引入即可正常使用。代码如下:
const app = Vue.createApp({
setup() {
const { onMounted } = Vue;
onMounted(() => {
console.log("onMounted");
});
},
});
注意点:
- 针对非
beforeCreated
和created
之外的生命周期函数,在前面加上on
即可 - 在 Composition API 内新增了两个生命周期函数,分别是
onRenderTracked
和onRenderTriggered
,onRenderTracked
是在页面渲染的时候,收集响应式依赖触发,初次渲染或者后期响应式依赖数据变化引发页面重新渲染都会触发;onRenderTriggered
只会在响应式依赖数据变化引发页面重新渲染时触发。
Composition API 内的 provide 和 inject
在父组件给更深的子组件传递数据的时候,可以使用到provide
和inject
,在 Composition API 中写法如下:
const app = Vue.createApp({
setup(){
const { provide,ref } = Vue;
const name = ref("hello");
provice('name',name);
},
template:`<child></child>`
})
app.component('child',{
setup(){
const { inject } = Vue;
const name = inject('name',"defaultValue"); //如果没有接收到name,则给予默认值defaultValue
return {name}
}
template:`<div>{{name}}</div>`
})
如果子组件需要修改name
的值,根据 Vue 的单向数据流的规则,子组件不要直接修改值,而是告诉父组件,由父组件自行修改,同时也为了避免子组件不小心修改,将传入的值做readonly
设置,代码如下:
<body>
<div id="root">
<child></child>
</div>
</body>
<script>
const app = Vue.createApp({
setup() {
const { provide, ref, readonly } = Vue;
const name = ref("dell");
// 设置传入的name为readonly,不影响上面定义的name修改,防止子孙组件误修改
provide("name", readonly(name));
const handleChange = (value) => {
//value接收传入的值
name.value = value;
};
// 将修改name值的方法提供给子孙组件
provide("handleChange", handleChange);
},
});
app.component("child", {
setup() {
const { inject } = Vue;
//如果没有接收到name,则给予默认值defaultValue
const name = inject("name", "defaultValue");
// 获取父组件的提供的方法
const handleChange = inject("handleChange");
const change = () => {
handleChange("joker"); // 调用父组件给定的方法
};
return {
name,
change,
};
},
template: `<div @click="change">{{name}}</div>`,
});
const vm = app.mount("#root");
</script>
Composition API 通过 ref 获取 dom
之前在非 Composition API 语法里面通过 ref 获取 dom 对象是通过this.$refs
,但是在 Composition API 语法中存在变化,具体代码如下:
<body>
<div id="root">
<!--这里的ref值保持和js中return的domRef相同即可-->
<div ref="domRef">this is dom ref</div>
</div>
</body>
<script>
const app = Vue.createApp({
setup() {
const { ref, onMounted } = Vue;
// 固定定义方法
const domRef = ref(null);
onMounted(() => {
// 通过domRef.value获取当前dom对象
console.log(domRef.value);
});
return {
domRef,
}; // 将domRef返回
},
});
const vm = app.mount("#root");
</script>
VueCLI 脚手架安装和使用
VueCLI 脚手架安装
- 安装 node,从 node 官网上下载 node 的最新版本 LTS,傻瓜式安装即可
- 一般 node 安装结束 npm 也会同步安装成功
- 更换源,因为默认的源是国外的,在使用的时候会很慢,而且容易出错,这里需要将源换成国内的
- 首先:
npm install nrm -g
- 然后:
nrm ls
,查看有哪些国内的源可以使用 - 再然后:
nrm use taobao
,选择你要使用的源
- 首先:
- 如果之前使用过脚手架,需要先卸载
npm uninstall vue-cli -g
和yarn global remove vue-cli
- 安装最新的脚手架工具,执行命令
npm install -g @vue/cli
,如果需要指定脚手架版本,可以在后面加上版本号@vue/cli@4.4.4
脚手架安装完成!
使用脚手架创建 Vue 工程
- 执行
vue create [appName]
命令 - 第一步选择:Manually select features
- 第二步选择:选择 Babel 和 Linter/Formatter(上下键移动,空格选中,回车确认)
- 第三步选择:3.X
- 第四步选择:ESlint with error prevention only
- 第五步选择:Lint on save
- 第六步选择:In dedicated config files
- 最后异步选择 N,回车后等子弹飞一会
完成 vue 工程的创建。
注意点:在第二步中,如果需要使用 vue-router、vuex,需要将其选中
vue-router
在实际项目中,会有很多的页面,当点击某个页面的时候,需要跳转到对应 vue 组件,此时就需要使用vue-router
进行配置。使用vue-router
生成项目后,会有对应router
目录,对应的index.js
文件,内容如下:
import { createRouter, createWebHashHistory } from "vue-router";
// 在这里表示同步加载,在访问首页的时候就会自动加载,这种可能导致打开首页有一定的卡顿,因为加载的内容太多
import HomeView from "../views/HomeView.vue";
import LoginView from "../views/LoginView.vue";
const routes = [
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/about",
name: "about",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// 表示异步加载路由,在没有打开此页面之前,此页面的内容不会提前加载,而是等待需要展示的时候才会加载(懒加载)
// 具体选择异步加载路由还是同步加载路由,根据实际项目需求而定
component: () => import("../views/AboutView.vue"),
},
{
path: "/login",
name: "login",
component: LoginView,
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
需要在main.js
文件中对vue-router
插件进行使用:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router"; // 导入router相关的配置
import store from "./store";
// 使用vuex的store
// 使用vue-router的router
createApp(App).use(store).use(router).mount("#app");
在 App.vue 中配置路由标签和路由对应组件内容的展示
<template>
<nav>
<!-- 跳转路由的标签 -->
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/login">Login</router-link>
</nav>
<!-- 负责展示当前路由对应的组件内容 -->
<router-view />
</template>
VueX
在 vue 里面,有很多数据需要跨页面、跨组件之间使用,如果使用父子组件之间的传递,会导致项目数据的维护困难,在 vue 中引入了 VueX 插件,让共用数据多页面使用更为方便。
在加入 VueX 后,会在项目的 src 目录下生成一个store
目录,内容如下:
import { createStore } from "vuex";
// VueX 数据管理框架,可以跨页面、组件
// VueX 创建了一个全局唯一的仓库,用来存放全局的数据
export default createStore({
state: {
name: "joker", // 在这里定义一个属性name
},
getters: {},
mutations: {},
actions: {},
modules: {},
});
在其他页面或者组件中使用此属性的方法:
computed: {
myName() {
return this.$store.state.name; // 引入store中定义的name属性
},
}
上面写的是获取 VueX 得属性的方法,采用的是原始的方式,如果这里采用 composition 方式获取,写法如下:
export default {
setup() {
const store = useStore();
// const myName = store.state.name;
// 通过解构的方式获取name值
const { name } = toRefs(store.state);
return { name };
},
};
如果需要修改这个 name 值的话,就需要进行一下的步骤:(最全步骤)
- 首先调用
dispatch
方法,触发一个派发action
,对应的方法是定义在actions
内; actions
内的方法执行,使用commit
调用mutations
内的方法;- 在
mutations
内执行的方法中修改对应的值。
注意点:
- 根据约定
mutations
内一般是定义同步修改方法,不是异步修改(虽然也可写异步修改逻辑,但是不建议); - 根据约定
actions
内一般是定义异步修改方法(虽然也可写同步修改逻辑,但是不建议); - 从以上两点可以得出结论,如果需要同步修改的时候,直接调用
commit
方法,如果需要异步修改的时候,直接调用dispatch
方法; - 在
actions
中的方法,接收到的参数第一个是store
,在 mutations 中的方法,接收到的第一个参数是state
。
示例代码:
// 在组件中使用并修改name
import { useStore } from "vuex";
import { toRefs } from "vue";
export default {
setup() {
const store = useStore();
// const myName = store.state.name;
// 通过解构的方式获取name值
const { name } = toRefs(store.state);
const changeName = () => {
// 方式一:直接通过调用commit方法,会直接定位到store内mutations内的change方法
// store.commit("change","joker-yang");
// 方式二:调用dispatch方法,会直接定位到store内actions内的change方法
store.dispatch("change", "yangbao");
};
return { name, changeName };
},
};
// 在store中定义对应change方法
// commit与mutations做关联
// 根据约定俗成的规定,mutations里面是不可以写异步操作代码
mutations: {
// 在mutations里面,接收的第一个参数是state,第二个是对应的值
change(state, value) {
state.name = value;
}
},
// 异步代码可写在actions内(异步修改)
// dispatch与actions做关联
actions: {
// 在actions里面,接收的第一个参数是store,第二个是对应的值
change(store, value) {
setTimeout(() => {
store.commit("change", value);
}, 2000)
}
}