文章目录

开篇总结

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

1.学习到的新点

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

2.学习的建议

  • 虽然之前通过各个方面资源学习vue,其中主要是通过官网和禹神的vue2视频学习,学习也简单,但是很重要的一点,就是要去多练,多接触,要不然很容易忘的。
  • 想学习更多的东西,看官网挺好的,当然听老师讲可能会对一个知识点豁然开朗。

1.创建 Vue3 工程

1.1 【基于 vue-cli 创建】

备注:现在官方推荐使用 create-vue 来创建基于 Vite 的新项目。

## 查看 @vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
vue -V
## 安装或 升级你的 @vue/cli
npm install -g @vue/cli
## 执行创建命令
vue create vue_test
## 随后选择 3.x
> Default ([Vue 3] babel, eslint)
  Default ([Vue 2] babel, eslint)

## 下面基于webpack搭建项目成功后执行下面的命令就是成功创建Vue3项目!!!
? Please pick a preset: Default ([Vue 3] babel, eslint)


Vue CLI v5.0.8
✨  Creating project in F:\桌面\vue_test.
🗃  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...


added 864 packages in 35s
🚀  Invoking generators...
📦  Installing additional dependencies...


added 103 packages in 7s
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project vue_test.
👉  Get started with the following commands:

 $ cd vue_test
 $ npm run serve

1.2 【基于 Vite 创建】(Vue 官方推荐)

## 创建命令
npm create vue@latest
## 一些可选项
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes

Scaffolding project in ./<your-project-name>...
Done.
## 在项目被创建后,通过以下步骤安装依赖并启动开发服务器
> cd <your-project-name>
> npm install
> npm run dev

总结:

  • Vite 项目中,index.html 是项目的入口文件,在项目的最外层。
  • 加载 index.html 后,Vite 解析 <script type="module" src="xxxxx"></script>指向的 JavaScript。
  • Vue3 中是通过 createApp 函数创建一个实例应用的。

1.3 【基于 Vite 官方 create-vite 创建】

## vite构建vue项目
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue

# yarn
yarn create vite my-vue-app --template vue

# pnpm
pnpm create vite my-vue-app --template vue

# bun
bunx create-vite my-vue-app --template vue

查看 create-vite 以获取每个模板的更多细节:vanilla,vanilla-ts, vue, vue-ts,react,react-ts,react-swc,react-swc-ts,preact,preact-ts,lit,lit-ts,svelte,svelte-ts,solid,solid-ts,qwik,qwik-ts

2.Vue3 语法核心

2.1 【拉开序幕的 setup】

setup 是 Vue3 中的一个新配置项,值是一个函数,它是 Composition API “表演的舞台”,组件中所用到的:数据、方法、计算属性、监视……等,均配置在 setup 当中。

特点如下

  • setup 函数返回的对象中的内容,可直接在模板中使用。
  • setup 中访问的 this 是 undefined。
  • setup 函数会在 beforeCreate 之前调用,它是“领先”所有钩子执行的。

setup 的返回值

//返回一个对象!!!
return {
  name,
  age,
  tel,
  changeName,
  changeAge,
  showTel,
};

//一个渲染函数
return () => "哈哈";

涉及到的面试问题

  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 的错误。

    const router = createRouter({
      history: createWebHistory(),
    });
    
  2. hash 模式(后台管理系统喜欢用!!!)

    优点:兼容性更好,因为不需要服务端处理路径。
    缺点:URl 带有#不太美观,且在 SEO 优化方面相对较差。

    const router = createRouter({
      history: createWebHashHistory(),
    });
    

3.4 路由传参

query 传参

传参

<!-- 第一种写法 -->
<RouterLink :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">主页</RouterLink>

<!-- 第二种写法 -->
<!-- 用 name 配合 query 也可以进行跳转 !!!-->
<RouterLink :to="{path:"/news/detail",query:{id:news.id,title:news.title,content:news.content}}">主页</RouterLink>

接收参数

import {useRoute} from "vue-router"
const route=useRoute()
//打印query参数
console.log(route.query)

parmas 传参

路由占位:

{
  name:'xinwen',
  path:'/news',
  component:Detail,
  children:[
    {
      name:'xiangqing',
      path:'detail/:id/:title/:content?',
      //在占位后面加一个 ? 表示参数可传可不传
      component:Detail
    }
  ]
}

路由传参:

<!-- 第一种写法 -->
<RouterLink :to="`/news/detail/${news.id}/${news.title}/${news.content}`">主页</RouterLink>

<!-- 第二种写法 -->
<!-- 注意:使用params传参 只能配合name进行,不要是path -->
<RouterLink :to="{name:"xiangqing",params:{id:news.id,title:news.title,content:news.content}}">主页</RouterLink>
<!-- 还有params是不能传数组的!一个不被推荐的数据类型 -->

注意:

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

  return { count, increment }
})

4.4【使用一个Store】

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()

// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>

为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
作为 action 的 increment 可以直接解构,getter 和 state 需要使用storeToRefs()

4.5 【State】

重置 state

使用选项式API 时,你可以通过调用 store 的 $reset() 方法将 state 重置为初始值。
在 $reset() 内部,会调用 state() 函数来创建一个新的状态对象,并用它替换当前状态。

const store = useStore()

store.$reset()

在 Setup Stores 中,您需要创建自己的 $reset() 方法:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  function $reset() {
    count.value = 0
  }

  return { count, $reset }
})

变更 state

除了用 store.count++ 直接改变 store,你还可以调用 $patch 方法。它允许你用一个 state 的补丁对象在同一时间更改多个属性:

store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})

不过,用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice 操作)都需要你创建一个新的集合。因此,$patch 方法也接受一个函数来组合这种难以用补丁对象实现的变更。

  store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

订阅 state

通过 store 的 $subscribe() 方法侦听 state 及其变化

const cartStore = useSomeStore()
cartStore.$subscribe((mutation, state) => {
  // import { MutationType } from 'pinia'
  mutation.type // 'direct' | 'patch object' | 'patch function'
  // 和 cartStore.$id 一样
  mutation.storeId // 'cart'
  // 只有 mutation.type === 'patch object'的情况下才可用
  mutation.payload // 传递给 cartStore.$patch() 的补丁对象。

  // 每当状态发生变化时,将整个 state 持久化到本地存储。
  localStorage.setItem('cart', JSON.stringify(state))
},{ detached: true })

默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离

可以使用watch监听整个 state

watch(
  pinia.state,
  (state) => {
    // 每当状态发生变化时,将整个 state 持久化到本地存储。
    localStorage.setItem('piniaState', JSON.stringify(state))
  },
  { deep: true }
)

4.6 【Getter】

getter 拿什么来计算?

  • getter 基于 state参数
  • 也可以通过 this 访问到整个 store 实例,这样可以访问到其他的getter
export const useStore = defineStore('main', {
  state: () => ({
    count: 0,
  }),
  getters: {
    // 自动推断出返回类型是一个 number
    doubleCount(state) {
      return state.count * 2
    },
    // 返回类型**必须**明确设置
    doublePlusOne(): number {
      // 整个 store 的 自动补全和类型标注 ✨
      return this.doubleCount + 1
    },
  },
})

向 getter 传递参数

Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数

export const useStore = defineStore('main', {
  getters: {
    getUserById: (state) => {
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})

并在组件中使用:

<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
// <script setup> 中的函数
</script>

<template>
  <p>User 2: {{ getUserById(2) }}</p>
</template>

4.7【Action】

订阅 action

通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行

这里有一个例子,在运行 action 之前以及 action resolve/reject 之后打印日志记录。

const unsubscribe = someStore.$onAction(
  ({
    name, // action 名称
    store, // store 实例,类似 `someStore`
    args, // 传递给 action 的参数数组
    after, // 在 action 返回或解决后的钩子
    onError, // action 抛出或拒绝的钩子
  }) => {
    // 为这个特定的 action 调用提供一个共享变量
    const startTime = Date.now()
    // 这将在执行 "store "的 action 之前触发。
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // 这将在 action 成功并完全运行后触发。
    // 它等待着任何返回的 promise
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // 如果 action 抛出或返回一个拒绝的 promise,这将触发
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// 手动删除监听器
unsubscribe()

继续搬砖
默认情况下,action 订阅器会被绑定到添加它们的组件上(如果 store 在组件的 setup() 内)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 true 作为第二个参数传递给 action 订阅器,以便将其从当前组件中分离:

<script setup>
const someStore = useSomeStore()
// 此订阅器即便在组件卸载之后仍会被保留
someStore.$onAction(callback, true)
</script>

5.组件通信

5.1. 【props】

概述:props是使用频率最高的一种通信方式,常用与 :父 ↔ 子

  • 父传子:属性值是非函数
  • 子传父:属性值是函数

子传父就是调用父组件的函数,带参数传过去的!!!

父组件:

<template>
  <div class="father">
    <h3>父组件,</h3>
		<h4>我的车:{{ car }}</h4>
		<h4>儿子给的玩具:{{ toy }}</h4>
		<Child :car="car" :getToy="getToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	// 数据
	const car = ref('奔驰')
	const toy = ref()
	// 方法
	function getToy(value:string){
		toy.value = value
	}
</script>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
		<h4>我的玩具:{{ toy }}</h4>
		<h4>父给我的车:{{ car }}</h4>
		<button @click="getToy(toy)">玩具给父亲</button>
  </div>
</template>

<script setup lang="ts" name="Child">
	import { ref } from "vue";
	const toy = ref('奥特曼')
	
	defineProps(['car','getToy'])
</script>

5.2 【mitt】

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。

安装mitt

npm i mitt

新建文件:src\utils\emitter.ts

// 引入mitt 
import mitt from "mitt";

// 创建emitter
const emitter = mitt()

/*
  // 绑定事件
  emitter.on('abc',(value)=>{
    console.log('abc事件被触发',value)
  })
  emitter.on('xyz',(value)=>{
    console.log('xyz事件被触发',value)
  })

  setInterval(() => {
    // 触发事件
    emitter.emit('abc',666)
    emitter.emit('xyz',777)
  }, 1000);

  setTimeout(() => {
    // 清理事件
    emitter.all.clear()
  }, 3000); 
*/

// 创建并暴露mitt
export default emitter

接收数据的组件中:绑定事件、同时在销毁前解绑事件:

import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 绑定事件
emitter.on('send-toy',(value)=>{
  console.log('send-toy事件被触发',value)
})

onUnmounted(()=>{
  // 解绑事件
  emitter.off('send-toy')
})

【第三步】:提供数据的组件,在合适的时候触发事件

import emitter from "@/utils/emitter";

function sendToy(){
  // 触发事件
  emitter.emit('send-toy',toy.value)
}

注意这个重要的内置关系,总线依赖着这个内置关系

5.3 【v-model】

原生input上使用v-model

首先,原生是这样用的:

<input v-model="data" />

其实在vue2/3的背后,上面的这个代码等价于下面这行代码:

<input
  :value="data"
  @input="data = $event.target.value"
/>

其实看到这里,应该有考虑过我还能用emit事件触发吗?不会啦,它只是对于它自己本身进行操作的,只是我们今天遇到的主要问题是输入组件上绑定的v-model,我们怎么通过组件里面的代码进行事件的触发。

在一个输入组件上使用v-model

先看组件上:

<CustomInput v-model="searchText" />

在背后实际上被展开成:

<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>
<!-- 或者: -->
<CustomInput
  :model-value="searchText"
  @update:model-value=" searchText = $event"
/>

看官网文档介绍的重点!!!

要让这个例子实际工作起来,<CustomInput> 组件内部需要做两件事:

  1. 将内部原生 <input> 元素的 value attribute 绑定到 modelValue prop
  2. 当原生的 input 事件触发时,触发一个携带了新值的 update:modelValue 自定义事

再看看官网给的例子,就知道父子组件关于数据传输的真实情况了。实际上在做这题也是这样的,我们在组件中触发父组件中的输入组件的update:modelValue事件,顺便将原生输入框的变化值带过去。

<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

$event 到底是啥?啥时候能.target

对于原生事件,$event就是事件对象===>能.target

对于自定义事件,$event就是触发事件时,传递的数据 ===> 不能.target

下面又是搬砖!!!

v-model 的参数​
默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。我们可以通过给 v-model 指定一个参数来更改这些名字:

<MyComponent v-model:title="bookTitle" />

在这个例子中,子组件应声明一个 title prop,并通过触发 update:title 事件更新父组件值:

<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

多个 v-model 绑定​
利用刚才在 v-model 参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model 双向绑定。

组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

5.4【$attrs 】

  1. 概述:$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。

  2. 具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。

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

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
		<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	let a = ref(1)
	let b = ref(2)
	let c = ref(3)
	let d = ref(4)

	function updateA(value){
		a.value = value
	}
</script>

子组件:

<template>
	<div class="child">
		<h3>子组件</h3>
		<GrandChild v-bind="$attrs"/>
	</div>
</template>

<script setup lang="ts" name="Child">
	import GrandChild from './GrandChild.vue'
</script>

孙组件:

<template>
	<div class="grand-child">
		<h3>孙组件</h3>
		<h4>a:{{ a }}</h4>
		<h4>b:{{ b }}</h4>
		<h4>c:{{ c }}</h4>
		<h4>d:{{ d }}</h4>
		<h4>x:{{ x }}</h4>
		<h4>y:{{ y }}</h4>
		<button @click="updateA(666)">点我更新A</button>
	</div>
</template>

<script setup lang="ts" name="GrandChild">
	defineProps(['a','b','c','d','x','y','updateA'])
</script>

5.5 【$refs、$parent】

  1. 概述:

    • $refs用于 :父→子。
    • $parent用于:子→父。
  2. 原理如下:

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

5.6 【provide、inject】

  1. 概述:实现祖孙组件直接通信

  2. 具体使用:

    • 在祖先组件中通过provide配置向后代组件提供数据
    • 在后代组件中通过inject配置来声明接收数据
  3. 具体编码:

    【第一步】父组件中,使用provide提供数据

    <template>
      <div class="father">
        <h3>父组件</h3>
        <h4>资产:{{ money }}</h4>
        <h4>汽车:{{ car }}</h4>
        <button @click="money += 1">资产+1</button>
        <button @click="car.price += 1">汽车价格+1</button>
        <Child/>
      </div>
    </template>
    
    <script setup lang="ts" name="Father">
      import Child from './Child.vue'
      import { ref,reactive,provide } from "vue";
      // 数据
      let money = ref(100)
      let car = reactive({
        brand:'奔驰',
        price:100
      })
      // 用于更新money的方法
      function updateMoney(value:number){
        money.value += value
      }
      // 提供数据
      provide('moneyContext',{money,updateMoney})
      provide('car',car)
    </script>
    

    注意:子组件中不用编写任何东西,是不受到任何打扰的

    【第二步】孙组件中使用inject配置项接受数据。

    <template>
      <div class="grand-child">
        <h3>我是孙组件</h3>
        <h4>资产:{{ money }}</h4>
        <h4>汽车:{{ car }}</h4>
        <button @click="updateMoney(6)">点我</button>
      </div>
    </template>
    
    <script setup lang="ts" name="GrandChild">
      import { inject } from 'vue';
      // 注入数据
     let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
      let car = inject('car')
    
```

5.7 【slot】

1. 默认插槽

父组件中:
        <Category title="今日热门游戏">
          <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ul>
        </Category>
子组件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <!-- 默认插槽 -->
            <slot></slot>
          </div>
        </template>

2. 具名插槽

父组件中:
        <Category title="今日热门游戏">
          <template v-slot:s1><!-- 也可以是 #s1 -->
            <ul>
              <li v-for="g in games" :key="g.id">{{ g.name }}</li>
            </ul>
          </template>
          <template #s2>
            <a href="">更多</a>
          </template>
        </Category>
子组件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <slot name="s1"></slot>
            <slot name="s2"></slot>
          </div>
        </template>

3. 作用域插槽

  1. 理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在News组件中,但使用数据所遍历出来的结构由App组件决定)

  2. 具体编码:

    父组件中:
          <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. 用法:

    let myVar = shallowRef(initialValue);
    
  3. 特点:只跟踪引用值的变化,不关心值内部的属性变化。

shallowReactive

  1. 作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的

  2. 用法:

    const myObj = shallowReactive({ ... });
    
  3. 特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。

总结

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

6.2【readonly 与 shallowReadonly】

readonly

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

  2. 用法:

    const original = reactive({ ... });
    const readOnlyCopy = readonly(original);
    
  3. 特点:

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

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

shallowReadonly

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

  2. 用法:

    const original = reactive({ ... });
    const shallowReadOnlyCopy = shallowReadonly(original);
    
  3. 特点:

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

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

6.3【toRaw 与 markRaw】

toRaw

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

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

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

  2. 具体编码:

    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. 编码:

    /* markRaw */
    let citys = markRaw([
      {id:'asdda01',name:'北京'},
      {id:'asdda02',name:'上海'},
      {id:'asdda03',name:'天津'},
      {id:'asdda04',name:'重庆'}
    ])
    // 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
    let citys2 = reactive(citys)
    

6.4【customRef】

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

实现防抖效果(useSumRef.ts):

import {customRef } from "vue";

export default function(initValue:string,delay:number){
  let msg = customRef((track,trigger)=>{
    let timer:number
    return {
      get(){
        track() // 告诉Vue数据msg很重要,要对msg持续关注,一旦变化就更新
        return initValue
      },
      set(value){
        clearTimeout(timer)
        timer = setTimeout(() => {
          initValue = value
          trigger() //通知Vue数据msg变化了
        }, delay);
      }
    }
  }) 
  return {msg}
}

7.Vue3新组件

7.1 【Teleport】

  • 什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。
<teleport to='body' >
    <div class="modal" v-show="isShow">
      <h2>我是一个弹窗</h2>
      <p>我是弹窗中的一些内容</p>
      <button @click="isShow = false">关闭弹窗</button>
    </div>
</teleport>

7.2 【Suspense】

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
  • 使用步骤:
    • 异步引入组件
    • 使用Suspense包裹组件,并配置好defaultfallback
import { defineAsyncComponent,Suspense } from "vue";
const Child = defineAsyncComponent(()=>import('./Child.vue'))
<template>
    <div class="app">
        <h3>我是App组件</h3>
        <Suspense>
          <template v-slot:default>
            <Child/>
          </template>
          <template v-slot:fallback>
            <h3>加载中.......</h3>
          </template>
        </Suspense>
    </div>
</template>

7.3【全局API转移到应用对象】

  • app.component
  • app.config
  • app.directive
  • app.mount
  • app.unmount
  • app.use

7.4【其他】

  • 过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from

  • keyCode 作为 v-on 修饰符的支持。

  • v-model 指令在组件上的使用已经被重新设计,替换掉了 v-bind.sync。

  • v-ifv-for 在同一个元素身上使用时的优先级发生了变化。

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

  • 移除了过滤器 filter

  • 移除了$children 实例 propert

Logo

前往低代码交流专区

更多推荐