[TOC]
首先声明:这篇笔记中是在学习禹神最新Vue3教程记下来的,其中有一些自己写的笔记见解、搬禹神、vue、vite官网的一些笔记。主要目的还是给自己多一点知识点的总结。其实一遍下来,基本大部分都会,所以学习会很快。有几点我得好好在下面总结。
1.学习到的新点
2.学习的建议
- 虽然之前通过各个方面资源学习vue,其中主要是通过官网和禹神的vue2视频学习,学习也简单,但是很重要的一点,就是要去多练,多接触,要不然很容易忘的。
- 想学习更多的东西,看官网挺好的,当然听老师讲可能会对一个知识点豁然开朗。
备注:现在官方推荐使用 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
## 创建命令
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
总结:
<script type="module" src="xxxxx"></script>
指向的 JavaScript。## 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**。
setup 是 Vue3 中的一个新配置项,值是一个函数,它是 Composition API “表演的舞台”,组件中所用到的:数据、方法、计算属性、监视……等,均配置在 setup 当中。
特点如下
setup 的返回值
//返回一个对象!!!
return {
name,
age,
tel,
changeName,
changeAge,
showTel,
};
//一个渲染函数
return () => "哈哈";
涉及到的面试问题
官网的 setup 语法糖
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的 <script>
语法,它具有更多优势:
要启用该语法,需要在 <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 lang="ts" setup name="Person456">
...
</script>
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)
})
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
// 情况一:监视【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 定义的对象中的属性,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 }
);
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);
});
注意点:
结论:监视的要是对象里面的属性,那么最好写函数式。注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视!
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);
});
作用:用于注册模板引用
- 用于普通 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>
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>
注意模块化的用法就行了!!!
// 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>
<router-link ></router-link>
<RouterLink></RouterLink>
- 路由组件通常存放在 pages 或者 views 文件夹中,一般组件通常存放在 components 文件夹。
- 通过点击导航,视觉效果上“消失了”的路由组件,默认是被销毁掉的,需要的时候再去挂载。
active-class 是 RouterLinkProps:链接在匹配当前路由时被应用到 class。
<!-- 第一种:to的字符串写法 -->
<RouterLink active-class="active" to="/home">主页</RouterLink>
<!-- 第二种:to的对象写法 -->
<RouterLink active-class="active" :to="{path:"/home"}">主页</RouterLink>
> 优点:URL 更加美观,不带有#,更接近于传统的网站 URL。
> 缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有 404 的错误。
>
> js > const router = createRouter({ > history: createWebHistory(), > }); >
js > const router = createRouter({ > history: createWebHashHistory(), > }); >
传参
<!-- 第一种写法 -->
<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)
路由占位:
{
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参数时,需要提前在规则中占位!
第一种写法:将路由收到的所有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'
}
}
]
}
开启replace模式:
<RouterLink replace >主页</RouterLink>
编程式路由导航
import { useRouter } from "vue-router"
let router=useRouter()
router.push(
//这里和 router-link 中的 to 一个样!!!
)
重定向
//在路由规则中
{
path:'/',
redirect:"/home"
}
Pinia
符合直觉的
Vue.js 状态管理库
安装
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')
先创建一个 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 }
})
我们得知道 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 对象**。
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
在 Setup Store 中:
return { count, increment }
})
## 4.4【使用一个Store】
> 为了从 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)
},
},
})
并在组件中使用:
import { useUserListStore } from ‘./store’
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 getUserById.value
来访问
// 中的函数
## 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 订阅器,以便将其从当前组件中分离:
# 5.组件通信
## 5.1. 【props】
概述:`props`是使用频率最高的一种通信方式,常用与 :**父 ↔ 子**。
- 若 **父传子**:属性值是**非函数**。
- 若 **子传父**:属性值是**函数**。
**子传父就是调用父组件的函数,带参数传过去的!!!**
父组件:
子组件
## 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
首先,原生是这样用的:
其实在vue2/3的背后,上面的这个代码等价于下面这行代码:
<input
:value=“data”
@input=“data = $event.target.value”
/>
其实看到这里,应该有考虑过我还能用emit事件触发吗?不会啦,它只是对于它自己本身进行操作的,只是我们今天遇到的主要问题是输入组件上绑定的v-model,我们怎么通过组件里面的代码进行事件的触发。
### 在一个输入组件上使用v-model
先看组件上:
在背后实际上被展开成:
<CustomInput
:model-value=“searchText”
@update:model-value=“newValue => searchText = newValue”
/>
<CustomInput
:model-value=“searchText”
@update:model-value=“ searchText = $event”
/>
> 看官网文档介绍的重点!!!
要让这个例子实际工作起来,`<CustomInput>` 组件内部需要做两件事:
1. 将内部原生 `<input>` 元素的 `value` attribute 绑定到 `modelValue` prop
2. 当原生的 `input` 事件触发时,触发一个携带了新值的 `update:modelValue` 自定义事
> 再看看官网给的例子,就知道父子组件关于数据传输的真实情况了。实际上在做这题也是这样的,我们在组件中触发父组件中的输入组件的`update:modelValue`事件,顺便将原生输入框的变化值带过去。
> $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 事件更新父组件值:
**多个 v-model 绑定**
利用刚才在 v-model 参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model 双向绑定。
组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:
<UserName
v-model:first-name=“first”
v-model:last-name=“last”
/>
html
defineProps({
firstName: String,
lastName: String
})
defineEmits([‘update:firstName’, ‘update:lastName’])
## 5.4【$attrs 】
1. 概述:`$attrs`用于实现**当前组件的父组件**,向**当前组件的子组件**通信(**祖→孙**)。
2. 具体说明:`$attrs`是一个对象,包含所有父组件传入的标签属性。
> 注意:`$attrs`会自动排除`props`中声明的属性(可以认为声明过的 `props` 被子组件自己“消费”了)
父组件:
子组件:
孙组件:
## 5.5 【\$refs、$parent】
1. 概述:
* `$refs`用于 :**父→子。**
* `$parent`用于:**子→父。**
2. 原理如下:
| 属性 | 说明 |
| --------- | -------------------------------------------------------- |
| `$refs` | 值为对象,包含所有被`ref`属性标识的`DOM`元素或组件实例。 |
| `$parent` | 值为对象,当前组件的父组件实例对象。 |
## 5.6 【provide、inject】
1. 概述:实现**祖孙组件**直接通信
2. 具体使用:
* 在祖先组件中通过`provide`配置向后代组件提供数据
* 在后代组件中通过`inject`配置来声明接收数据
4. 具体编码:
【第一步】父组件中,使用`provide`提供数据
```vue
<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`配置项接受数据。
```vue
<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')
</script>
```
## 5.7 【slot】
### 1. 默认插槽
父组件中:
<li v-for=“g in games” :key=“g.id”>{{ g.name }}
子组件中:
{{ title }}
### 2. 具名插槽
父组件中:
<template v-slot:s1>
<li v-for=“g in games” :key=“g.id”>{{ g.name }}
<template #s2>
更多
子组件中:
{{ title }}
### 3. 作用域插槽
1. 理解:<span style="color:red">数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。</span>(新闻数据在`News`组件中,但使用数据所遍历出来的结构由`App`组件决定)
3. 具体编码:
```html
父组件中:
<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`
1. 作用:创建一个响应式数据,但只对顶层属性进行响应式处理。
2. 用法:
```js
let myVar = shallowRef(initialValue);
```
3. 特点:只跟踪引用值的变化,不关心值内部的属性变化。
### `shallowReactive`
1. 作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的
2. 用法:
```js
const myObj = shallowReactive({ ... });
```
3. 特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。
### 总结
> 通过使用 [`shallowRef()`](https://cn.vuejs.org/api/reactivity-advanced.html#shallowref) 和 [`shallowReactive()`](https://cn.vuejs.org/api/reactivity-advanced.html#shallowreactive) 来绕开深度响应。浅层式 `API` 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
## 6.2【readonly 与 shallowReadonly】
### **`readonly`**
1. 作用:用于创建一个对象的深只读副本。
2. 用法:
```js
const original = reactive({ ... });
const readOnlyCopy = readonly(original);
```
3. 特点:
* 对象的所有嵌套属性都将变为只读。
* 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
4. 应用场景:
* 创建不可变的状态快照。
* 保护全局状态或配置不被修改。
### **`shallowReadonly`**
1. 作用:与 `readonly` 类似,但只作用于对象的顶层属性。
2. 用法:
```js
const original = reactive({ ... });
const shallowReadOnlyCopy = shallowReadonly(original);
```
3. 特点:
* 只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
* 适用于只需保护对象顶层属性的场景。
## 6.3【toRaw 与 markRaw】
### `toRaw`
1. 作用:用于获取一个响应式对象的原始对象, `toRaw` 返回的对象不再是响应式的,不会触发视图更新。
> 官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
> 何时使用? —— 在需要将响应式对象传递给非 `Vue` 的库或外部系统时,使用 `toRaw` 可以确保它们收到的是普通对象
2. 具体编码:
```js
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`
1. 作用:标记一个对象,使其**永远不会**变成响应式的。
> 例如使用`mockjs`时,为了防止误把`mockjs`变为响应式对象,可以使用 `markRaw` 去标记`mockjs`
2. 编码:
```js
/* 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】
> 这里对于track,trigger可能会考面试题!!!
作用:创建一个自定义的`ref`,并对其依赖项跟踪和更新触发进行逻辑控制。
实现防抖效果(`useSumRef.ts`):
import {customRef } from “vue”;
export default function(initValue:string,delay:number){
let msg = customRef((track,trigger)=>{
let timer:number
return {
//get什么时候调用?——msg被读取时
get(){
track() // 告诉Vue数据msg很重要,要对msg持续关注,一旦变化就更新
return initValue
},
//set什么时候调用?——msg被修改时
set(value){
clearTimeout(timer)
timer = setTimeout(() => {
initValue = value
trigger() //通知Vue数据msg变化了
}, delay);
}
}
})
return {msg}
}
# 7.Vue3新组件
## 7.1 【Teleport】
- 什么是Teleport?—— Teleport 是一种能够将我们的**组件html结构**移动到指定位置的技术。
- 下面这个to中的值是选择器属性值。
我是一个弹窗
我是弹窗中的一些内容
<button @click=“isShow = false”>关闭弹窗
## 7.2 【Suspense】
- 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
- 使用步骤:
- 异步引入组件
- 使用`Suspense`包裹组件,并配置好`default` 与 `fallback`
import { defineAsyncComponent,Suspense } from “vue”;
const Child = defineAsyncComponent(()=>import(‘./Child.vue’))
```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>
app.component
app.config
app.directive
app.mount
app.unmount
app.use
过渡类名 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
。
……