vue 3 学习

  1. 组合式API

  2. setup,setup里的this是undifined,最后ret到模版

    选项式的数据,方法,就是定义在setup里面的数据函数

    setup比beforeCreate的声明周期更早,由于其生命周期很早,因此其他的组件也可以读取到setup里定义的变量/函数,通过this读取,也就是说旧写法能读取新写法的,但是新写法不能读取旧的,也就是setup读不了其他的

    箭头函数:例子return ()=>'hello',setup的返回值也可以是一个函数/箭头函数

  3. 响应式:如果直接定义一个变量,比如 let name = "EPsilon",这个就不是响应式的,当我们通过一些事件修改这个变量的时候,虽然日志打印是没问题的,但是前端不会同步响应,要想达到响应式,就得用 ref 来定义,比如 let name = ref("EPsilon")

  4. setup 语法糖:在 <script> 标签里面写 <script setup> ,就相当于写了一个 setup 函数

    vue3 组件可能会有两个 <script> 标签,具体如下:

    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 这个script标签是为了声明组件名称的,但是这么写稍微有点麻烦,专门为了声明name开一个script -->
    <script lang="ts">
    export default {
    name: 'Person'
    }
    <script>
    <script lang="ts" setup>
    <script>

    可以通过插件变成如下写法:

    1
    npm install vite-plugin-vue-extend
    1
    2
    <script lang="ts" setup name="Person">
    <script>

    此外还要修改 vite.config.ts 文件如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import { fileURLToPath, URL } from 'node:url'

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import VueSetupExtend from "vite-plugin-vue-extend"

    // https://vitejs.dev/config/
    export default defineConfig({
    plugins: [
    vue(),
    VueSetupExtend()
    ],
    resolve: {
    alias: {
    '@': fileURLToPath(new URL('./src', import.meta.url))
    }
    }
    })
  5. 响应式数据: refreactive

    • ref:可以基本类型/对象类型响应式数据,被ref 包裹的是对象,script 里面要加 .value,html 里面自动加不要写(vscode volar 插件可以自动加 value),ref 也不能直接整个替换。

    • reactive:只能对象类型响应式数据,可以不用加 .value,但是当其重新分配对象(就是直接一整个把对象给换了)时会失去响应式,可以用 Object.assign() 接口改。

    • 区别就是 refRefImpl 对象,同时里面的 value 内嵌了一个 Proxy 对象,reactive 就是一个 Proxy 对象,也就是说 ref 是依赖 reactive 实现的。

  • 使用原则:基本类型必须 ref,对象类型层级不深都可以,层级较深最好 reactive
  • toRefs:解构响应式对象的时候使用,比如 let {name, age} = personname, age 就不是响应式的,这时候let {name, age} = toRefs(person)name, age 就是响应式的,而且还是深拷贝
  • toRef:一次解构一个,let age = toRef(person, 'age')
  1. computed 计算属性,可以用箭头函数,此外计算属性还有缓存,但是方法没有缓存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 首字母大写,这样定义的计算属性是只读的,不可修改
    let fullname = computed(() => {
    return firstName.value.slice(0,1).toUppercase() + firstName.value.slice(1)+ '-' + lastName.value
    })
    // 下面这种是可修改的方案
    let fullname = computed({
    get() {
    return firstName.value.slice(0,1).toUppercase() + firstName.value.slice(1)+ '-' + lastName.value
    },
    set(val) {
    const [str1, str2] = val.split('-')
    firstName.value = str1
    lastName.value = str2
    }
    })

    // fullname = "li-si",执行这条语句的时候会调用 set() 方法,传入val并处理
  2. watch:监视以下四种数据:

    • ref 定义的数据
    • reactive 定义的数据
    • 函数返回的值 (getter函数)
    • 包含上述内容的数组

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      // 监视ref基本类型数据,注意不是 value 属性,是 ref 本身
      let sum = ref(0)
      watch(sum, (newValue, oldValue) => {
      console.log("sum changed")
      })

      // 监视ref对象类型数据,监视的是对象的地址(也就是说内部属性改了就不会触发),如果要监视内部属性就要加上 deep: true,如果要在刷新后立即触发就要加上 immediate: true
      // 注意:实际开发中一般不管这个 oldValue,当ref对象地址被替换(一整个替换了)的时候 new/old Value才不一样(因为本质还是监视地址),如果只改变了一个属性,newValue和oldValue就是一样的,因为地址没变
      let person = ref({name: "", age: 0})
      watch(person, (newValue, oldValue) => {
      console.log("person changed")
      },{
      deep: true,
      immediate: true
      })

      // 监视 reative 对象类型数据,我们知道 reactive 对象替换用的是 Object.assign(),这个方法不会改地址,隐式创建深层监听
      let person = reactive({name: "", age: 0})
      watch(person, (newValue, oldValue) => {
      console.log("person changed")
      })

      // 假如我只想监听一个reactive对象中的某个属性,就要监听一个 getter 函数,也就是有一个返回值的函数,可以直接写箭头函数
      let person = reactive({name: "", age: 0})
      watch(() => person.name, (newValue, oldValue) => {
      console.log("person.name changed")
      })

      // 假如我向监听一个reactive对象中包含的一个对象,直接写 person.car,监听的就是car的内部属性,隐式创建深层监听,如果写箭头函数,监听的就是person.car的地址,如果既要监听地址又要监听内部属性,就在 getter 函数的基础上手动开启深度监听
      let person = reactive({name: "", age: 0, car: {car1: "a", car2: "b"}})
      watch(() => person.name, (newValue, oldValue) => {
      console.log("person.name changed")
      })

      // 同时监视多个数据
      let person = reactive({name: "", age: 0, car: {car1: "a", car2: "b"}})
      watch([()=>person.name,person.car], (newValue, oldValue)=>{
      console.log('person.car changed', newValue, oldValue)
      },{deep: true})
  3. watchEffect 不用指定监视谁,而是函数中用到哪些属性,那就监视哪些属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    let temp = ref(0)
    let height = ref(0)

    // 用watch实现,需要明确的指出要监视:temp, height, 室温达到50℃,或水位达到20cm,立刻联系服务器
    watch([temp,height],(value)=>{
    const [newTemp,newHeight] = value
    if(newTemp >= 50 || newHeight >= 20){
    console.log('联系服务器')
    }
    })

    // 用watchEffect实现,不用明确的指出要监视谁,
    const stopWtach = watchEffect(()=>{
    if(temp.value >= 50 || height.value >= 20){
    console.log(document.getElementById('demo')?.innerText)
    console.log('联系服务器')
    }
    if(temp.value === 100 || height.value === 50){
    console.log('清理了')
    stopWtach()
    }
    })
  4. 标签的 ref 属性

    • 用在普通DOM标签上,获取的是DOM节点。

    • 用在组件标签上,获取的是组件实例对象。

      template 最后是要融合在一起的,如果 id 冲突了就会产生覆盖,这时候要用 ref 新建容器来避免

      下面这个写法如果另一个 vue 里面也定义了 id="title2",就会出现覆盖

      1
      <h2 id="title2">北京</h2>

      因此我们应该用 ref 属性

      1
      <h2 ref="title2">上海</h2>

      用在组件上,返回的就是组件实例,比如,这时候 person 就是一个组件实例,但是被保护了,需要用在子组件用 defineExpose 暴露给父组件

      1
      <Person ref="person"/>
  5. props 属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 定义一个接口,限制每个Person对象的格式
    export interface PersonInter {
    id:string,
    name:string,
    age:number
    }

    // 定义一个自定义类型Persons
    export type Persons = Array<PersonInter>

    app.vue 提供参数 list

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import {defineProps} from 'vue'
    import {type PersonInter} from '@/types'

    // 第一种写法:仅接收
    const props = defineProps(['list'])

    // 第二种写法:接收+限制类型
    defineProps<{list:Persons}>()

    // 第三种写法:接收+限制类型+指定默认值+限制必要性
    let props = withDefaults(defineProps<{list?:Persons}>(),{
    list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
    })
    console.log(props)
  6. 生命周期/生命周期函数/生命周期钩子

    • 生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。

    • Vue2的生命周期

      创建阶段:beforeCreatecreated

      挂载阶段:beforeMountmounted

      更新阶段:beforeUpdateupdated

      销毁阶段:beforeDestroydestroyed

    • Vue3的生命周期

      创建阶段:setup

      挂载阶段:onBeforeMountonMounted

      更新阶段:onBeforeUpdateonUpdated

      卸载阶段:onBeforeUnmountonUnmounted

    • 常用的钩子:onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)

    • 子组件先挂载,父组件后挂载

  7. hook(模块化开发):axios

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 从指定 URL 获取 message
    async function get() {
    try{
    let res = await axios.get('URL')
    list.push(res.data.message)
    } catch (error) {
    alert(error)
    }
    }

    新建一个 hooks 文件夹,然后命名格式是 use 开头的 ts 文件

    export default + 值(立即数,数组,对象,没名字的 function), export + 有名字的 function

    一个 hooks 文件例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import {reactive,onMounted} from 'vue'
    import axios,{AxiosError} from 'axios'

    export default function(){
    let dogList = reactive<string[]>([])

    // 方法
    async function getDog(){
    try {
    // 发请求
    let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
    // 维护数据
    dogList.push(data.message)
    } catch (error) {
    // 处理错误
    const err = <AxiosError>error
    console.log(err.message)
    }
    }

    // 挂载钩子
    onMounted(()=>{
    getDog()
    })

    //向外部暴露数据
    return {dogList,getDog}
    }
  8. 路由:route

    • 路由时一组 key-value 的对应关系
    • 多个路由,需要经过路由器管理

    路由变化以后路径改变,然后 router 卸载原先的组件挂载新组件

    一个页面分为标题,导航区,展示区,有一个文件夹 router 里面一个 index.ts 是路由器,还有一个 pages 文件夹里面是组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import {createRouter, createWebHistory} from 'vue-router'

    // 引入组件
    import Home from '@/pages/Home.vue'
    import News from '@/pages/News.vue'
    import About from '@/pages/About.vue'

    // 创建路由器
    const router = createRouter({
    history:createWebHistory(), // 路由器的工作模式
    routes:[
    {
    path:'/home',
    component:Home
    },
    {
    path:'/about',
    component:About
    }
    {
    path:'/news',
    conponent: News
    }
    ]
    })

    // 暴露 router
    export default router

    main.ts

    1
    2
    3
    import router from './router/index'
    app.use(router)
    app.mount('#app')

    app.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <template>
    <div class="app">
    <h2 class="title">Vue路由测试</h2>
    <!-- 导航区 -->
    <div class="navigate">
    <RouterLink to="/home" active-class="active">首页</RouterLink>
    <RouterLink to="/news" active-class="active">新闻</RouterLink>
    <RouterLink to="/about" active-class="active">关于</RouterLink>
    </div>
    <!-- 展示区 -->
    <div class="main-content">
    <RouterView></RouterView>
    </div>
    </div>
    </template>

    <script lang="ts" setup name="App">
    import {RouterLink,RouterView} from 'vue-router'
    </script>
    • 路由组件(靠路由规则渲染出来的)通常存放在pagesviews文件夹,一般组件(自己加标签引入)通常存放在components文件夹。

    • 通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载

    history模式

    优点:URL更加美观,不带有#,更接近传统的网站URL

    缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有404错误。

    1
    2
    3
    4
    const router = createRouter({
    history:createWebHistory(), //history模式
    /******/
    })

    hash模式

    优点:兼容性更好,因为不需要服务器端处理路径。

    缺点:URL带有#不太美观,且在SEO优化方面相对较差。

    1
    2
    3
    4
    const router = createRouter({
    history:createWebHashHistory(), //hash模式
    /******/
    })
    • to 两种写法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <!-- 第一种:to的字符串写法 -->
      <router-link active-class="active" to="/home">主页</router-link>

      <!-- 第二种:to的对象写法 -->
      <router-link active-class="active" :to="{path:'/home'}">Home</router-link>

      // 也可以给路由规则命名
      {
      name:'xinwen',
      path:'/news',
      component:News,
      }
      <router-link active-class="active" :to="{name:'news'}">Home</router-link
    • 嵌套路由,使用children配置项:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      {
      name:'xinwen',
      path:'/news',
      component:News,
      children:[
      {
      name:'xiang',
      path:'detail',
      component:Detail
      }
      ]
      }
    • 跳转路由(记得要加完整路径):

      1
      2
      3
      <router-link to="/news/detail">xxxx</router-link>
      <!-- 或 -->
      <router-link :to="{path:'/news/detail'}">xxxx</router-link>

      记得去Home组件中预留一个<router-view>

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <template>
      <div class="news">
      <nav class="news-list">
      <RouterLink v-for="news in newsList" :key="news.id" :to="{path:'/news/detail'}">
      {{news.name}}
      </RouterLink>
      </nav>
      <div class="news-detail">
      <RouterView/>
      </div>
      </div>
      </template>
    • query 传参:

      • 传递参数

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        <!-- 跳转并携带query参数(to的字符串写法) -->
        <router-link to="/news/detail?a=1&b=2&content=欢迎你">
        跳转
        </router-link>

        <!-- 跳转并携带query参数(to的对象写法) -->
        <RouterLink
        :to="{
        //name:'xiang', //用name也可以跳转
        path:'/news/detail',
        query:{
        id:news.id,
        title:news.title,
        content:news.content
        }
        }"
        >
        {{news.title}}
        </RouterLink>
      • 接收参数

        1
        2
        3
        4
        import {useRoute} from 'vue-router'
        const route = useRoute()
        // 打印query参数
        console.log(route.query)
- params 参数

  - 传递参数

    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 跳转并携带params参数(to的字符串写法) -->
<RouterLink :to="`/news/detail/001/新闻001/内容001`">{{news.title}}</RouterLink>

<!-- 跳转并携带params参数(to的对象写法) -->
<RouterLink
:to="{
name:'xiang', //用name跳转
params:{
id:news.id,
title:news.title,
content:news.title
}
}"
>
{{news.title}}
</RouterLink>
- 接收参数:
1
2
3
4
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印params参数
console.log(route.params)
> 备注1:传递`params`参数时,若使用`to`的对象写法,必须使用`name`配置项,不能用`path`。 > > 备注2:传递`params`参数时,需要提前在规则中占位。 - props 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
name:'xiang',
path:'detail/:id/:title/:content',
component:Detail,

// props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件
// props:{a:1,b:2,c:3},

// props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件
// props:true

// props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件
props(route){
return route.query
}
}
- replace 属性 作用:控制路由跳转时操作浏览器历史记录的模式。 浏览器的历史记录有两种写入方式:分别为```push```和```replace```: - ```push```是追加历史记录(默认值)。 - `replace`是替换当前记录。 - 编程式导航:路由组件的两个重要的属性:`$route`和`$router`变成了两个`hooks`
1
2
3
4
5
6
7
8
9
import {useRoute,useRouter} from 'vue-router'

const route = useRoute()
const router = useRouter()

console.log(route.query)
console.log(route.parmas)
console.log(router.push)
console.log(router.replace)
- 重定向:将特定的路径,重新定向到已有路由。
1
2
3
4
{
path:'/',
redirect:'/about'
}
  1. pinia:集中式状态(数据)管理

    [Pinia | The intuitive store for Vue.js (vuejs.org)](https://pinia.vuejs.org/zh/)
    
    • Store是一个保存:状态业务逻辑 的实体,每个组件都可以读取写入它。

      • 它有三个概念:stategetteraction,相当于组件中的: datacomputedmethods

      存储、读取、修改数据:

      需要暴露一个 store,首先关注 state,里面存的是数据,然后外部组件直接引入 useCountStore 即可得到这个 store

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // 引入defineStore用于创建store
      import {defineStore} from 'pinia'

      // 定义并暴露一个store
      export const useCountStore = defineStore('count',{
      // 动作
      actions:{},
      // 状态
      state(){
      return {
      sum:6
      }
      },
      // 计算
      getters:{}
      })

      修改数据可以单个数据修改,也可以用 store.$patch() 批量修改,还可以借助 action 修改(定义成动作可以复用,项目常用)

      在引入 store 的时候,如果要解构成响应式的数据,不要用 toRefs ,这样会把整个 store 对象的每个成员都变成 ref,应该用 pinia 提供的 storeToRefs 来进行解构

    • getters:当state中的数据,需要经过处理后再使用时,可以使用getters配置,也就是说 getters 可以直接读取 state 不通过 this ,当然也可以通过 this

      1
      2
      3
      4
      5
      getters:{
      bigSum:(state):number => state.sum *10,
      upperSchool():string{
      return this. school.toUpperCase()
      }
    • store.$subscribe :通过 store$subscribe() 方法侦听 state 及其变化,有点类似 watch

      1
      2
      3
      4
      talkStore.$subscribe((mutate,state)=>{
      console.log('LoveTalk',mutate,state)
      localStorage.setItem('talk',JSON.stringify(talkList.value))
      })
    • 组合式写法:不写 state, getters, action,而是直接定义数据、函数然后返回

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      import {defineStore} from 'pinia'
      import axios from 'axios'
      import {nanoid} from 'nanoid'
      import {reactive} from 'vue'

      export const useTalkStore = defineStore('talk',()=>{
      // talkList就是state
      const talkList = reactive(
      JSON.parse(localStorage.getItem('talkList') as string) || []
      )

      // getATalk函数相当于action
      async function getATalk(){
      // 发请求,下面这行的写法是:连续解构赋值+重命名
      let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
      // 把请求回来的字符串,包装成一个对象
      let obj = {id:nanoid(),title}
      // 放到数组中
      talkList.unshift(obj)
      }
      return {talkList,getATalk}
      })
  2. 组件通信:分四类来说

    image-20241007170704786

    • props:使用频率最高的一种通信方式,常用与 :父 ↔ 子。若 父传子:属性值是非函数;若 子传父:属性值是函数,用 defineProps

    • 自定义事件:常用于 子 => 父

      • 原生事件:

        • 事件名是特定的(clickmosueenter等等)
        • 事件对象$event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
      • 自定义事件:

        • 事件名是任意名称
        • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型
      • 示例:

        1
        2
        3
        4
        5
        <!--在父组件中,给子组件绑定自定义事件:-->
        <Child @send-toy="toy = $event"/>

        <!--注意区分原生事件与自定义事件中的$event-->
        <button @click="toy = $event">测试</button>
        1
        2
        //子组件中,触发事件:
        this.$emit('send-toy', 具体数据)
    • mitt:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信,其实也就是一个全局变量任意两个组件只要找到 emitter 即可通信

      1
      2
      3
      4
      5
      import mitt from "mitt"
      const emitter = mitt()
      emitter.on('your-event', (value)=> {console.log(value)}) // 绑定事件
      emitter.off() // 清理事件
      emitter.emit('your-event', value) // 触发事件
    • v-model:实现 父↔子 之间相互通信

      v-model本质:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <!-- 使用v-model指令 -->
      <input type="text" v-model="userName">

      <!-- v-model的本质是下面这行代码 -->
      <input
      type="text"
      :value="userName"
      @input="userName =(<HTMLInputElement>$event.target).value"
      >

      组件标签上的 v-modle 本质:

      1
      2
      3
      4
      5
      6
      7
      <!-- 组件标签上使用v-model指令,这里v-model后面不跟,默认就是 modelValue -->
      <epInput v-model="userName"/>
      <epInput v-model: epName="userName"/>

      <!-- 组件标签上v-model的本质 -->
      <epInput :modelValue="userName" @update:modelValue="userName = $event"/>
      <epInput :epName="userName" @update:ep-name="userName = $event"/>

      epInput 组件内部写法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <template>
      <div class="box">
      <!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
      <!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
      <input
      type="text"
      :value="epName"
      @input="emit('update:ep-value',$event.target.value)"
      >
      </div>
      </template>

      <script setup lang="ts" name="epInput">
      // 接收props
      defineProps(['epName'])
      // 声明事件
      const emit = defineEmits(['update:ep-name'])
      </script>
    • $attrs:用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。$attrs是一个对象,包含所有父组件传入的标签属性。

      父组件传给子组件一个 v-bind,子组件将其分为两类:propsattrs,如果用 defineProps 接收,就是 props,孙组件就用不了,如果不接收,就是 attrs,子组件没用的数据就可以通过 v-bind: $attrs 传给孙组件

      注意:$attrs会自动排除props中声明的属性(可以认为声明过的 props 被子组件自己“消费”了)

    • refs, parentrefs 用于 父→子$parent用于:子→父。

      | 属性 | 说明 |
      | ————- | ———————————————————————————— |
      | $refs | 值为对象,包含所有被ref属性标识的DOM元素或组件实例。 |
      | $parent | 值为对象,当前组件的父组件实例对象。 |

    • provide, inject:实现祖孙组件直接通信,

      • 具体使用:
        • 在祖先组件中通过provide配置向后代组件提供数据
        • 在后代组件中通过inject配置来声明接收数据
  3. slot 插槽:一个组件被多次复用,里面显示的内容结构大部分都一致,但是有部分内容有时显示列表,有时是图片,有时又是视频等,可以使用插槽来解决

    • 默认插槽:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      父组件中:
      <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>
    • 具名插槽

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      父组件中:
      <Category title="今日热门游戏">
      <template v-slot: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>
    • 作用域插槽:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。

      • 组件通过v-slot把数据传给调用者,但是组件本身没有权利决定这些数据如何展示,而是由调用者决定
      • 也可以具名
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      父组件中:
      <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>
  4. shallowRefshallowReactive:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的

    也就是说如果只关注是不是新的整体修改,就用这俩,不考虑内部的成员是不是响应式

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

  5. readonly

    • 作用:用于创建一个对象的深只读副本。

    • 用法:

      1
      2
      const original = reactive({ ... });
      const readOnlyCopy = readonly(original);
    • 特点:

      • 对象的所有嵌套属性都将变为只读。
      • 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
    • 应用场景:

      • 创建不可变的状态快照。
      • 保护全局状态或配置不被修改。
  6. shallowReadonly

    • 作用:与 readonly 类似,但只作用于对象的顶层属性。

    • 用法:

      1
      2
      const original = reactive({ ... });
      const shallowReadOnlyCopy = shallowReadonly(original);
    • 特点:

      • 只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。

      • 适用于只需保护对象顶层属性的场景。

  7. toRaw:用于获取一个响应式对象的原始对象, toRaw 返回的对象不再是响应式的,不会触发视图更新。

    官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

    何时使用? —— 在需要将响应式对象传递给非 Vue 的库或外部系统时,使用 toRaw 可以确保它们收到的是普通对象

  8. markRaw:作用:标记一个对象,使其永远不会变成响应式的。

    例如使用mockjs时,为了防止误把mockjs变为响应式对象,可以使用 markRaw 去标记mockjs

  9. customRef:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行逻辑控制,一般是写入 hooks 方便复用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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}
    }
  10. Teleport:一种能够将我们的组件html结构移动到指定位置的技术。

    1
    2
    3
    4
    5
    6
    7
    <teleport to='body' >
    <div class="modal" v-show="isShow">
    <h2>我是一个弹窗</h2>
    <p>我是弹窗中的一些内容</p>
    <button @click="isShow = false">关闭弹窗</button>
    </div>
    </teleport>
  11. suspense:等待异步组件时渲染一些额外内容,让应用有更好的用户体验

    • 使用步骤:

      • 异步引入组件

      • 使用Suspense包裹组件,并配置好defaultfallback

  12. 其他 API:

    • 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-ifv-for 在同一个元素身上使用时的优先级发生了变化。
    • 移除了$on$off$once 实例方法。
    • 移除了过滤器 filter
    • 移除了$children 实例 propert

Typescript 学习

  1. 一些JS要注意的点,比如 == 符号,还有类似1<x<3的表达式这种坑

  2. 推断类型,声明一个对象以后可以用 interface声明类型,然后在变量声明后使用类似 : TypeName 的语法来声明 JavaScript 对象符合新 interface 的形状,此外也支持面向对象

  3. 构建类型有两种语法,一种是interface,一种是type,常用这两种类型可以相互转化和扩展,但是interface支持多次定义,当多次定义的时候interface会自动合并这些成员类型,但是type不会

  4. 组合类型有联合和泛型,联合的意思如下:type MyBool = true | false;,就是用 | 符号连接,泛型有点类似cpp里面的 template

  5. 结构类型系统:TypeScript 的核心原则之一是类型检查侧重于值的形状。这有时称为 “duck typing” 或 “结构类型”。

    在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。例如一个对象定义的时候没有声明类型,但是如果其被调用的时候符合 interface 声明的格式,或者声明格式是该对象的一个子集,也能通过类型检查

  6. typescript支持静态类型检查以及检查非异常错误,比如拼写错误,undefine错误等

  7. 类型:string, boolean, number, array<type> or type[], any

    类型注释:const, let, var

    匿名函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const names = ["Alice", "Bob", "Eve"];

    // 即使参数 s 没有类型注释,TypeScript 还是使用 forEach 函数的类型以及推断的数组类型来确定 s 将具有的类型。这个过程称为上下文类型,因为函数发生的上下文告知它应该具有什么类型。
    names.forEach(function (s) {
    console.log(s.toUpperCase());
    });

    names.forEach((s) => {
    console.log(s.toUpperCase());
    });