Vue
Vue的API书写风格有两种,一种是选项式API,一种是组合式API
npm create vue@latest
这条命令可以初始化一个vue项目
基础语法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
用户名:<p>{{ username }}</p>
年龄:<p>{{ age }}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
username: '张三',
age: 18
}
}
});
</script>
</body>
</html>这样可以动态输入数据
<p>{{1 > 2 ? 'yes' : 'no'}}</p>在花括号中还可以写逻辑运算符
<script>
const vm = new Vue({
el: '#app',
data() {
return {
title: "hello vue"
}
},
methods: {
output() {
return "title is " + this.title
}
}
});
</script><h1>{{ output() }}</h1>可以在methods中写各种方法
computed: {
outputContent() {
console.log("computed");
return "title is " + this.title
}
}当然还有一个计算属性。如果要打印多个的话,用{{ output() }} 就会重新获取,如果用 {{ outputContent }} 的话每次调用就会用缓存的数据(它具有缓存属性),可以提高性能
还有一个属性监听器watch :
watch: {
title(newValue, oldValue) {
console.log("watch", newValue, oldValue);
}
}指令
<p v-text="title"></p>也可以通过v-text 来向花括号那样插值,用法是一样的
<p v-html="htmlContent"></p>v-html 可以向标签内覆写html并渲染出来
<p v-for="item in 5">Content</p>v-for 可以循环输出多个标签
<p v-for="item in arr">Content:{{ item }}</p>可以把数组中的内容循环输出
<p v-for="(item,key,index) in obj">Content:{{ item }}{{ key }}{{ index }}</p>obj:{a:1,b:2,c:3}遍历对象
<p v-if="true">标签内容</p>
<p v-if="false">标签内容</p>v-if 可以根据真假来控制div的显示与不显示,不显示就会销毁,当然也可以用v-show来控制显示,但它控制的只是display的样式
v-for建议写上key属性

<p v-bind:title="title">1</p>
<p :title="title">1</p>事件指令
<button v-on:click="title = 'hello button'">按钮</button>
<button v-on:click="output">按钮</button>
<button @click="output">按钮</button>表单指令
v-model 可以双向数据绑定
<input type="text" v-model="inputValue">
<p v-text="inputValue"></p> data() {
return {
inputValue: ""
}这样更改输入框的内容文字标签内的也会跟着动态改变
<select v-model.number="n">转成数字
修饰符
<input type="text" v-model.trim="inputValue">.trim 可以去除输入框两端的空格
入口
main.ts:
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
这个入口文件就初始化了App到index.html的#app上
在App.vue 中,在<template>标签中写页面的结构(HTML)<script>中写js或ts,<style>标签中写样式
App.vue
<template>
<div class="app">
<h1>Hello World</h1>
</div>
</template>
<script lang="ts">
export default {
name: 'App',
}
</script>
<style>
.app {
background-color: #ddd;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
</style>我们可以在components 中写其他页面,比如Person.vue:
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<p>年龄:{{ age }}</p>
<button @click="showTel">查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name: 'Person',
data() {
return {
name: 'John Doe',
age: 30,
telephone: '123-456-7890'
}
},
methods: {
showTel() {
alert(`电话:${this.telephone}`);
}
}
}
</script>
<style scoped>
.person {
background-color: skyblue;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
}
</style>style里面的scoped是让这个样式只在这个vue文件里面生效的
但是我们还需要让App.vue认识Person.vue
import Person from './components/Person.vue'
export default {
name: 'App', //组件名
components:{Person} //注册组件
}然后我们使用<Person/> 就可以让那个页面出现在页面里了

组件的通信方式
<template>
<div class="app">
<Connect msg="Hello from App.vue" count="5" />
</div>
</template>
<script lang="ts">
import Connect from './components/Connect.vue'
export default {
name: 'App', //组件名
components: { Connect } //注册组件
}
</script>
<style>
.app {
background-color: #ddd;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
</style><template>
<p>{{ msg }}</p>
<p>Count: {{ count }}</p>
</template>
<script lang="ts">
export default {
name: 'Connect',
props: {
msg: String,
count: {
type: [Number, String],
default: 100,
required: true
}
},
data() {
return {
name: 'John Doe'
}
}
}
</script>我门可以通过属性来传值,然后通过props来接收,还可以规定接收值的类型,也可以用default 来设置默认值。可以用required 来规定这个数据是否是必须的。
这是父传子的方式,当然子组件也可以传给父组件,我们就需要用.emit 设置一个自定义触发事件
<template>
<div class="app">
<Connect msg="Hello from App.vue" count="5" @child-count-change="handler" />
<p>{{ childData }}</p>
</div>
</template>
<script lang="ts">
import Connect from './components/Connect.vue'
export default {
name: 'App', //组件名
components: { Connect }, //注册组件
data() {
return {
childData: 0
};
},
methods: {
handler(childCount: any) {
console.log('子组件的count值变化了', childCount)
this.childData = childCount
}
}
}
</script>
<style>
.app {
background-color: #ddd;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
</style><template>
<p>{{ msg }}</p>
<p>Count: {{ count }}</p>
<button @click="handler"></button>
</template>
<script lang="ts">
export default {
name: 'Connect',
props: {
msg: String,
count: {
type: [Number, String],
default: 100,
required: true
}
},
data() {
return {
childCount:0
}
},
methods: {
handler() {
this.childCount++
this.$emit('child-count-change',this.childCount)
}
}
}
</script>这样{{ childData }} 就可以获取组件里面传过来的数据了。@child-count-change 可以对child-count-change 进行监听,监听到数据后会执行这个属性内的方法并把值传进去
组合式API
刚才我们写的都是选项式API
export default {
name: 'Person',
setup() {
//数据
let name = '张三';
let age = 18;
let tel = '12345678901';
//方法
function changeName() {
name = '李四';
}
function changeAge() {
age += 1;
}
function showTel() {
alert(`电话:${tel}`);
}
return { name, age, tel, changeName, changeAge, showTel }
}
}我们可以把变量和方法都写进setup() ,但是这里面的变量还不是响应式的。所有变量和方法都要用return返回。我们也可以用return () => a 来返回一个简单的数据。但是这样每写一个就要在return里面写一个,十分麻烦
<script lang="ts">
export default {
name: 'Person'
}
</script>
<script lang="ts" setup>
let a = 10;
</script>我们把代码写在有setup后缀的script标签里就可以不用return就直接使用
<script lang="ts" setup name="Person222">可以在这个标签里写上name属性,这样就不需要export那个代码来了
我们现在需要把setup里面数据变为响应式的
import { ref } from 'vue';
//数据
let name = ref('张三');
let age = ref(18);
let tel = '12345678901';
//方法
function changeName() {
name.value = '李四';
}
function changeAge() {
age.value += 1;
}要先引入ref函数,然后它会变成一个对象,然后通过.value改值

想要对=将对象类型的数据进行响应式处理,就需要用reactive 替换ref

reactive声明的如果直接更改整个对象会导致失去响应式,需要用Object.assign()来浅拷贝才行。
toRefs与toRef
如果每个变量都要写ref的话未免就太麻烦了,我们就可以用toRefs或者toRef
import { reactive,toRefs,toRef } from 'vue';
let person = reactive({
name: 'John Doe',
age: 18
});
let {name,age} = toRefs(person);
let nl = toRef(person,'age');
function changeName() {
name.value = 'Jane Doe';
}
function changeAge() {
age.value = 25;
}
Computed属性
<template>
<div class="person">
姓:<input type="text" v-model="firstName"></input><br>
名:<input type="text" v-model="lastName"></input><br>
全名:<span>{{ firstName }}-{{ lastName }}</span>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref } from 'vue';
let firstName = ref('张');
let lastName = ref('三');
</script>
<style scoped>
.person {
background-color: skyblue;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
}
button {
margin: 0 5px;
}
</style>这里写了一个组合姓名的逻辑,但是如果我想要名字首字母大写,在模板里面写太多代码明显是不符合规范的,这时候我们就可以用到计算属性
<template>
<div class="person">
姓:<input type="text" v-model="firstName"></input><br>
名:<input type="text" v-model="lastName"></input><br>
全名:<span>{{ fullName }}</span>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref,computed } from 'vue';
let firstName = ref('zhang');
let lastName = ref('san');
let fullName = computed(() => {
return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value;
});
</script>
<style scoped>
.person {
background-color: skyblue;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
}
button {
margin: 0 5px;
}
</style>这样就行了,但是computed方法是只读的
import { ref, computed } from 'vue';
let firstName = ref('zhang');
let lastName = ref('san');
let fullName = computed({
get() {
return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value;
},
set(vl) {
const [str1,str2] = vl.split('-');
firstName.value = str1
lastName.value = str2
console.log(vl);
}
});用get()与set()就可以做到改变fullname的值,set()会收到新改的值作为参数
Watch

import { ref, watch } from 'vue';
let sum = ref(0);
function changeSum() {
sum.value+=1;
}
watch(sum,(newVal, oldVal) => {
console.log(`sum的值从${oldVal}变成了${newVal}`);
} );我们可以引入并使用watch函数,然后用回调函数接收旧数据与新数据
const stopWatch = watch(sum,(newVal, oldVal) => {
console.log(`sum的值从${oldVal}变成了${newVal}`)
if (newVal > 10) {
stopWatch();
}
});达到条件停止监视

但是如果监听对象的话监听的只是对象的地址变化,如果要监听内部的属性就不可以,所以我们需要手动开启深度模式
watch(person, (newVal, oldVal) => {
console.log('person changed from', oldVal, 'to', newVal);
}, { deep: true });在后面追加一个对象,在里面可以写上这个监视函数的各种配置,deep就是控制深度模式,immediate可以让数据没有变化,页面刚加载的时候执行一次监视。如果只写一个形参那么就会默认接收最新的数据。reactive默认开启深度监视
如果我们只想监听对象里一个特定的属性,就可以用getter函数
watch(()=>{return person.name},(newValue,oldValue) => {
console.log(newValue,oldValue);
})这样就会接收到person.name了
watch(person.car,(newValue,oldValue) => {
console.log(newValue,oldValue);
})对象里面的“对象”可以直接监听,不过还是建议写成函数的,因为如果整个改person.car不会监听到
watch([()=> person.age,()=> person.car],(newValue,oldValue) => {
console.log(newValue,oldValue);
})监听多个
watchEffect
watch([temp, height], (value) => {
let [newTemp, newHeight] = value
if(newTemp >= 60 || newHeight >= 100){
console.log("发请求")
}
})监听里面的逻辑需要多少数据就要监听多少数据,这样未免太麻烦了,特别是在数据特别多的时候
watchEffect(() => {
if(temp.value >= 60 || height.value >= 100){
console.log("发请求")
}
})这时候就可以用watchEffect,可以自动监视用到的数据。watchEffect会在页面加载自动执行内部的代码一次
ref属性
<template>
<div class="person">
<h1>中国</h1>
<h2 ref="title2">北京</h2>
<h3>尚硅谷</h3>
<button @click="showLog">点击我输出h2</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref } from 'vue';
// 创建一个title2,用于存储ref标记的内容
let title2 = ref()
function showLog() {
console.log(title2.value);
}
</script>
<style scoped>
.person {
background-color: skyblue;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
}
button {
margin: 0 5px;
}
</style>可以用ref属性代替id,他的value会输出标签本身。defineExpose可以放在子组件,这样父组件获取子组件的ref就可以获取子组件内部的变量
TypeScript
接口
//定义一个接口用于限制person对象的具体属性
export interface PersonInter {
id: string,
name: string,
age:number
}
导出后我们可以在别的地方使用
import { type PersonInter } from '@/types/'
let person: PersonInter = {
id: "asdsjf01",
name: "张三",
age: 18
}
let personsList: Array<PersonInter> = [
{
id: "asdsjf01",
name: "张三",
age: 18
},
{
id: "asdsjf02",
name: "李四",
age: 20
},
{
id: "asdsjf03",
name: "王五",
age: 22
}
]接口可以规定每个属性都应该是什么属性名。还可以用Array<PersonInter> 规定数组里面放什么样的对象
export type Persons = Array<PersonInter>也可以自定义一个类型,把Array<PersonInter>替换为Persons
export interface PersonInter {
id: string,
name: string,
age?:number
}加问号的就是可选参数
断言:
talkList: JSON.parse(localStorage.getItem('loveTalk') as string)TS可能担心返回的不是正确的类型,因为JSON.parse只接受字符串类型的数据,但是localStorage这里取过来的也有可能是null类型,这时候我们就可以在后面加上as String来“保证”它返回的一定是字符串类型,这样编译的时候就不会报错了,如果运行的时候返回不正确的类型还是会报错
断言还有一种老写法:
<类型>名Props
我们可以用这个向其他组件传递数据
父组件:
<Person a="哈哈"/>子组件:
import { defineProps } from 'vue'
defineProps(["a","b"])引入之后在调用defineProps() ,传入一个数组,数组内写上数据名就可以接收到了,我们可以直接在{{}}写上传进来的数据名
但是这个a如果我们使用console.log()是打印不出来的,它还不是一个变量。但是这个函数会给一个对象的返回值,内部包含着数据,我们可以把返回值赋值给一个变量,这样就能打印出来了:
let x = defineProps(["a","b"])
console.log(x.a)let x = defineProps<{ list: Persons }>()我们还可以限制它传进来的值是什么类型的。在list后面加上问号就代表这个数据可传可不传
import { defineProps,withDefaults } from 'vue'
import { type Persons } from '@/types/'
withDefaults(defineProps<{ list: Persons }>(), {
list: () => [{ id: "000", name: "默认", age: 0 }]
})可以设置默认值,但是list冒号后面收到的必须是一个函数返回的返回值
但是这样只能传字符串,我们可以在前面加上冒号来传数据:
<Child :car="car" :sendToy="getToy"/>生命周期

Vue2的生命周期
钩子就是生命周期函数

debugger; 可以在这个位置停止代码的运行
//创建前的钩子
beforeCreate() {
console.log('创建前');
},
//创建完毕的钩子
created() {
console.log('创建完毕');
},
//挂载前的钩子
beforeMount() {
console.log('挂载前');
},
//挂载完毕的钩子
mounted() {
console.log('挂载完毕');
},
//更新前的钩子
beforeUpdate() {
console.log('更新前');
},
//更新完毕的钩子
updated() {
console.log('更新完毕');
},
//销毁前的钩子
beforeDestroy() {
console.log('销毁前');
},
//销毁完毕的钩子
destroyed() {
console.log('销毁完毕');
}Vue3的生命周期
//创建
console.log('创建');
//挂载
onBeforeMount(() => {
console.log('挂载前');
});
onMounted(() => {
console.log('挂载完');
});
onBeforeUpdate(() => {
console.log('更新前');
});
onUpdated(() => {
console.log('更新完');
});
//卸载
onBeforeUnmount(() => {
console.log('卸载前');
});
onUnmounted(() => {
console.log('卸载完');
});父组件也是有生命周期的,并且子组件比父组件先挂载
自定义hook
import {ref,reactive} from 'vue'
let sum = ref(0)
let dogList = reactive(['https:\/\/images.dog.ceo\/breeds\/clumber\/n02101556_3736.jpg'])
function add(){
sum.value += 1;
}
async function getDog(){
fetch('https://dog.ceo/api/breeds/image/random')
.then(response => response.json())
.then(data => {
dogList.push(data.message)
})
.catch(error => console.error('Error:', error));
}这里用fetch获取了狗的图片,但是如果我们有很多要获取的东西就要写多个方法,这样会很乱并降低可读性。
我们可以创建一个hook 文件夹,在下面写上useDog.ts (命名规范就是useSth)
import { ref, reactive } from 'vue'
export default function qwe() {
let dogList = reactive(['https:\/\/images.dog.ceo\/breeds\/clumber\/n02101556_3736.jpg'])
async function getDog() {
fetch('https://dog.ceo/api/breeds/image/random')
.then(response => response.json())
.then(data => {
dogList.push(data.message)
})
.catch(error => console.error('Error:', error));
}
//向外部提供
return { dogList, getDog }
}我们在useSum文件夹里写入:
import { ref } from 'vue'
export default function () {
let sum = ref(0)
function add() {
sum.value += 1
}
return { sum, add }
}最后我们在Peson.vue只需要调用这些写好的方法就行了
import useSum from '@/hooks/useSum'
import useDog from '@/hooks/useDog'
const { sum, add } = useSum()
const { dogList, getDog } = useDog()当然,在hooks里面也可以写钩子,计算属性等等
路由
在使用路由之前,我们需要使用npm i vue-router来安装一下“路由器”,我们还需要创建一个router文件夹,并在下面创建一个index.ts ,在里面引入组件,并且创建一个路由器并导出
//创建一个路由器并暴露出去
import { createRouter, createWebHistory } from "vue-router";
import Home from '@/components/Home.vue'
import News from '@/components/News.vue'
import About from '@/components/About.vue'
//创建路由器
const router = createRouter({ //路由器的工作模式
history: createWebHistory(),
routes: [
{
path: '/home',
component: Home
},
{
path: '/news',
component: News
},
{
path: '/about',
component: About
}
]
})
export default router; //暴露出去然后回到main.ts使用路由器:
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router' //引入路由器
//创建一个应用
const app = createApp(App)
//使用路由器
app.use(router)
//挂载整个应用到app容器中
app.mount('#app')但是这时候Vue还不知道要把路由后的内容放在哪里,这时候就需要我们在App.vue里面引入
import {RouterView} from 'vue-router'
<RouterView></RouterView>然后内容就会渲染在那个标签里了。如果需要做到切换路由,我们需要引入RouterLink,然后把a标签替换为<RouterLink></RouterLink> ,在to属性中写上要前往的地方。我们还可以使用active-class属性来规定激活点击之后会获得什么类名
路由组件一般会放在views或pages文件夹里
嵌套路由
有时候我们在路由里面还要写一个路由,这个就叫嵌套路由。我们需要来到router/index.ts引入子路由,然后找到要添加嵌套路由的路由:
{
path: '/news',
component: News,
children: [
{
path: 'detail',
component: Detail
}
]
}注意,子路由的路径不需要写斜杠
query参数
<RouterLink to="/news/detail?a=哈哈&b=你好&c=嘿嘿">{{ news.title }}</RouterLink>我们在to里面的路径写上查询字符串,就可以在接收的组件里面去接收:
<template>
<ul class="news-list">
<li>编号:{{ route.query.a }}</li>
<li>标题:{{ route.query.b }}</li> </ul>
</template>
<script setup lang="ts" name="About">
import { useRoute } from "vue-router";
const route = useRoute();
</script>模板字符串传值:
<RouterLink :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">{{ news.title }}</RouterLink>
我们也可以选择一种可读性更好的写法:
<ul>
<li v-for="news in newsList" :key="news.id">
<router-link :to="{
path: '/news/detail',
query: {
id: news.id,
title: news.title
}
}">
{{ news.title }}
</router-link>
</li>
</ul>params参数
我们还可以用路径的方式传参数:
<router-link to="/news/detail/哈哈/嘿嘿">{{ news.title }}</router-link>但是我们需要在router/index.ts里配置好占位符,规定路径后面哪个路径对应哪个参数(在后面写上?表示可选参数):
{
path: '/news',
component: News,
children: [
{
path: 'detail/:id/:title',
component: Detail
}
]
},<template>
<ul class="news-list">
<li>编号:{{ route.params.id }}</li>
<li>标题:{{ route.params.title }}</li>
</ul>
</template>
<script setup lang="ts" name="About">
import { useRoute } from "vue-router";
const route = useRoute();
</script>也可以使用对象写法:
<router-link
:to="{
name: 'news-detail',
params: {
id: news.id,
title: news.title
}
}
">
{{ news.title }}
</router-link>但是使用对象写法不能写路径,只能写路由名称,这个名称需要预先配置好
props配置
你想想,如果仅仅接收一个数据就要写那么多代码,未免过于麻烦了,这时候我们就需要props配置

props就相当于把数据放入了标签的属性当中
<template>
<ul class="news-list">
<li>编号:{{ id }}</li>
<li>标题:{{ title }}</li>
</ul>
</template>
<script setup lang="ts" name="About">
defineProps(["id", "title"])
</script>启用props后就可以把接收过来的params参数直接使用
函数式写法:
{
path: '/news',
component: News,
children: [
{
name: 'news-detail',
path: 'detail/:id?/:title?',
component: Detail,
props(route) {
return route.query
}
}
]
}
则有,这样query和params的都可以接收,但是一般只有query用这个,params一般直接用props:true
replace属性
路由默认的模式是push模式,也就是说,在浏览器的历史记录里面,每到一个新的路由都会新建一条历史记录,而replace从始至终只会显示一条你最后浏览的路由
我们只需要给RouerLink加上replace模式就能切换到了:
<RouterLink replace></RouterLink>编程式路由导航
如果我们需要设计一个button,点击就能跳转到对应路由,但是如果没有学编程式路由导航,那就只能用RouterLink
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
onMounted(() => {
setTimeout(() => {
router.push('/news');
}, 3000);
})直接对router进行操作以实现编程式路由导航,直接一push就能跳转到/news页面。要想实现点击按钮跳转页面,我们只需要给button绑定一个onclick事件就行,但是要记住一定要给调用的函数里面传入一个值。router.push()里面和to的写法都是一样的
重定向
{
path: '/',
redirect: 'home'
}这个配置项可以把指定的路径重定向到一个路径
Pinia
Pinia就是一个集中式的状态管理工具。一些组件共享使用的数据就可以放入Pinia进行集中管理
搭建环境
npm i pinia
修改main.ts:
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import {createPinia} from "pinia";
const app = createApp(App)
//创建pinia
const pinia = createPinia()
//安装pinia
app.use(pinia)
app.mount('#app')存储/读取数据
我们需要先在src目录下创建一个store文件夹,在这个文件夹里面可以创建各种用于存储数据的ts文件。比如我们可以创建一个count.ts来存储计数相关数据:
import {defineStore} from "pinia";
export const useCountStore = defineStore('count',{
//真正存储数据的地方
state:()=>{
return {
sum:6
}
}
});state就是存储数据的地方。
import {useCountStore} from "@/store/count";
const countStore = useCountStore()
let n = ref(1)
function add() {
countStore.sum += n.value
}
function minus() {
countStore.sum -= n.value
}只要一引入,就可以使用这个数据了。
修改数据
countStore.sum -= n.value可以以复合直觉的方式直接修改
countStore.$patch({
sum: 888,
school: '尚硅谷',
address: '北京'
})也可以这样批量一次性修改
还有一种action的写法,但是意义不大,就是在store文件把数据处理的相关逻辑写好,比如:
actions:{
increment(value:number){
this.$patch({
sum: this.sum + value
})
}然后直接在其他文件调用数据上的这个方法:
countStore.increment(n.value)但是这样每次实用数据都要写store很麻烦,我们这时候就可以用storeToRefs来简化写法:
import {storeToRefs} from "pinia";
const countStore = useCountStore()
const {sum, school, address} = storeToRefs(countStore)可能你会问,我都有toRefs了,为什么还要用这个东西,因为toRefs会把store里面所有的属性都变成响应式的,性能很差劲
getters的使用
import { defineStore } from "pinia";
export const useCountStore = defineStore('count', {
actions: {
increment(value: number) {
this.$patch({
sum: this.sum + value
})
}
},
//真正存储数据的地方
state: () => {
return {
sum: 6,
school: 'tsinghua',
address: 'beijing'
}
},
getters: {
bigSum(state) {
return state.sum * 100
},
upperSchool(state) {
return state.school.toUpperCase()
}
}
});我们可以在配置文件里写一个getters属性来对数据进行一些操作,其实就类似于计算属性。之后在其他文件直接写这个函数的名字就能获取返回的修改的值了
$subscribe的使用
talkStore.$subscribe((mutate, state) => {
localStorage.setItem('loveTalk', JSON.stringify(state.talkList))
})我们可以用$subscribe监视数据本身的变化,并传入两个分别代表更新后的详情与更新后的数据的参数。我们可以把pinia的数据改成用localStorage引用,这样就能保留了
talkList: JSON.parse(localStorage.getItem('loveTalk') as string)组合式写法
import { defineStore } from "pinia";
import { reactive } from "vue";
export const useTalkStore = defineStore('talk', () => {
//相当于state
const talkList = reactive(
JSON.parse(localStorage.getItem('loveTalk') as string) || []
);
//相当于action
async function getATalk() {
const res = await fetch(`https://api.zxki.cn/api/twqh`);
const data = await res.text();
talkList.unshift({ id: Date.now().toString(), title: data });
}
return {
talkList,
getATalk
};
});
还可以以选项式的写法去写,但是记住一定要return出来
组件通信
父传子很简单,只需要传属性就够了
<Child :car="car">
但是子传父不一样,需要父组件先给子组件传递一个函数,子组件再通过调用这个函数传递给父组件
父组件传递函数:
<Child :car="car" :sendToy="getToy"/>子组件:
<button @click="sendToy(toy)">把玩具给父亲</button>子组件这里就需要通过一个按钮来调用这个函数并把数据穿过去,父组件那里在通过定义好的函数对数据进行处理
自定义事件
$event 是事件对象,在TS的lei类型是:Event
自定义事件这个东西是专门用于子传父的。我们可以像@click那样自定义一个事件,在这个事件里面写上要调用的函数,然后在子组件里触发事件并传值,以此来达到子传父的效果
<Child @send-toy="saveToy"/>
function saveToy(value:string){
console.log('saveToy',value)
toy.value = value
}比如在父组件的子组件标签里先写上一个自定义事件
<button @click="emit('send-toy',toy)">测试</button>
let toy = ref('奥特曼')
// 声明事件
const emit = defineEmits(['send-toy'])之后就可以在子组件通过emit写上事件名称与要传的值把数据传给父组件。但是必须要在子组件先通过defineEmits()定义一个事件,并赋值给一个变量。
mitt
这玩意可以实现组件间的任意通信。原来的通信方式太绕了,使用这个的话,只要两个组件连接到mitt就可以很方便地互相通信
使用mitt要先npm i mitt安装。然后创建src/utils文件夹并创建emitter.ts:
import mitt from 'mitt'
// 调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()
// 暴露emitter
export default emitter然后我们还需要在main.ts引入
import emitter from './utils/emitter'emitter能够绑定事件与触发事件

all是获取所有事件,emit触发事件,off解绑事件,on绑定事件
emitter.all.clear()这个可以批量解绑所有事件
使用方式
数据的接受方需要绑定一个事件,发送方才能发过来:
emitter.on('send-toy',(value:any)=>{
toy.value = value
})
// 在组件卸载时解绑send-toy事件
onUnmounted(()=>{
emitter.off('send-toy')
})这里的卸载是必不可少的,否则组件被卸载后数据监听还一直在那里。
<button @click="emitter.emit('send-toy',toy)">玩具给弟弟</button>发送方这里直接调用事件即可触发。
v-model

或许我们看过一些UI组件库,封装了一个输入框,他们可以直接在标签上写
<AtguiguInput v-model="username"/>来双向绑定,这是怎么实现的呢?
实际上,v-model是只能给input标签双向绑定的,你是给一个HTML元素上的这个东西,如果里给你的自定义组件上这个是没有效果的,所以这个东西需要我们去单独封装一个。
<input type="text" v-model="username">
<input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value"> v-model的原理实际上是这样的,给value整上冒号,后面绑定了一个input事件,只要输入就触发这个事件,把数据更新成用户最新输入的数据。
<AtguiguInput v-model="username"/> <AtguiguInput
:modelValue="username"
@update:modelValue="username = $event"
/> -->自定义组件使用v-model的原理也差不多。但是我们还需要去在底层改一改。
<template>
<input
type="text"
:value="ming"
@input="emit('update:ming',(<HTMLInputElement>$event.target).value)"
>
<br>
<input
type="text"
:value="mima"
@input="emit('update:mima',(<HTMLInputElement>$event.target).value)"
>
</template>
<script setup lang="ts" name="AtguiguInput">
defineProps(['ming','mima'])
const emit = defineEmits(['update:ming','update:mima'])
</script>
<style scoped>
input {
border: 2px solid black;
background-image: linear-gradient(45deg,red,yellow,green);
height: 30px;
font-size: 20px;
color: white;
}
</style>我们要在自定义组件这里设定一个emit接收更新事件,再把更新过来的值传到input
但是在新版中已经有了更好的做法:
https://cn.vuejs.org/guide/components/v-model
$attrs
我们可以用$attrs来实现祖传孙
在vue的设计中,props传的数据接收方没有接收的数据会放到attrs里,这时候我们就可以通过v-bind绑定$attrs来传递给对应的组件。
父:
<div class="father">
<h3>父组件</h3>
<h4>a:{{a}}</h4>
<h4>b:{{b}}</h4>
<h4>c:{{c}}</h4>
<h4>d:{{d}}</h4>
<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import {ref} from 'vue'
let a = ref(1)
let b = ref(2)
let c = ref(3)
let d = ref(4)
function updateA(value:number){
a.value += value
}
</script
>子
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<h4>c:{{ c }}</h4>
<h4>d:{{ d }}</h4>
<h4>x:{{ x }}</h4>
<h4>y:{{ y }}</h4>
<button @click="updateA(6)">点我将爷爷那的a更新</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a','b','c','d','x','y','updateA'])
</script>$parent与$refs
$refs与$parent这两个变量分别代表着组件的子元素与父元素,我们通过操控这两个变量,来实现子孙之间的通信。
<button @click="getAllChild($refs)">让所有孩子的书变多</button> function getAllChild(refs:{[key:string]:any}){
console.log(refs)
for (let key in refs){
refs[key].book += 3
}
} 比如这个就可以直接可以通过遍历修改每个子元素某个变量的值,但是在修改变量之前我们还需要把它暴露出去:
defineExpose({toy,book})provide_inject
这是一种专门用来实现祖孙传递的一个工具。可能你会问,为什么我不直接使用$attrs呢?因为$attrs还需要一个中间组件进行传递,很麻烦。
import {ref,reactive,provide} from 'vue'
let money = ref(100)
let car = reactive({
brand:'奔驰',
price:100
})
function updateMoney(value:number){
money.value -= value
}
// 向后代提供数据
provide('moneyContext',{money,updateMoney})
provide('car',car)
我们可以在祖组件引入provide来向后代提供数据,方法。
<template>
<div class="grand-child">
<h3>我是孙组件</h3>
<h4>银子:{{ money }}</h4>
<h4>车子:一辆{{car.brand}}车,价值{{car.price}}万元</h4>
<button @click="updateMoney(6)">花爷爷的钱</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
import { inject } from "vue";
let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(param:number)=>{}})
let car = inject('car',{brand:'未知',price:0})
</script>然后在孙组件内引入。这里的第一个是接收的数据,第二个参数则是默认值(如果祖组件没有传递数据默认用默认值),默认值还可以起到一个告诉TS传过来的数据是什么结构,类型的作用,否则会飘红。
插槽
默认插槽
我们可以使用插槽在组件内插入HTML元素。
<Game>
<span>nihao</span>
</Game>把组件标签写成闭标签的形式,然后在内部写上要加入的HTML元素
<template>
<div class="game">
<h2>游戏列表</h2>
<slot>11</slot>
</div>
</template>插入内容就会在子组件的slot标签内显示。如果在slot标签内先写上一些内容,那就会作为默认值。
具名插槽
顾名思义就是具有名字的插槽。我们可以把指定的HTML元素放到指定的插槽当中。我们需要在子组件的slot插槽定义一个name属性并写上插槽名,回到父组件,对HTML元素外包裹一个template标签,标签写上v-slot:插槽名,就可以把这个内容放到对应的插槽了,v-slot:插槽名 只能写在组件标签或template标签上。这个还有一个语法糖,v-slot:插槽名 可以简写为#插槽名 的形式。
默认插槽的name属性其实是default,不过平常一般不用单独写出来。
实例:
<Category>
<template #s2>
<video video :src="videoUrl" controls></video>
</template>
<template #s1>
<h2>今日影视推荐</h2>
</template>
</Category><template>
<div class="category">
<slot name="s1">默认内容1</slot>
<slot name="s2">默认内容2</slot>
</div>
</template>作用域插槽
如果我们想将一个遍历子组件数据的列表塞到插槽里面,就会遇到访问不到子组件内部数据的问题,这时候我们就需要使用作用域插槽把数据传给父组件
<template>
<div class="game">
<h2>游戏列表</h2>
<slot :youxi="games" x="哈哈" y="你好"></slot>
</div>
</template>我们要先在子组件的插槽标签内写上要传递的数据
<Game>
<template v-slot="params">
<ul>
<li v-for="y in params.youxi" :key="y.id">
{{ y.name }}
</li>
</ul>
</template>
</Game>副组件里可以在v-slot里接受,并使用数据。
<template #default="{youxi}">
<h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3>
</template>还可以使用花括号对传递过来的数据进行解构,这样就不用每次都写params或者什么别的了。#default="{youxi}" 等价于v-slot:"{youxi}"
一些API
shallowRef与shallowReactive
shallowRef与shallowReactive定义的数据只能操作数据的第一层,对象不能深入修改。shallowRef最多只能访问到.value,shallowReactive只能访问到第一层属性,
readonly与shallowReadonly
let a = readonly(b) 可以把一个数据变为只读属性,并且依旧跟随原来数据的响应式,但无法直接修改。
shallowReadonly() 只限制第一层的访问权限。比如用这个定义了一个对象,那么只有第一层不能修改,再深层的数据就可以修改了。
toRaw与markRaw
toRaw() 可以把一个响应式对象的数据提取出来变成普通的对象。Vue的对象是Proxy对象,对于其他库或者外部系统不能直接使用,就需要这个方法来转换一下。转换后的数据就失去响应式了,不会受原来的数据影响。
let b = markRaw(a) 定义的数据会使其永远不会变成响应式,你无法用ref或者reactive转变它。
teleport
我们可以使用<teleport to="name"></teleport>把包裹在其中的组件依靠于to中的元素
全局API
app.component("name",name) 在main.ts中使用可以把一个组件挂在到全局当中,可以在任何地方使用这个组件而不用引入
自定义指令


其他
