Vue3学习笔记

一、Vue3 和 Vue2 的区别:
  • (1)vue2 的<template>标签内只允许存在一个根节点,vue3 允许出现多个根节点
  • (2)vue3 组件引入只需要 import,不需要再像 vue2 上进行 component 注册
  • (3)vue3 的结构是<script><template><style>,vue2 的结构是<template><script><style>,但 vue3 能够兼容 vue2 的结构
  • (4)vue3 的数据和方法定义都在 setup(){}里进行
二、ref 和 reactive

定义变量的两种方式:ref 和 reactive,直接将赋值包括在 ref/reactive 的函数括号内:

  • ref:适用于定义普通类型的数据,number、string
  • reactive:适用于定义引用类型的数据,object、Array
let num = ref(“123”);   let obj=reactive({price:”123”});
console.log(num.value);
console.log(obj);

ref 也可以包括引用数据类型的数据,想使用 ref 所包括的数据,需要指向 ref 包括的变量的 value 属性:

let num = ref({ price: "123" });
console.log(num.value.price);
let nums = ref({ price: "123" }, { price: "456" });
console.log(num.value[0].price);

实际原理其实是 ref 底层去调用了 reactive 实现的

被 ref 修饰的变量只要修改其 value 指向的值,结果都是响应式的。
被 reactive 修饰的变量被重新赋值之后不再具有响应式,需要使用Object.assign(a,b)进行响应式赋值
被 reactive 包裹的对象,内部的属性用ref修饰后会被reactive解构,无需再使用.value

let obj=reactive({price:”123”});
obj={name:"abc"};//不具有响应式
Object.assign(obj,{name:"abc"});//表示将新对象分配给obj时,才具有响应式

响应式对象数据进行解构之后,解构之后的修改数据不再具有响应式,且不会修改到原先对象的属性值。

let obj=reactive({price:”123”,name:"abc"});
let {price,name}=obj;
//修改price和name的值时,页面显示不具有响应式,且obj.price,obj.name不会发生任何改变
let {price,name}=toRefs(obj);//将多个属性解构为ref包括的响应式数据
let n=toRef(obj,'name');//将单个属性解构为ref包括的响应式数据
//修改price和name的值时,页面显示具有响应式,且obj.price,obj.name会同步发生改变

Q:vue2 中的双向数据绑定和 vue3 的 ref 有什么区别?

A:vue2 中的双向数据绑定是利用了 Object.defineProperties(数据劫持),允许通过此属性劫取各个属性的 get 和 set 来实现的,修改的都是同一个目标。ref 借助的是 new proxy()(数据代理),通过 ref 传的值,用 msg 变量进行代理,msg 作为数据的克隆体
msg===‘我是内容’(×) msg.value===‘我是内容’(√)

三、父组件传值:
//父组件
//<Person  a="哈哈哈"  :c="personList"/>
//a="哈哈哈"是字符串,  :c="personList"是表达式
let peronList =[{name:"aaa",age:12},{name:"bbb",age:14}]
//子组件
//<h2>{{a}}</h>
import {defineProps} from 'vue'
import type Persons from '@/...'
//接收变量
defineProps(['a','b','c']);//可以接收但是无法在js里使用,这种写法无法校验父组件传递的类型
let x = defineProps(['a','b','c']);//接收a,b且将props保存起来,又b未传递,b=undefined
//调用时:
console.log(x.a);

//接收c,且校验类型:父组件调用子组件时必须传c,并且c必须是Persons类型的值
defineProps<{c:Persons}>();
//接收c,并限制c的类型,并调整c为非必传,指定默认值
withDefaults( defineProps<{c?:Persons}>(),{
c:()=>[{name:"21edf",age:12}]
})
四、子组件传值

建立一个父页面.vue 和子页面.vue,在父页面中将子组件引入进来,作为组件进行声明。

  • 子页面:
<button @click=’handler’>我是子组件</button>
export default{
emits:[‘showChildMsg’],//检验子组件调用的传参方法是不是showChildMsg,不对的话控制台会报出警告,也可写作:
emits:{
showChildMsg:(value)=>{
if(typeof value ===’string’)
return true;
else{
console.warn(“类型不对!”)
return false;//但传值还是会穿过去,只是作为一个提醒
        }
    }
}
setup(props,context){
const handler = ()=>{
context.emit(“showChildMsg”,”我是子组件内部的消息”)           }
return{ handler}
}
}
  • 父页面:
<child  @showChildMsg=‘fn($event)’></child>
export default{
setup(){
const fn= (event)=>{
altert(event)
}
return{ fn}
}
}
五、计算属性
//<p>{{fullName }}</p>

const num = ref("123");
let fullName = computed(() => {
  return num.value + "2778";
});
//这种计算属性为只读属性,即fullName 只可以获取值,而不可以赋值

let fullName = computed({
  get() {
    return num.value + "2778";
  },
  set(val) {
    num.value = val;
  },
});
fullName.value = "7557";
//会执行一次set方法,因为set里num的值改变了,所以执行get,最终{{fullName}}=75572778
//这种计算属性为可读可写属性
六、watch 监听

取消了 vue2 的深度监听和普通监听,整合成了同一种监听

//(1)监听ref修饰的基本类型数据
let num = ref(1);
watch(num ,(newValue , oldValue)=>{
console.log(newValue,oldValue);
} );


//(2)监听ref修饰的对象类型数据
let num = ref({name:"abc",old:"12"});
watch(num,(newValue,oldValue)=>{
console.log("num变化了");
})
//此时如果修改num的属性值,watch监听并不执行,因为num始终指向的是原先的地址
num.value.name="aaaa";//watch监听不执行
num.value={name:"cba",old:"21"};//watch监听执行,因为num指向了新的地址
//如果需要监听具体数据变化,则需开启深度监听:
watch(num,(newValue,oldValue)=>{
console.log("num变化了");
},{deep:true,immediate:true});
//immediate:true意思为初始化时就触发监听,即oldValue: undefined → newValue: {name:"abc",old:"12"}
num.value.name="aaaa";
//watch监听执行,但oldValue和newValue打印是相同的(修改后的)值,因为num指向的仍然是原先的地址
num.value={name:"cba",old:"21"};
//watch监听执行,因为num指向了新的地址,所以oldValue和newValue不相同


//(3)监听reactive修饰的对象类型数据,隐式开启深度监听(不可关闭)
let num1 = reactive( {name:"abc",old:"12"});
watch(num1 , (newValue,oldValue)=>{
console.log(newValue,oldValue)
} );
num1.name="dbc";
Object.assign(num1,{name:"cba",old:"21"})
//都会触发watch,且newValue和oldValue相同,因为num1始终指向的是同一地址

//(4)用ref、reactive包括的对象类型,单独监听对象内的基本类型的属性,
//需要用函数形式(本质上是一个getter函数);监听对象内的对象类型属性需要分类查看:
let person=reactive({
name:"xiaoming",
grade:{
grade1: "12",
grade2: "23"
}
})
//单独监听对象内的基本类型的属性
watch(()=>person.name,(newValue,oldValue)=>{
console.log("person.name改变了");
})
//监听对象内的对象类型,分类讨论:
watch(person.grade,(newValue,oldValue)=>{
console.log("person.grade属性改变了");
})//只能监听到grade1或grade2的变化,无法监听到person.grade={grade1:"22",grade2:"22"}情况,
//因为只监听最初的对象{grade1: "12",grade2: "23"}
watch(()=>person.grade,(newValue,oldValue)=>{
console.log("person.grade改变了");
})//只能监听到person.grade={grade1:"22",grade2:"22"}情况,无法监听到grade1或grade2的变化
watch(()=>person.grade,(newValue,oldValue)=>{
console.log("person.grade改变了");
},{deep:true})//两种变化皆可监听


//(5)监听多个元素(任一变化就会触发)
watch([()=>person.name,person.grade],()=>{
console.log("")
})
watch([()=>person.name,()=>person.grade.grade1],()=>{
console.log("")
})
  • watchEffect(默认监听):不设定监听的具体值,但在监听的方法体内用到的所有的值发生改变了,那么就会重新调用监听方法
setup(){
const count = ref(0);
const count1 =ref(‘wwww’);
watchEffect(()=>{//页面初始化的时候会触发监听
console.log(count.value);
console.log(count1.value);
//count、count1任一值发生改变之后两个console.log都会被执行,效果类似于全局监听
})
}
  • 停止默认监听:
const count = ref(0);
const count1 =ref(‘wwww’);
const abc= watchEffect(()=>{//页面初始化的时候会触发监听
console.log(count.value);
console.log(count1.value);
if(某条件){
abc()//停止默认监听
}});
七、配置路径别名

vue3 中没有@的路径名,我们可以通过 vite.config.js 进行配置:

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'//引入路径模块
export default defineConfig({
plugins:[vue()],
resolve:{
alias:{
'@': path.resolve(__dirname , 'src/');
//__dirname指当前vite.config.js的绝对路径,即进入vite.config.js绝对路径/src的目录下,定义之后需要重启整个项目
     }
    }
})
八、配置 Router 路由
  • 路由组件通常存放在/pages 或/views 文件夹里,一般组件通常存放在\components 文件夹 - 路由组件:靠路由的规则渲染出来的
    routes:[ {path:'\home',component:Home}] - 一般组件:亲手写标签进行使用的:<HOME/>
    通过点击导航,视觉上消失了的路由组件默认是被卸载了,需要的时候再去挂载。

在 src 目录下创建 router 文件夹,在文件夹下创建 commonRouter.js 和 index.js

  • index.js:
import { createRouter, createWebHashHistory } from "vue-router"; //引入createRouter
import { routes } from "./commonRouter"; //引入路由配置
const router = createRouter({
  //创建路由器实例并传递routes配置
  history: createWebHistory(), //使用了history模式,需要配合后端
  routes, //’routes:routes’的缩写
});
export default router;
  • commonRouter.js:也可以直接定义在 index.js 里的 const router = createRouter...之前
export const routes = [
  {
    path: "/",
    alias: "/home", //路径别名
    component: () => import("@/components/HelloWorld.vue"),
  },
];

或写成:

import Home from "@/components/HelloWorld.vue";
import News from "@/components/News.vue";
export const routes = [
  {
    path: "/",
    alias: "/home", //路径别名
    component: Home,
  },
  {
    path: "/news",
    component: News,
  },
];
  • src 路径下的 main.js 进行引入:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index"; //引入路由
const app = createApp(App);
app.use(router);
app.mount("#app"); //页面渲染
//可写成:router.isReady(()=>app.mount(‘#app’)),
//即等router加载完成之后进行页面的渲染,可以大大提高性能
  • 导航区 router 使用:
<div>
<RouterLink to="/home" active-class="isActive">首页</RouterLink >
<RouterLink to="/news" active-class="isActive">新闻</RouterLink >
<!-- to:跳转到的path ,也可写成  :to="{path:'/news'}"
 active-class:点击后使用的布局class-->
</div>

命名路由:{
name:'xinwen',
path:'/news',
component: News
}
<RouterLink :to="{name:'xinwen'}" >新闻</RouterLink >

  • 使用路由组件展示:
    在展示区域的 div 标签内:
<div>
<RouterView></RouterView>
</div>
//import { RouterView , RouterLink } from 'vue-router'
路由器工作模式
  • history 模式:
    vue2: mode:'history'
    vue3: history:createWebHistory()
    react: BrowserRouter
    优点:URL 更加美观,不带有#,更接近传统的网站 URL
    缺点:后期项目上线,需要服务器配合处理路径问题,否则刷新会有 404 错误
const router = createRouter({
  //创建路由器实例并传递routes配置
  history: createWebHistory(), //使用了history模式,需要配合后端
  routes,
});
  • hash 模式
    优点:兼容性更好,因为不需要服务器端处理路径
    缺点:URL 带有#不太美观,且在 SEO 优化方面相对较差
const router = createRouter({
  //创建路由器实例并传递routes配置
  history: createWebHashHistory(),
  routes,
});
嵌套路由

如果路由组件的内部,需要引用其他路由组件(例:Detail.vue),则称为嵌套路由

{
name:'xinwen',
path:'/news',
component: News ,
children:[
{  path:'detail',//嵌套路由不需要写斜杠
    component:Detail
      }
  ]
}

在 news.vue 中:

//导航区
<RouterLink to="/news/detail">详细</RouterLink>
//展示区
<div>
  <RouterView></RouterView>
</div>
路由传值
  • query 用法:
<ul>
<li v-for="news in newsList" :key="news.id">
<!-- 写法一 -->
<RouterLink  :to=" `/news/detail?id=${news.id}&title=${news.title}` " >
<!-- 写法二 -->
<RouterLink  :to="{
path:'/news/detail',
query:{
id:news.id,
title:news.title,
content:news.content
}
} " >
详细</RouterLink >
</li>
</ul>

获取路由传递的值:

<!-- {{query.id}} -->
import {toRefs} from 'vue'
import {useRoute} from 'vue-router'
let route = useRoute()
let {query} = toRefs(route)
  • params 用法
{
name:'xinwen',
path:'/news',
component: News ,
children:[
{
    name:'xian',
    path:'detail/:id/:title/:content?',//先声明传递参数,加问号代表该参数可传可不传
    component:Detail
      }
  ]
}
//第一种写法
<RouterLink  :to=" `/news/detail?id=${news.id}&title=${news.title}` " >
//第二种写法
<RouterLink  :to="{
name:'xian',    //不可以写path
params:{//不可以传递对象和数组
id:news.id,
title:news.title,
content:news.content
  }
} " >
详细</RouterLink >
</li>
</ul>

获取路由传递的值:

<!-- {{route.params.id}} -->
import {toRefs} from 'vue'
import {useRoute} from 'vue-router'
let route = useRoute()
  • props 用法
{
name:'xinwen',
path:'/news',
component: News ,
children:[
{
    name:'xian',
    path:'detail/:id/:title/:content?',
    component:Detail,
    //第一种写法:将路由收到的所有params参数作为props传递给路由组件
    props:true
    //第二种写法:函数写法:自己决定将什么参数作为props传递给路由组件(主要为适应query写法)
    props(route){
      return route.query
           }
    //第三种写法:对象写法:自己决定将什么参数作为props传递给路由组件
    props:{
    a:100,
    b:200,
    c:300
           }
      }
  ]
}

获取路由传递的值:

<!-- {{id}} -->
defineProps(['id','title','content'])
push 和 replace 属性

我们通常浏览网页都是push模式,将浏览的历史记录通过push,压入栈内,我们可以通过浏览器的前进后退来调整栈顶指针的指向,以此来实现内容的切换。如果我们需要当浏览了当前网页后无法再回到以前的网页,则使用replace模式

<RouterLink replace :to=" `` "> </RouterLink>
编程式路由

作用:为了取代 RouterLink 标签实现编程式的写法,RouterLink 在浏览器里会被解释为 a 标签

import {useRouter} from 'vue-router'
const router = useRouter();
//写法一:字符串
router.push('/news') //用push(也可以用replace)的方式跳转到 /news
//写法二:对象
router.push({
  name:'xiang'
  query:{
    id:"123",
    title:"456",
    content:"789"

  }
})
//用法和:RouterLink的:to是一样的
路由重定向

当我们访问一个指定路径的时候,默认跳转到另一个路径,即为路由重定向

export const routes = [
  {
    path: "/home", //路径别名
    component: Home,
  },
  {
    path: "/news",
    component: News,
  },
  {
    path:"/"
    redirect:'/home'
//当我们访问http//:localhost:8088时,路由重定向到当我们访问http//:localhost:8088/home
  }
];
九、ref 唯一标识的使用

ref 的使用:由于父子组件中相同 id 会导致一些 Dom 操作的混淆,所以推荐使用 ref 唯一标识当前 vue 文件中的元素,类似于局部变量,防止组件之间 id 元素相互混淆。

  • ref 使用在基本 HTML 标签:
//<h ref="title">abccccc</h>
let title = ref(); //创建一个title,存储ref标记的内容
console.log(title.value); //输出<h >abccccc</h>
  • ref 使用在组件标签中:
//<Person ref="title1"/>
let title1 = ref();
console.log(title1.value);
//能够获取Person组件的相关内容,但是内容被封装无法展示,需要子组件使用`defineExpose`

子组件 Person:

let a = ref(0);
let b = ref(1);
defineExpose({ a, b }); //父组件中即可输出a,b的内容
十、TS 接口规范

用于在 vue3 内规范引用数据类型的使用,避免不必要的错误

export interface PersonInter{
name:string,
age:number,
id:string,
x?:string  //?代表这个属性可有可无
}
export type Persons=Array<PersonInter>//数组写法1
export type Persons=PersonInter[]//数组写法2

import {type PersonInter  ,  type Persons} from "@/..."
let person:PersonInter=reactive({name:"abc",age:12,id:"001"});
let personList:Array<PersonInter>=[{},{},{}]
let personList:Persons=[{},{},{}];
let personList=reactive<Persons>([{},{},{}])
十一 、生命周期函数
                        vue2                                        vue3
创建  创建前         beforeCreate()                                 setup
      创建完毕        created()                                     setup
挂载  挂载前         beforeMount()                           onBeforeMount(()=>{})
      挂载完毕        mounted()                                onMounted(()=>{})
更新  更新前         beforeUpdate()                          onBeforeUpdate(()=>{})
      更新完毕        updated()                                onUpdated(()=>{})
销毁  销毁前         beforeDestory()               onBeforeUnmount(()=>{}) (卸载前)
      销毁完毕        destoryed()                    onUnmounted(()=>{})(卸载完毕)

通常父组件需要等所有的子组件挂载完毕后,才算父组件挂载完毕。

十二、自定义 hooks

体现的是封装的思想,把一系列处理特定数据的行为封装存入在 src 目录下创建的 hooks 文件夹,并命名为 useXXX.ts,此处 XXX 一般指处理指定的数据名称。且 hooks 内也存在属于自己的生命周期
例子:src/hooks/useSum.ts:

import { ref } from 'vue'
export default function () {//默认抛出的方法或属性不能设定名称
\\ 数据
let sum = ref(0)
\\方法
 function add(){
   sum.value+=1;
 }
//向外部提供相关属性和方法
return {sum ,add}
}

app.vue:

import useSum from "@/hooks/useSum";
const { sum, add } = useSum();
十三、 Pinia的使用

(1) 准备:npm i pinia
(2) 在main.ts里引入:

import {createApp} from 'vue'
import App from './App.vue'
//引入pinia
import {createPinia} from 'pinia'
///
const app = createApp(App)
//创建pinia
const pinia = createPinia()
//安装pinia
app.use(pinia)
app.mount('#app')

(3) 在src目录下创建文件夹store,里面根据存储的分类创建相应命名的ts文件,下文以count.ts为例:

import {defineStore} from 'pinia'
export const useCountStore = defineStore('count',{
    //真正存储数据的地方
    state(){
        return{
            sum:6
            school:'aaaa',
            address:'bbbbb'
        }
    }
})//第一个参数是文件名,第二个参数时存储的内容

使用时:

import {useCountStore} from '@/store/count'
const countStore = useCountStore()
//以下两种方式皆可拿到state中的数据
console.log(countStore.sum)
console.log(countStore.$state.sum)
//<p>  {{countStore.sum}}  </p>

//修改数据
function add(){
    //第一种修改方式,改几条数据就会执行几次修改
    countStore.sum+=1;
    //第二种修改方式:适合批量修改,在内部只执行一次修改,
    countStore.$patch({
        sum:888,
        school:"cccc",
        address:"12121"
        })
    //第三种修改方式,需要设定
    countStore.increment(n.value)
}
import {defineStore} from 'pinia'
export const useCountStore = defineStore('count',{
    //actions里面放置的是一个一个的方法,用于相应组件中的动作
    actions:{
        increment(value){
            this.sum += value
        }
    }
    //真正存储数据的地方
    state(){
        return{
            sum:6
            school:'aaaa',
            address:'bbbbb'
        }
    }
})//第一个参数是文件名,第二个参数时存储的内容

响应式解构:storeToRefs()

const {sum,school,address} = toRefs(countStore)
//这会导致countStore里包括属性和方法都受到响应式包裹,消耗大
const {sum,school,address} = storeToRefs(countStore);
//仅关注属性,消耗小

getters的使用:对数据进行加工

import {defineStore} from 'pinia'
export const useCountStore = defineStore('count',{
    //actions里面放置的是一个一个的方法,用于相应组件中的动作
    actions:{
        increment(value){
            this.sum += value
        }
    },
    //真正存储数据的地方
    state(){
        return{
            sum:6
            school:'aaaa',
            address:'bbbbb'
        }
    },
    getters:{
        bigSum(state){
            return state.sum * 10
        },
        // bigSum:state=> state.sum * 10,//箭头函数
        upperSchool():string{
            return this.school.toUpperCase
        }//要用this就不可以用箭头函数

    }
})//第一个参数是文件名,第二个参数时存储的内容
// <p>{{bigSum}}使用展示{{upperSchool}}</p>
const {sum,school,address,bigSum,upperSchool} = storeToRefs(countStore);
订阅$subscribe()的使用:

设talkList是一个字符串数组

import {useTalkStore} from '@/store/loveTalk'
import {storeToRefs} from 'pinia'
const talkStore = unTalkStore()
const {talkList} = storeToRefs(talkStore)
talkStore.$subscribe((mutate,state)=>{
    //当talkStore里面保存的数据发生了变化时会调用
    localStorage.setItem('talkList',JSON.stringify(state.talkList);
    //json格式存储对象类型数据需要使用JSON.stringify
    //将变化后的talkList存入浏览器的localStorage,保证关闭浏览器后数据也不会丢失
})
export const useTalkStore = defineStore('talk',{
    state(){
        return{
            talkList:JSON.parse(localStorage.getItem('talkList') as string)|| []
            //talkList可能为空,设定一个空数组避免返回null
        }
    }
})
Store的组合式写法
export const useTalkStore = defineStore('talk',()=>{
    //talkList就是state
    const talkList = reactive(JSON.parse(localStorage.getItem('talkList') as string)|| [])
    //getTalk函数相当于action
    function getATalk(){
        let obj={id:aaa,title:1111}
        talkList.unshift(obj)
    }
    //组合式写法一定要注意进行return
    return {talkList,getATalk}
})
十四、组件传值

基础传值:父组件向子组件传递数据,可直接通过属性传递给子组件,如car。但子组件要向父组件传递数据,需要父组件给予子组件一个方法,子组件通过调用方法向父组件传递数据,如sendToy

<!-- 父组件 -->
<div class="father">
    <Child :car="car" :sendToy="getToy" />
    <!--  当子组件调用sendToy时,执行父组件的getToy -->
</div>
<script>
    import Child from './Child.vue'
    import {ref} from 'vue'
    let car = ref('奔驰')
    let toy = ref('')
    //方法
    function getToy(value:string){
        toy.value = value
    }
</script>
<!-- 子组件 -->
<div class="child">
    <h>{{toy}}</h>
    <h>{{car}}</h>
    <!-- 用法一 -->
    <button @click="sendToy(toy)">把toy值传递给父组件</button>
    <!-- 用法二 -->
    <button @click="func">把toy值传递给父组件</button>
    <!-- 用法三 -->
    <button @click="emit('sendToy',toy)">把toy值传递给父组件</button>
</div>
<script>
     import {ref} from 'vue'
     let toy = ref('aaa')
     //声明接收props
     defineProps(['car','sendToy'])//用法一
 
     let props = defineProps(['car','sendToy'])//用法二
     function func(){//用法二
        props.sendToy(toy);
     } 

    //  用法三
    const emit = defineEmits(['sendToy'])
</script>
mitt实现任意组件通信

类似于pubsub$busmitt都是实现:
任意组件之间:
接收数据方:提前绑定好事件(pubsub:提前订阅消息)
提供数据方:任意组件在合适的时候触发事件(pubsub:发布消息)

  • 准备:npm i mitt
  • 在src目录下创建utils文件夹,新建emitter.ts:
//引入mitt
import mitt from 'mitt'
//调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()
//暴露emitter
export default emitter
  • 使用方法:
//绑定事件:
emitter.on('test1',()=>{
    console.log('test1被调用了')
})
//调用
function fun(){
    //触发事件
    emitter.emit('test1')
    //解绑事件
    emitter.off('test1')
    //获取所有绑定事件,统一解绑
    emitter.all.clear()
}
  • 样例:
    接收方:
import {ref,onUnmounted} from 'vue'
import emitter from '@/utils/emitter'
//数据
let toy = ref("")
//给emitter绑定send-toy事件
emitter.on('send-toy',(value:any)=>{
    toy.value = value
})
//在组件卸载时解绑send-toy事件,避免资源消耗
onUnmounted(()=>{
    emitter.off('send-toy')
})

发送方:

<button @click="emitter.emit('send-toy',toy)"></button>
<script setup lang='ts'>
import {ref} from 'vue'
import emitter from '@/utils/emitter'
let toy = ref("aaaa");
</script>
使用v-model 进行组件通信
<!-- 父组件 -->
<Child v-model:name="userName"/>
<!-- name是username的别名,也可以:v-model="userName" -->
<!-- 子组件 -->
<script>
    defineProps(['name'])
</script>
使用$attrs实现(爷)祖-孙组件通信

在组件通信中,未被defineProsps获取的数据都会存在$attrs中,子组件不做获取时,可以将$attrs传递给它的子组件使用,即实现祖孙通信。

<!-- 父组件 -->
<!-- 可写成 v-bind="{a:123}"-->
<Child :a="123" :update="getName"></Child>
<script>
    function getName(value){}
</script>
<!-- 子组件 -->
<GrandChild v-bind="$attrs"/>
<!-- 孙组件 -->
<button @click="update('12345')"></button>
<script>
    defineProps(['a','update'])
</script>
使用 $refs$parent
  • $refs实现父组件向子组件通信
  • $parent实现子组件向父组件通信
<!-- 父组件 -->
<button @click="changeToy">改变child1的玩具</button>
<Child1 ref="c1"></Child1>
<Child1 ref="c2"></Child1>
<script>
let c1 = ref();
function changeToy(){
     c1.value.toy="bbb"
}
</script>
<!-- 子组件1 -->
<script>
let toy = ref('aaaa')
let book = ref(3)
defineExpose({toy,book})//把数据交给外部
</script>

$refs里存着所有的子组件暴露给父组件的数据,值为对象,包含所有被ref属性标识的Dom元素或组件实例

<button @click="getChild($refs)">改变child1的玩具</button>
<Child1 ref="c1"></Child1>
<Child1 ref="c2"></Child1>
<script>
    function getChild(refs:{[key:string]:any}){
        for(let key in refs){
            refs[key].book +=3
        }
    }
</script>

$parent里存着所有的父组件抛出的数据。值为对象,当前组件的父组件实例对象。

<!-- 父组件 -->
<script>
let name = "AAA"
 defineExpose({name})
</script>
<!-- 子组件 -->
<button @click="changeName($parent)"></button>
<script>
    function changeName(parent:any){
        parent.name+="123"
    }
</script>
使用provide-inject

优势在于:实现祖孙之间直接传递数据,且不需要经过中间的父组件。

//祖组件
let money = ref(123)
let car = reactive({
    brand:'奔驰',
    price:200

})
//向后代提供数据,无论隔了多少代都可以获取
provide('qian',money)//不能传.value是因为需要传一个响应式对象过去才能具有响应式
provide('che',car)

// 后代组件获取
let x = inject("qian","我是默认值")//如果没有找到qian,则x为默认值,如果不设定默认值可以运行但是ts会有代码警告
let y = inject("che",{brand:"未知",price:0})

祖组件给予后代组件修改数据的方法:

<script>
    let money = ref(123)
    function changeMoney(value:number){
        money.value-=value;
    }
    provide('moneyContext',{money,changeMoney})
</script>

// 子组件获取
<button @click="changeMoney(6)">改变祖组件的money值</button>
<script>
 let {money,changeMoney} = inject("moneyContext",{money:0,changeMoney:(param:number)=>{}}) 
</script>
插槽

父组件中自定义展示在子组件内部的内容

  • 默认插槽
<!-- 父组件 -->
<Category>
    <!-- 双标签内部加入需要展示在子组件内部的内容 -->
    <p>12334452</p>
</Category>
<!-- 子组件 -->
<div>
    <h2>title</h2>
    <slot>如果父组件没有定义传入内容,则默认展示这段文字。若传了内容,则不展示这段文字</slot>
    <!-- 若子组件中出现了多次默认插槽,则父组件中传入的内容都会展示在每个slot的位置 -->
</div>
  • 具名插槽
    为了指定内容展示在特定的子组件区域
<!-- 父组件 -->
<Category>
    <template v-slot:s2>
        <!-- 指定这段内容展示在子组件中插槽名为s2的插槽中 -->
        <p>12334452</p>
    </template>
    <template #s1>
        <!-- v-slot:s1 也可写成#s1 -->
        <p>这是标题</p>
    </template>
</Category>
<!-- 子组件 -->
<div>
   <slot name="s1"></slot>
   <slot name="s2"></slot>
   <!-- 默认插槽的name="default" -->
</div>
  • 作用域插槽
    为了让子组件统一管理需要展示的数据内容,父组件调用时数据需要从子组件获取
<!-- 父组件 -->
<Game v-slot="params">
    <p>{{params.x}}</p>
</Game>
<Game v-slot:s1="{x}">
    <p>{{x}}</p>
</Game>
<!-- 子组件 -->
<slot name="s1"  x="2334" :youxi="games"></slot>
<slot  x="2334" ></slot>
<!-- 传递的数据全部会生成一个params对象被父组件接收 -->
<script>
    let games = reactive([{...}])
</script>
其他API
shallowRefshallowReactive
  • shallowRef:创建一个响应式数据,但只对顶层属性进行响应式处理
    用法:
let a = shallowRef({
    name:"123"
})
a.value={name:"321"}//可以生效
a.value.name="321"//不可以生效  shallowRef只关注.value(最外层)层级的修改

特点:只跟踪引用值的变化,不关心值内部的属性变化

  • shallowReactive:创建一个浅层响应式对象,只会使对象的最顶部属性变成响应式的,对象内部的嵌套属性则不会变成响应式的
    用法:
const obj =shallowReactive({...})

特点:对象的顶层属性是响应式的,但嵌套对象的属性不是

通过使用shallowRef()shallowReactive()来绕开深度响应。浅层式API创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可以提升性能

readonlyshallowReadonly

限制属性的修改权限

let sum = ref(1)
let sum2 = readonly(sum)//sum2不可修改,但sum改变时sum2会跟着改变

let car = reactive({
    brand:"奔驰",
    option:{
        color:'红色',
        price:100
    }
})
let car1 = shallowReadonly(car)
//car1.brand不可更改,但car1.option.color可以更改,shallowReadonly只保证最顶层只读。
//但car1的所有值都会随着car更改而更改
toRawmarkRaw
  • toRaw:用于获取一个响应式对象的原始对象,toRaw返回的对象不再是响应式的,不会触发试图更新
  • markRaw:一个非响应式对象可以通过reactive包裹后变成一个具有响应式的对象,markRaw标记一个对象后,永远不会是响应式的。其即便是reactive包裹之后也不会变成响应式的
customRef

作用:用于创建一个自定义的ref,并对其依赖项跟踪和更新触发进行逻辑控制

export default function(initValue:string,delay:number){
let msg = customRef((track,trigger)=>{
    let timer:number
    return{
        get(){//msg被读取时调用
            track()//告诉vue数据msg很重要,你要对msg进行持续关注一旦msg变化就更新
            return initValue
        },
        set(value){//msg被修改的时候调用
            clearTimeout(timer)
            timer = setTimeout(()=>{
                initValue = value
                trigger()//通知vue数据msg变化了
            },delay)
        }
    }
})
}
Teleport

一种能够将我们的组件html结构移动到指定位置的技术

<teleport to='body'>
    <!-- 将内部的内容转移至body标签内部 -->
    <div>...</div>
</teleport>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容