跟着禹神学Vue3

亮子 2024-04-10 01:05:23 28742 0 0 0

[TOC]

开篇总结

首先声明:这篇笔记中是在学习禹神最新Vue3教程记下来的,其中有一些自己写的笔记见解、搬禹神、vue、vite官网的一些笔记。主要目的还是给自己多一点知识点的总结。其实一遍下来,基本大部分都会,所以学习会很快。有几点我得好好在下面总结。

1.学习到的新点

  • props和provide传值可以通过传函数给下面的组件,组件调用后传参可以更改父元素。
  • 一个reactive响应式对象中包含有ref类型数据,通过reactive可以直接获取数据,有解包的过程
  • 新了解的mitt进行组件间的通信
  • 禹神yyds,作用域插槽每次看到都会有不一样的见解,但是不用容易忘,这次讲的例子很让人记忆深刻
  • position:fixed定位入口父元素中有filter CSS属性,也会和position:relative;一样。

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 () => "哈哈";

涉及到的面试问题

  1. (data、methods……) 和 setup 能不能同时存在?
    能,可以在一个 Vue 3 组件中同时使用 Options API 和 Composition API。但是,它们的使用场景是不同的。一般来说,如果你想要定义一些基础的响应式数据和钩子函数,使用 Options API 就足够了。而当你需要更复杂的逻辑或希望更好地组织你的代码时,Composition API 会是一个更好的选择。
  2. 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>

和上面这样写可能有点麻烦了!我们可以进行如下配置:

  1. 我们可以借助一个插件
npm i vite-plugin-vue-setup-extend -D
  1. 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)),
    },
  },
});
  1. 在 script 标签上使用
<script lang="ts" setup name="Person456">
  ...
</script>

2.2 ref 和 reactive 的学习

ref ==============> 可以定义:基本类型、对象类型的响应式数据
reactive ===========> 只能定义:对象类型是响应式数据

ref 对比 reactive
宏观角度:

  1. ref 用来定义:基本数据类型、对象数据类型
  2. reactive 用来定义:对象数据类型

区别:

  1. ref 创建的变量必须使用 .value (可以使用 volar 插件自动添加 .value)
  2. reactive 重新分配一个新对象,会失去响应式 (可以使用 Object.assign 去整体替换)

使用原则:

  1. 若需要一个基本类型的响应式数据,必须使用 ref
  2. 若需要一个响应式对象,层级不深,ref、reactive 都可以
  3. 若需要一个响应式对象,且层级较深,推荐使用 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 只能监视以下四种数据:
    > 1. ref 定义的数据
    > 2. reactive 定义的数据
    > 3. 函数返回一个值(getter 函数)
    > 4. 一个包含上述内容的数组

*情况一:监视【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 定义的【对象类型】数据中的 某个属性

注意点:

  1. 若该属性值不是【对象类型】,需要写成函数形式
  2. 若该属性值依然是【对象类型】,可以直接编,也可以写成函数,还是建议写成函数!

结论:监视的要是对象里面的属性,那么最好写函数式。注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视!

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
    > 1. 都能监视响应式数据的变化,不同的是监听数据变化的方式不同
    > 2. watch:需要指明出监视的数据
    > 3. 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 两个注意点

  1. 路由组件通常存放在 pages 或者 views 文件夹中,一般组件通常存放在 components 文件夹。
  2. 通过点击导航,视觉效果上“消失了”的路由组件,默认是被销毁掉的,需要的时候再去挂载。

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 路由器的工作模式

  1. history 模式

> 优点:URL 更加美观,不带有#,更接近于传统的网站 URL。
> 缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有 404 的错误。
>
> js > const router = createRouter({ > history: createWebHistory(), > }); >

  1. hash 模式(后台管理系统喜欢用!!!)
    > 优点:兼容性更好,因为不需要服务端处理路径。
    > 缺点:URl 带有#不太美观,且在 SEO 优化方面相对较差。
    >
    > js > 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是不能传数组的!一个不被推荐的数据类型 -->

注意:

  1. 传递params参数时,若使用to的对象写法,必须使用name配置项,不能使用path。
  2. 传递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属性】

  1. 作用:控制路由跳转时操作浏览器历史记录的模式
  2. 浏览器的历史记录有两种写入方式:分别为push、replace:
    • push 是追加历史记录(默认)
    • replace 是替换当前记录
  3. 开启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 状态管理库

Pinia官网

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
    ```js
    export const useCounterStore = defineStore(‘counter’, () => {
    const count = ref(0)
    function increment() {
    count.value++
    }

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>

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-ifv-for 在同一个元素身上使用时的优先级发生了变化。

  • 移除了$on$off$once 实例方法。

  • 移除了过滤器 filter

  • 移除了$children 实例 propert

……