2024年Vue 3学习笔记:Vu3基础的完整指南
这篇笔记中是在学习禹神最新Vue3教程记下来的,其中有一些自己写的笔记见解、搬禹神、vue、vite官网的一些笔记。主要目的还是给自己多一点知识点的总结。其实一遍下来,基本大部分都会,所以学习会很快。有几点我得好好在下面总结。
开篇总结
首先声明:这篇笔记中是在学习禹神最新Vue3教程记下来的,其中有一些自己写的笔记见解、搬禹神、vue、vite官网的一些笔记。主要目的还是给自己多一点知识点的总结。其实一遍下来,基本大部分都会,所以学习会很快。有几点我得好好在下面总结。
1.学习到的新点
- props和provide传值可以通过传函数给下面的组件,组件调用后传参可以更改父元素。
- 一个reactive响应式对象中包含有ref类型数据,通过reactive可以直接获取数据,有解包的过程
- 新了解的mitt进行组件间的通信
- 禹神yyds,作用域插槽每次看到都会有不一样的见解,但是不用容易忘,这次讲的例子很让人记忆深刻
2.学习的建议
- 虽然之前通过各个方面资源学习vue,其中主要是通过官网和禹神的vue2视频学习,学习也简单,但是很重要的一点,就是要去多练,多接触,要不然很容易忘的。
- 想学习更多的东西,看官网挺好的,当然听老师讲可能会对一个知识点豁然开朗。
1.创建 Vue3 工程
1.1 【基于 vue-cli 创建】
备注:现在官方推荐使用 create-vue 来创建基于 Vite 的新项目。
## 查看 @vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
vue -V
## 安装或 升级你的 @vue/cli
npm install -g @vue/cli
## 执行创建命令
vue create vue_test
## 随后选择 3.x
> Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
## 下面基于webpack搭建项目成功后执行下面的命令就是成功创建Vue3项目!!!
? Please pick a preset: Default ([Vue 3] babel, eslint)
Vue CLI v5.0.8
✨ Creating project in F:\桌面\vue_test.
🗃 Initializing git repository...
⚙️ Installing CLI plugins. This might take a while...
added 864 packages in 35s
🚀 Invoking generators...
📦 Installing additional dependencies...
added 103 packages in 7s
⚓ Running completion hooks...
📄 Generating README.md...
🎉 Successfully created project vue_test.
👉 Get started with the following commands:
$ cd vue_test
$ npm run serve
1.2 【基于 Vite 创建】(Vue 官方推荐)
## 创建命令
npm create vue@latest
## 一些可选项
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
Scaffolding project in ./<your-project-name>...
Done.
## 在项目被创建后,通过以下步骤安装依赖并启动开发服务器
> cd <your-project-name>
> npm install
> npm run dev
总结:
- Vite 项目中,index.html 是项目的入口文件,在项目的最外层。
- 加载 index.html 后,Vite 解析
<script type="module" src="xxxxx"></script>
指向的 JavaScript。 - Vue3 中是通过 createApp 函数创建一个实例应用的。
1.3 【基于 Vite 官方 create-vite 创建】
## vite构建vue项目
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue
# yarn
yarn create vite my-vue-app --template vue
# pnpm
pnpm create vite my-vue-app --template vue
# bun
bunx create-vite my-vue-app --template vue
查看 create-vite 以获取每个模板的更多细节:vanilla,vanilla-ts, vue, vue-ts,react,react-ts,react-swc,react-swc-ts,preact,preact-ts,lit,lit-ts,svelte,svelte-ts,solid,solid-ts,qwik,qwik-ts。
2.Vue3 语法核心
2.1 【拉开序幕的 setup】
setup 是 Vue3 中的一个新配置项,值是一个函数,它是 Composition API “表演的舞台”,组件中所用到的:数据、方法、计算属性、监视……等,均配置在 setup 当中。
特点如下
- setup 函数返回的对象中的内容,可直接在模板中使用。
- setup 中访问的 this 是 undefined。
- setup 函数会在 beforeCreate 之前调用,它是“领先”所有钩子执行的。
setup 的返回值
//返回一个对象!!!
return {
name,
age,
tel,
changeName,
changeAge,
showTel,
};
//一个渲染函数
return () => "哈哈";
涉及到的面试问题
-
(data、methods……) 和 setup 能不能同时存在?
能,可以在一个 Vue 3 组件中同时使用 Options API 和 Composition API。但是,它们的使用场景是不同的。一般来说,如果你想要定义一些基础的响应式数据和钩子函数,使用 Options API 就足够了。而当你需要更复杂的逻辑或希望更好地组织你的代码时,Composition API 会是一个更好的选择。 -
setup 中定义的数据,在 data 中能否获得?
能,setup 这个钩子执行的时期比 data 早,当 data 中数据解析完毕时,setup 中数据早已经解析完毕了!!!但是注意的是 setup 不能再去读取 data 中的数据了
官网的 setup 语法糖
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的<script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
要启用该语法,需要在 <script>
代码块上添加 setup attribute:
<script setup>console.log('hello script setup')</script>
里面的代码会被编译成组件 setup() 函数的内容。这意味着与普通的 <script>
只在组件被首次引入的时候执行一次不同,<script setup>
中的代码会在每次组件实例被创建的时候执行。
Vue3 项目注意点
Vue3 中可能会遇到两个 script 标签,一个是用来配置组件名称的,另一个是用来配置 setup 的。
<script lang="ts">
export default {
name:"Person123"
}
</script>
<script lang="ts" setup>
...
</script>
和上面这样写可能有点麻烦了!我们可以进行如下配置:
- 我们可以借助一个插件
npm i vite-plugin-vue-setup-extend -D
- 在
vite.config.ts
中进行配置
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import VueSetupExtend from "vite-plugin-vue-setup-extend";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), VueSetupExtend()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
- 在 script 标签上使用
<script lang="ts" setup name="Person456">
...
</script>
2.2 ref 和 reactive 的学习
ref ==============> 可以定义:基本类型、对象类型的响应式数据
reactive ===========> 只能定义:对象类型是响应式数据
ref 对比 reactive
宏观角度:
- ref 用来定义:基本数据类型、对象数据类型
- reactive 用来定义:对象数据类型
区别:
- ref 创建的变量必须使用 .value (可以使用 volar 插件自动添加 .value)
- reactive 重新分配一个新对象,会失去响应式 (可以使用 Object.assign 去整体替换)
使用原则:
- 若需要一个基本类型的响应式数据,必须使用 ref
- 若需要一个响应式对象,层级不深,ref、reactive 都可以
- 若需要一个响应式对象,且层级较深,推荐使用 reactive
reactive中一个注意点:(自动解包)
//当访问 obj.c 的时候,底层会自动读取value属性,因为c是在obj这个响应式对象里!!!
let obj=reactive({
a:1,
b:2,
c:ref(3)
})
2.3 toRefs、toRef、computed
toRefs 和 toRef 的作用
将一个响应式对象的每个属性,转换成为一个 ref 对象。两个基本相似,但是 toRefs 可以批量转换!
import {reactive,toRefs,toRef} from "vue"
//数据
let person=reactive({
name:"xiaoyu",
age:18
})
// let {name,age}=person;
// console.log(name,age);//产生出普通的值!
let {name,age}=toRefs(person);
console.log(name,age);
let name2=toRef(person,"name");
console.log(name2);
//像如下这种
ObjectRefImpl {_object: Proxy(Object), _key: 'name', _defaultValue: undefined, __v_isRef: true}__v_isRef: true_defaultValue: undefined_key: "name"_object: Proxy(Object) {name: 'xiaoyu', age: 18}dep: (...)value: (...)[[Prototype]]: Object
2.4 【watch】
- 作用:监视数据的变化(和 Vue2 中的 watch 一致)
- 特点:Vue3 的 watch 只能监视以下四种数据:
- ref 定义的数据
- reactive 定义的数据
- 函数返回一个值(getter 函数)
- 一个包含上述内容的数组
*情况一:监视【ref】定义的 【基本类型】数据
// 情况一:监视【ref】定义的 【基本类型】数据
let sum = ref(0);
const stopWatch = watch(sum, (newVal, oldVal) => {
console.log(newVal, oldVal);
//这个watch还返回一个函数,调用它可以解除监视!!!
if (newVal > 10) {
stopWatch();
}
});
console.log(stopWatch); //返回一个函数
/*
() => {
effect2.stop();
if (instance && instance.scope) {
remove(instance.scope.effects, effect2);
}
}
*/
*情况二:监视【ref】定义的 【对象类型】数据
监视【ref】定义的 【对象类型】数据:直接写数据名,监视的是对象的地址值,若想监视对象内部的属性变化,需要手动开启深度监视。
注意
- 若修改的是 ref 定义的对象中的属性,newVal 和 oldVal 都是新值,因为他们是同一个对象
- 若修改整个 ref 定义的对象,newVal 是新值,oldVal 是旧值,因为不是同一个对象了。
let person = ref({
name: "张三",
age: 18,
});
const changeName = () => {
person.value.name += "~";
};
const changeAge = () => {
person.value.age += 1;
};
const changePerson = () => {
person.value = { name: "李明", age: 20 };
};
//监视【ref】定义的 【对象类型】数据,监视的是对象的地址值,
// 若想监视对象内部的属性变化,需要手动开启深度监视。
watch(
person,
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: true }
);
*情况三:监视 【reactive】定义的【对象类型】数据,且默认开启了深度监视
let p = reactive({
name: "小雨",
age: 20,
});
const pName = () => {
p.name += "~";
};
const pAge = () => {
p.age += 1;
};
const pPerson = () => {
//这里不是真正意义上的修改整个人,而是进行了批量的修改!!!
Object.assign(p, { name: "小柔", age: 19 });
};
//监视【reactive】定义的 【对象类型】数据,默认开启了深度监视!!!
watch(p, (newVal, oldVal) => {
console.log(newVal, oldVal);
});
*情况四:监视 ref 或 reactive 定义的【对象类型】数据中的 某个属性
注意点:
- 若该属性值不是【对象类型】,需要写成函数形式
- 若该属性值依然是【对象类型】,可以直接编,也可以写成函数,还是建议写成函数!
结论:监视的要是对象里面的属性,那么最好写函数式。注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视!
let p2 = reactive({
name: "xiaoyu",
age: 18,
car: {
c1: "奔驰",
c2: "宝马",
},
});
const p2Car = () => {
p2.car = { c1: "大奔驰", c2: "大宝马" };
};
//情况四:监视响应式对象中的某个属性,且该属性是基本数据类型,要写成函数式。
watch(
() => p2.name,
(newVal, oldVal) => {
console.log(newVal, oldVal);
}
);
//下面这个情况就很诡异,能监视到对象里面的值变化,对象本身却不进行监视!
// watch(p2.car,(newVal,oldVal)=>{
// console.log(newVal,oldVal);
// })
//建议写成监视其中的属性任然是一个对象类型的,也推荐使用函数形式的!
watch(
() => p2.car,
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: true }
);
*情况五:监视多个数据
// 情况五:监视多个数据:
watch([() => p2.name, () => p2.car.c1], (newVal, oldVal) => {
console.log(newVal, oldVal);
});
2.5【watchEffect】
- 官网:立即运行一个函数,同时响应式的跟踪器依赖,并在依赖更新时重新执行该函数。
- watch 对比 watchEffect
- 都能监视响应式数据的变化,不同的是监听数据变化的方式不同
- watch:需要指明出监视的数据
- watchEffect:不用明确指出监听的数据(函数中用到了那些属性,那就是监视那些属性!)
2.6【标签的 ref 属性】
作用:用于注册模板引用
- 用于普通 DOM 标签上,获取的是 DOM 节点
- 用于组件标签上,获取的是组件实例对象
这里有个说法:在获取组件实例实例身上的东西时,vue3 不允许直接获取,要求获取组件的本身主动暴露出来属性才行!!!如 defineExpose({a,b,c})
对于子组件 Person 中如下:
<template>
<div class="person">
<h1>中国</h1>
<h2 ref="title">北京</h2>
<button @click="showLog">点击输出h2这个元素</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,defineExpose} from "vue"
let title=ref()
let a=ref(0)
let b=ref(1)
let c=ref(2)
const showLog=()=>{
console.log(title.value);//获取的是DOM
}
defineExpose({a,b,c})
</script>
父组件中如下:
<script setup lang="ts" name="App">
import Person from './components/Person.vue';
import {ref} from "vue"
let title=ref()
let ren=ref()
const showLog=()=>{
console.log(title.value);//获取DOM
console.log(ren.value);//组价实例
}
</script>
<template>
<h2 ref="title">你好</h2>
<button @click="showLog">点我输出h2</button>
<Person ref="ren"/>
</template>
2.7 props
App.vue
<script setup lang="ts" name="App">
import Person from "./components/Person.vue";
import type { PersonInter, Persons } from "@/types/index";
import { reactive, ref } from "vue";
let person: PersonInter = { id: "sgbusjdbsj", name: "xiaoyu", age: 20 };
let personList = reactive<Persons>([
{ id: "sgbusjdbsjas", name: "xiaoyu", age: 18 },
{ id: "sgbusjdbsjsf", name: "xiaohh", age: 20 },
{ id: "sgbusjdbsjdf", name: "xiaogege", age: 10 },
]);
</script>
<template>
<Person a="哈哈" b="嘿嘿" :list="personList" />
</template>
Person.vue
<template>
<div class="person">
<h2>{{ list }}</h2>
</div>
</template>
<script lang="ts" setup name="Person">
//其实这里的 宏函数 无需引入就能进行使用!!!
import { defineProps, withDefaults } from "vue";
import type { Persons } from "@/types/index";
//接收props,顺便使用!
// let x=defineProps(['a','b',"list"])
// console.log(x);
//接收list + 限制类型
// defineProps<{list:Persons}>()
//接收list + 类型限制 + 限制必要性 + 指定默认值
withDefaults(defineProps<{ list?: Persons }>(), {
list: () => [{ id: "1", name: "xiao", age: 19 }],
});
</script>
2.8 Hooks
注意模块化的用法就行了!!!
// mouse.ts
import { ref, onMounted, onUnmounted } from "vue";
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0);
const y = ref(0);
// 组合式函数可以随时更改其状态。
function update(event: any) {
x.value = event.pageX;
y.value = event.pageY;
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
// 通过返回值暴露所管理的状态
return { x, y };
}
<template>
<div class="person">Mouse position is at: {{ x }}, {{ y }}</div>
</template>
<script lang="ts" setup name="Person">
import { useMouse } from "@/hooks/mouse";
const { x, y } = useMouse();
</script>
3.路由
<router-link ></router-link>
<RouterLink></RouterLink>
3.1 两个注意点
- 路由组件通常存放在 pages 或者 views 文件夹中,一般组件通常存放在 components 文件夹。
- 通过点击导航,视觉效果上“消失了”的路由组件,默认是被销毁掉的,需要的时候再去挂载。
3.2 RouterLink 中 to 的两种写法
active-class 是 RouterLinkProps:链接在匹配当前路由时被应用到 class。
<!-- 第一种:to的字符串写法 -->
<RouterLink active-class="active" to="/home">主页</RouterLink>
<!-- 第二种:to的对象写法 -->
<RouterLink active-class="active" :to="{path:"/home"}">主页</RouterLink>
3.3 路由器的工作模式
-
history 模式
优点:URL 更加美观,不带有#,更接近于传统的网站 URL。
缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有 404 的错误。const router = createRouter({ history: createWebHistory(), });
-
hash 模式(后台管理系统喜欢用!!!)
优点:兼容性更好,因为不需要服务端处理路径。
缺点:URl 带有#不太美观,且在 SEO 优化方面相对较差。const router = createRouter({ history: createWebHashHistory(), });
3.4 路由传参
query 传参
传参
<!-- 第一种写法 -->
<RouterLink :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">主页</RouterLink>
<!-- 第二种写法 -->
<!-- 用 name 配合 query 也可以进行跳转 !!!-->
<RouterLink :to="{path:"/news/detail",query:{id:news.id,title:news.title,content:news.content}}">主页</RouterLink>
接收参数
import {useRoute} from "vue-router"
const route=useRoute()
//打印query参数
console.log(route.query)
parmas 传参
路由占位:
{
name:'xinwen',
path:'/news',
component:Detail,
children:[
{
name:'xiangqing',
path:'detail/:id/:title/:content?',
//在占位后面加一个 ? 表示参数可传可不传
component:Detail
}
]
}
路由传参:
<!-- 第一种写法 -->
<RouterLink :to="`/news/detail/${news.id}/${news.title}/${news.content}`">主页</RouterLink>
<!-- 第二种写法 -->
<!-- 注意:使用params传参 只能配合name进行,不要是path -->
<RouterLink :to="{name:"xiangqing",params:{id:news.id,title:news.title,content:news.content}}">主页</RouterLink>
<!-- 还有params是不能传数组的!一个不被推荐的数据类型 -->
注意:
- 传递params参数时,若使用to的对象写法,必须使用name配置项,不能使用path。
- 传递params参数时,需要提前在规则中占位!
3.5 路由的Props配置
第一种写法:将路由收到的所有params参数作为props传递给组件。
{
name:'xinwen',
path:'/news',
component:Detail,
children:[
{
name:'xiangqing',
path:'detail/:id/:title/:content?',//在占位后面加一个 ? 表示参数可传可不传
component:Detail,
//第一种写法:将路由收到的所有params参数作为props传递给组件。
props:true
}
]
}
第二种写法:可以自己决定将什么作为props传给路由组件
{
name:'xinwen',
path:'/news',
component:Detail,
children:[
{
name:'xiangqing',
path:'detail',
component:Detail,
// 第二种写法:可以自己决定将什么作为props传给路由组件
props(route){
return route.query
}
//一种固定值的写法,用到的比较少!!!也是将参数传给组件身上。
props:{
a:'1',
b:'2'
}
}
]
}
3.6 【replace属性】
- 作用:控制路由跳转时操作浏览器历史记录的模式
- 浏览器的历史记录有两种写入方式:分别为push、replace:
- push 是追加历史记录(默认)
- replace 是替换当前记录
- 开启replace模式:
<RouterLink replace >主页</RouterLink>
3.7 【编程式路由导航 和 重定向】
编程式路由导航
import { useRouter } from "vue-router"
let router=useRouter()
router.push(
//这里和 router-link 中的 to 一个样!!!
)
重定向
//在路由规则中
{
path:'/',
redirect:"/home"
}
4.【pinia】(建议看官网)
Pinia
符合直觉的
Vue.js 状态管理库
4.1 在Vue3项目中引入pinia
安装
yarn add pinia
# 或者使用 npm
npm install pinia
在 main.ts中引入
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
4.2 官网的一个基础示例
先创建一个 Store(选项式)
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// 也可以这样定义
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
在一个组件中使用该 store
<script setup>
import { useCounterStore } from '@/stores/counter'
//这下面三种方式用来更改状态!!!
const counter = useCounterStore()
//常规更改!
counter.count++
// 自动补全! ✨
counter.$patch({ count: counter.count + 1 })
// 或使用 action 代替
counter.increment()
</script>
<template>
<!-- 直接从 store 中访问 state -->
<div>Current Count: {{ counter.count }}</div>
</template>
一个特殊的定义Store的方式(组合式)
为实现更多高级用法,你甚至可以使用一个函数 (与组件 setup() 类似) 来定义一个 Store
// stores/counter.js
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
4.3 【定义一个Store】
我们得知道 Store 是用 defineStore() 定义的,它的第一个参数要求是一个独一无二的名字:
import { defineStore } from 'pinia'
// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAlertsStore = defineStore('alerts', {
// 其他配置...
})
这个名字 ,也被用作 id ,是必须传入的, Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use… 是一个符合组合式函数风格的约定。
defineStore() 的第二个参数可接受两类值:Setup 函数或 Option 对象。
【Option Store】
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
【Setup Store】
在 Setup Store 中:
- ref() 就是 state 属性
- computed() 就是 getters
- function() 就是 actions
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
4.4【使用一个Store】
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
作为 action 的 increment 可以直接解构,getter 和 state 需要使用storeToRefs()
4.5 【State】
重置 state
使用选项式API 时,你可以通过调用 store 的 $reset() 方法将 state 重置为初始值。
在 $reset() 内部,会调用 state() 函数来创建一个新的状态对象,并用它替换当前状态。
const store = useStore()
store.$reset()
在 Setup Stores 中,您需要创建自己的 $reset() 方法:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function $reset() {
count.value = 0
}
return { count, $reset }
})
变更 state
除了用 store.count++ 直接改变 store,你还可以调用 $patch 方法。它允许你用一个 state 的补丁对象在同一时间更改多个属性:
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})
不过,用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice 操作)都需要你创建一个新的集合。因此,$patch 方法也接受一个函数来组合这种难以用补丁对象实现的变更。
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
订阅 state
通过 store 的 $subscribe() 方法侦听 state 及其变化
const cartStore = useSomeStore()
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// 和 cartStore.$id 一样
mutation.storeId // 'cart'
// 只有 mutation.type === 'patch object'的情况下才可用
mutation.payload // 传递给 cartStore.$patch() 的补丁对象。
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('cart', JSON.stringify(state))
},{ detached: true })
默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离
可以使用watch监听整个 state
watch(
pinia.state,
(state) => {
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
4.6 【Getter】
getter 拿什么来计算?
- getter 基于 state参数
- 也可以通过 this 访问到整个 store 实例,这样可以访问到其他的getter
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注 ✨
return this.doubleCount + 1
},
},
})
向 getter 传递参数
Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
export const useStore = defineStore('main', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})
并在组件中使用:
<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
// <script setup> 中的函数
</script>
<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>
4.7【Action】
订阅 action
通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行
这里有一个例子,在运行 action 之前以及 action resolve/reject 之后打印日志记录。
const unsubscribe = someStore.$onAction(
({
name, // action 名称
store, // store 实例,类似 `someStore`
args, // 传递给 action 的参数数组
after, // 在 action 返回或解决后的钩子
onError, // action 抛出或拒绝的钩子
}) => {
// 为这个特定的 action 调用提供一个共享变量
const startTime = Date.now()
// 这将在执行 "store "的 action 之前触发。
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// 这将在 action 成功并完全运行后触发。
// 它等待着任何返回的 promise
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
// 如果 action 抛出或返回一个拒绝的 promise,这将触发
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// 手动删除监听器
unsubscribe()
继续搬砖
默认情况下,action 订阅器会被绑定到添加它们的组件上(如果 store 在组件的 setup() 内)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 true 作为第二个参数传递给 action 订阅器,以便将其从当前组件中分离:
<script setup>
const someStore = useSomeStore()
// 此订阅器即便在组件卸载之后仍会被保留
someStore.$onAction(callback, true)
</script>
5.组件通信
5.1. 【props】
概述:props
是使用频率最高的一种通信方式,常用与 :父 ↔ 子。
- 若 父传子:属性值是非函数。
- 若 子传父:属性值是函数。
子传父就是调用父组件的函数,带参数传过去的!!!
父组件:
<template>
<div class="father">
<h3>父组件,</h3>
<h4>我的车:{{ car }}</h4>
<h4>儿子给的玩具:{{ toy }}</h4>
<Child :car="car" :getToy="getToy"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";
// 数据
const car = ref('奔驰')
const toy = ref()
// 方法
function getToy(value:string){
toy.value = value
}
</script>
子组件
<template>
<div class="child">
<h3>子组件</h3>
<h4>我的玩具:{{ toy }}</h4>
<h4>父给我的车:{{ car }}</h4>
<button @click="getToy(toy)">玩具给父亲</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
const toy = ref('奥特曼')
defineProps(['car','getToy'])
</script>
5.2 【mitt】
概述:与消息订阅与发布(pubsub
)功能类似,可以实现任意组件间通信。
安装mitt
npm i mitt
新建文件:src\utils\emitter.ts
// 引入mitt
import mitt from "mitt";
// 创建emitter
const emitter = mitt()
/*
// 绑定事件
emitter.on('abc',(value)=>{
console.log('abc事件被触发',value)
})
emitter.on('xyz',(value)=>{
console.log('xyz事件被触发',value)
})
setInterval(() => {
// 触发事件
emitter.emit('abc',666)
emitter.emit('xyz',777)
}, 1000);
setTimeout(() => {
// 清理事件
emitter.all.clear()
}, 3000);
*/
// 创建并暴露mitt
export default emitter
接收数据的组件中:绑定事件、同时在销毁前解绑事件:
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";
// 绑定事件
emitter.on('send-toy',(value)=>{
console.log('send-toy事件被触发',value)
})
onUnmounted(()=>{
// 解绑事件
emitter.off('send-toy')
})
【第三步】:提供数据的组件,在合适的时候触发事件
import emitter from "@/utils/emitter";
function sendToy(){
// 触发事件
emitter.emit('send-toy',toy.value)
}
注意这个重要的内置关系,总线依赖着这个内置关系
5.3 【v-model】
原生input上使用v-model
首先,原生是这样用的:
<input v-model="data" />
其实在vue2/3的背后,上面的这个代码等价于下面这行代码:
<input
:value="data"
@input="data = $event.target.value"
/>
其实看到这里,应该有考虑过我还能用emit事件触发吗?不会啦,它只是对于它自己本身进行操作的,只是我们今天遇到的主要问题是输入组件上绑定的v-model,我们怎么通过组件里面的代码进行事件的触发。
在一个输入组件上使用v-model
先看组件上:
<CustomInput v-model="searchText" />
在背后实际上被展开成:
<CustomInput
:model-value="searchText"
@update:model-value="newValue => searchText = newValue"
/>
<!-- 或者: -->
<CustomInput
:model-value="searchText"
@update:model-value=" searchText = $event"
/>
看官网文档介绍的重点!!!
要让这个例子实际工作起来,<CustomInput>
组件内部需要做两件事:
- 将内部原生
<input>
元素的value
attribute 绑定到modelValue
prop - 当原生的
input
事件触发时,触发一个携带了新值的update:modelValue
自定义事
再看看官网给的例子,就知道父子组件关于数据传输的真实情况了。实际上在做这题也是这样的,我们在组件中触发父组件中的输入组件的
update:modelValue
事件,顺便将原生输入框的变化值带过去。
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
$event 到底是啥?啥时候能.target
对于原生事件,$event就是事件对象===>能.target
对于自定义事件,$event就是触发事件时,传递的数据 ===> 不能.target
下面又是搬砖!!!
v-model 的参数
默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。我们可以通过给 v-model 指定一个参数来更改这些名字:
<MyComponent v-model:title="bookTitle" />
在这个例子中,子组件应声明一个 title prop,并通过触发 update:title 事件更新父组件值:
<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
多个 v-model 绑定
利用刚才在 v-model 参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model 双向绑定。
组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
5.4【$attrs 】
-
概述:
$attrs
用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。 -
具体说明:
$attrs
是一个对象,包含所有父组件传入的标签属性。注意:
$attrs
会自动排除props
中声明的属性(可以认为声明过的props
被子组件自己“消费”了)
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<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){
a.value = value
}
</script>
子组件:
<template>
<div class="child">
<h3>子组件</h3>
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>
孙组件:
<template>
<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(666)">点我更新A</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a','b','c','d','x','y','updateA'])
</script>
5.5 【$refs、$parent】
-
概述:
$refs
用于 :父→子。$parent
用于:子→父。
-
原理如下:
属性 说明 $refs
值为对象,包含所有被 ref
属性标识的DOM
元素或组件实例。$parent
值为对象,当前组件的父组件实例对象。
5.6 【provide、inject】
-
概述:实现祖孙组件直接通信
-
具体使用:
- 在祖先组件中通过
provide
配置向后代组件提供数据 - 在后代组件中通过
inject
配置来声明接收数据
- 在祖先组件中通过
-
具体编码:
【第一步】父组件中,使用
provide
提供数据<template> <div class="father"> <h3>父组件</h3> <h4>资产:{{ money }}</h4> <h4>汽车:{{ car }}</h4> <button @click="money += 1">资产+1</button> <button @click="car.price += 1">汽车价格+1</button> <Child/> </div> </template> <script setup lang="ts" name="Father"> import Child from './Child.vue' import { ref,reactive,provide } from "vue"; // 数据 let money = ref(100) let car = reactive({ brand:'奔驰', price:100 }) // 用于更新money的方法 function updateMoney(value:number){ money.value += value } // 提供数据 provide('moneyContext',{money,updateMoney}) provide('car',car) </script>
注意:子组件中不用编写任何东西,是不受到任何打扰的
【第二步】孙组件中使用
inject
配置项接受数据。<template> <div class="grand-child"> <h3>我是孙组件</h3> <h4>资产:{{ money }}</h4> <h4>汽车:{{ car }}</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:(x:number)=>{}}) let car = inject('car')
5.7 【slot】
1. 默认插槽
父组件中:
<Category title="今日热门游戏">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Category>
子组件中:
<template>
<div class="item">
<h3>{{ title }}</h3>
<!-- 默认插槽 -->
<slot></slot>
</div>
</template>
2. 具名插槽
父组件中:
<Category title="今日热门游戏">
<template v-slot:s1><!-- 也可以是 #s1 -->
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
<template #s2>
<a href="">更多</a>
</template>
</Category>
子组件中:
<template>
<div class="item">
<h3>{{ title }}</h3>
<slot name="s1"></slot>
<slot name="s2"></slot>
</div>
</template>
3. 作用域插槽
-
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在
News
组件中,但使用数据所遍历出来的结构由App
组件决定) -
具体编码:
父组件中: <Game v-slot="params"> <!-- <Game v-slot:default="params"> --> <!-- <Game #default="params"> --> <ul> <li v-for="g in params.games" :key="g.id">{{ g.name }}</li> </ul> </Game> 子组件中: <template> <div class="category"> <h2>今日游戏榜单</h2> <slot :games="games" a="哈哈"></slot> </div> </template> <script setup lang="ts" name="Category"> import {reactive} from 'vue' let games = reactive([ {id:'asgdytsa01',name:'英雄联盟'}, {id:'asgdytsa02',name:'王者荣耀'}, {id:'asgdytsa03',name:'红色警戒'}, {id:'asgdytsa04',name:'斗罗大陆'} ]) </script>
6.其它 API
6.1【shallowRef 与 shallowReactive 】
shallowRef
-
作用:创建一个响应式数据,但只对顶层属性进行响应式处理。
-
用法:
let myVar = shallowRef(initialValue);
-
特点:只跟踪引用值的变化,不关心值内部的属性变化。
shallowReactive
-
作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的
-
用法:
const myObj = shallowReactive({ ... });
-
特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。
总结
通过使用
shallowRef()
和shallowReactive()
来绕开深度响应。浅层式API
创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
6.2【readonly 与 shallowReadonly】
readonly
-
作用:用于创建一个对象的深只读副本。
-
用法:
const original = reactive({ ... }); const readOnlyCopy = readonly(original);
-
特点:
- 对象的所有嵌套属性都将变为只读。
- 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
-
应用场景:
- 创建不可变的状态快照。
- 保护全局状态或配置不被修改。
shallowReadonly
-
作用:与
readonly
类似,但只作用于对象的顶层属性。 -
用法:
const original = reactive({ ... }); const shallowReadOnlyCopy = shallowReadonly(original);
-
特点:
-
只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
-
适用于只需保护对象顶层属性的场景。
-
6.3【toRaw 与 markRaw】
toRaw
-
作用:用于获取一个响应式对象的原始对象,
toRaw
返回的对象不再是响应式的,不会触发视图更新。官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
何时使用? —— 在需要将响应式对象传递给非
Vue
的库或外部系统时,使用toRaw
可以确保它们收到的是普通对象 -
具体编码:
import { reactive,toRaw,markRaw,isReactive } from "vue"; /* toRaw */ // 响应式对象 let person = reactive({name:'tony',age:18}) // 原始对象 let rawPerson = toRaw(person) /* markRaw */ let citysd = markRaw([ {id:'asdda01',name:'北京'}, {id:'asdda02',name:'上海'}, {id:'asdda03',name:'天津'}, {id:'asdda04',name:'重庆'} ]) // 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了 let citys2 = reactive(citys) console.log(isReactive(person)) console.log(isReactive(rawPerson)) console.log(isReactive(citys)) console.log(isReactive(citys2))
markRaw
-
作用:标记一个对象,使其永远不会变成响应式的。
例如使用
mockjs
时,为了防止误把mockjs
变为响应式对象,可以使用markRaw
去标记mockjs
-
编码:
/* markRaw */ let citys = markRaw([ {id:'asdda01',name:'北京'}, {id:'asdda02',name:'上海'}, {id:'asdda03',name:'天津'}, {id:'asdda04',name:'重庆'} ]) // 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了 let citys2 = reactive(citys)
6.4【customRef】
作用:创建一个自定义的ref
,并对其依赖项跟踪和更新触发进行逻辑控制。
实现防抖效果(useSumRef.ts
):
import {customRef } from "vue";
export default function(initValue:string,delay:number){
let msg = customRef((track,trigger)=>{
let timer:number
return {
get(){
track() // 告诉Vue数据msg很重要,要对msg持续关注,一旦变化就更新
return initValue
},
set(value){
clearTimeout(timer)
timer = setTimeout(() => {
initValue = value
trigger() //通知Vue数据msg变化了
}, delay);
}
}
})
return {msg}
}
7.Vue3新组件
7.1 【Teleport】
- 什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。
<teleport to='body' >
<div class="modal" v-show="isShow">
<h2>我是一个弹窗</h2>
<p>我是弹窗中的一些内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
7.2 【Suspense】
- 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
- 使用步骤:
- 异步引入组件
- 使用
Suspense
包裹组件,并配置好default
与fallback
import { defineAsyncComponent,Suspense } from "vue";
const Child = defineAsyncComponent(()=>import('./Child.vue'))
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中.......</h3>
</template>
</Suspense>
</div>
</template>
7.3【全局API转移到应用对象】
app.component
app.config
app.directive
app.mount
app.unmount
app.use
7.4【其他】
-
过渡类名
v-enter
修改为v-enter-from
、过渡类名v-leave
修改为v-leave-from
。 -
keyCode
作为v-on
修饰符的支持。 -
v-model
指令在组件上的使用已经被重新设计,替换掉了v-bind.sync。
-
v-if
和v-for
在同一个元素身上使用时的优先级发生了变化。 -
移除了
$on
、$off
和$once
实例方法。 -
移除了过滤器
filter
。 -
移除了
$children
实例propert
。…
更多推荐
所有评论(0)