初面网初面网

学习VUE3

setup与data等能否共存

可以共存,旧的能读新的,但新的读不到旧的

ref、reactive

ref定义基本类型数据、对象类型数据。数据会变成refimpl。定义对象类型数据时内部会使用reactive。

reactive定义对象类型数据。会把数据包装成Proxy。当对象层级较深的时候,更推荐使用reactive。

在书写代码区域,必须使用.value来获取值。这个在vscode中可以通过设置volar插件的配置项来实现。在vscode设置中,搜索dot value,勾选上。

另外,reactive定义的变量不能改变对象本身,会失去响应式。如果想修改,使用Object.assign(原对象,新对象)

let originObj = reactive({name:'tom', age:18});
originObj = {name: 'Joy', age: 28}  // 失去响应式

toRefs、toRef

假设有一个响应式对象或基本类型数据,想直接对内部或值修改,同时保留响应式,需要用到这两个。toRefs自然是用于多值时。

toRefs是将对象中的值全部取出,toRef则需要指定所需的值。

let a = reactive({name: 'Tom', age: 18});
let num = 18;

let {name,age} = toRefs(a); // 这样,新赋值的name和age就变成了响应式
let newNum = toRef(a, num);

计算属性

可以看做是变量,跟其他变量用法相同。

<template>
  <div class="index">
    原始:{{ age }}
    结果: {{ result }}
    结果2: {{ result2 }}
  </div>
  <button @click="changeAge">修改年龄</button>
</template>
<script lang="ts" setup>
  import {ref, reactive, computed} from 'vue'

  let age = ref(18);

  const result = computed(()=>{
    return age.value + 1;
  })

  const result2 = computed({
    set(val){
      age.value = 24;
    },
    get(){
      return age.value + 2
    }
  });

  const changeAge = () => {
    result2.value = 23;
  };
  
</script>

watch

监视数据变化。只能监视四种数据:

  • ref定义的数据
  • reactive定义的数据
  • 函数返回一个值
  • 一个包含上述内容的数组

watchEffect

props

interface aType{
  name: string;
}

// 普通接收
defineProps(['a'])

// 限制类型
defineProps<{a:aType}>()

// 限制必要性并指定默认值
withDefaults(defineProps<{a: aType}>(), {
  a: ()=>({name: 'Tom'})
})

生命周期

vue2里有八个,对应四阶段:创建、挂载、更新、销毁

beforeCreate/created

beforeMounte/mounted

beforeUpdate/updated

beforeDestroyed/destroyed

vue3里有六个,仍旧对应四阶段

在setup中直接默认创建

挂载 ( 挂载前 onBeforeMount, 挂载完毕 onMounted )

更新 ( 更新前 onBeforeUpdate, 更新完毕 onUpdated )

销毁 ( 卸载前 onBeforeUnmount, 卸载完毕 onUnmounted )

hooks

命名以use开头。单独作为文件时是.js或.ts

实际上,只要做成一个use开头的函数,基本上都能被认为是一个钩子。

const useAdd = ()=>{
  let baseNum = 0;

  const addNum = () => {
    baseNum = baseNum + 1;
  }
  return {baseNum, addNum}
}

const {baseNum, addNum} = useAdd(); // 解构名要与钩子内部变量、函数的命名一致

钩子里也能使用vue里的ref等内容,也可以使用生命周期函数。

路由

如果没安装过,执行代码安装

npm

npm i vue-router

yarn

yarn add vue-router

安装好后配置一下路由文件和加载环境

src下新建router文件夹,新建index.ts文件

import { createRouter, createWebHistory } from "vue-router";

import Home from '@/components/Home.vue'
import News from '@/components/News.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/news', component: News },
  ]
});

export default router;

main.ts中使用router

import { createApp } from 'vue'
import App from './App.vue'

import router from './router/index'

createApp(App).use(router).mount("#app");

找个vue文件,写入跳转和展示

// 用active-class,会自动识别哪个按钮被激活了
<template>
  <div class="index">
    <RouterLink to="/" active-class="active">首页</RouterLink>
    <RouterLink to="/news" active-class="active">新闻</RouterLink>
    <RouterView />
  </div>
</template>
<script lang="ts" setup>
  import {RouterView, RouterLink} from 'vue-router'
  import Person from '@/components/Person.vue'
</script>
<style>
a.active {
  color: green;
}
</style>

路由另一种写法

import { createRouter, createWebHistory } from "vue-router";

import News from '@/components/News.vue'
import News2 from '@/components/News2.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { name: 'contact', path: '/contact', component: News1 },
    { name: 'name-news', path: '/news', component: News2 },
  ]
});

export default router;

链接里可以这样写

  <RouterLink :to="{name:'name-news'}" active-class="active">新闻</RouterLink>

路由传参--query

// 写法一
<RouterLink to="/user?uid=1516">

// 写法二
<RouterLink :to="{
  path: '/user',
  query: {
    uid: 1516
  }
}">

接收参数的vue文件里要使用useRoute

import {useRoute} from 'vue-router'
const route = useRoute()
console.log(route.query)

路由传参--params

使用params传递参数,to的对象写法里,必须使用name而不是path

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // 路径中添加冒号变量名作为占位
    // 问号代表可选,即可有可无
    { name: 'name-news', path: '/news/:id?', component: News2 },
  ]
});

接收参数的vue文件里要使用useRoute

import {useRoute} from 'vue-router'
const route = useRoute()
console.log(route.params)

路由工作模式

路由有两种工作模式:history模式,hash模式

history模式的URL更美观,但上线后需要服务端配合处理路径,否则刷新会404

hash模式兼容性好,但带有#不美观,在SEO优化方面相对较差

Pinia

Vue3推荐的集中式状态管理。没有安装过要先安装

npm i pinia

在main.ts中配置

import { createApp } from 'vue'
import App from './App.vue'

import { createPinia } from 'pinia';

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.mount('#app');

所有的状态管理,最好都存放在src下的store文件夹中。这是通用标准。

下面是一个案例:

// store中的文件count.ts
import { defineStore } from "pinia";

export const useCountStore = defineStore('count', {
  actions:{
    add(value: number){
      this.sum += value
    }
  },
  state() {
      return {
        sum: 6
      }
  }
})

// .vue文件中使用
<template>
  <div>{{ storeCount.sum }}</div>
  <button @click="addCount">加</button>
</template>

<script setup lang="ts" name="Home">
  import { useCountStore } from "@/store/count";

  const storeCount = useCountStore();

  const addCount = () => {
    // 修改方法一
    // storeCount.sum += 1;

    // 修改方法二
    //storeCount.$patch({
    //  sum: storeCount.sum + 1
    //})

    // 修改方法三

    storeCount.add(3)
  }
</script>

pinia--storeToRefs

如果想直接使用store中的变量,而不再用store.值的形式,可以用storeToRefs解构出变量。

<template>
  <div>{{ sum }}</div>
  <button @click="addCount">加</button>
</template>

<script setup lang="ts" name="Home">
  // 引入
  import { storeToRefs } from "pinia";
  import { useCountStore } from "@/store/count";

  const storeCount = useCountStore();
  const {sum} = storeToRefs(storeCount);  // 响应

  const addCount = () => {
    sum.value += 1;
  }
</script>

pinia--getters

在pinia中,也可以不借助action,对数据直接进行处理

import { defineStore } from "pinia";

export const useCountStore = defineStore('count', {
  state() {
      return {
        sum: 6
      }
  },
  getters:{
    bigSum: state => state.sum * 3
  }
})

.vue文件中的使用

<template>
  <div>{{ bigSum }}</div>
</template>

<script setup lang="ts" name="Home">
  import { storeToRefs } from "pinia";
  import { useCountStore } from "@/store/count";

  const storeCount = useCountStore();
  const {bigSum} = storeToRefs(storeCount);
</script>

pinia--$subscribe

监听。切记,监听的是store变量,不是其中的值。

import { useCountStore } from "@/store/count";

const storeCount = useCountStore();

storeCount.$subscribe((mutate, state)=>{
  console.log(mutate)
  console.log(state)
})

pinia--组合式写法

以上都是选项式写法。组合式写法如下

import { defineStore } from "pinia";

import {ref} from 'vue';
export const useCountStore = defineStore('count', ()=>{
  let sum = ref(0);

  const addCount = () => {
    sum.value = sum.value + 1;
  }

  return {sum, addCount}
})

.vue文件里使用

<template>
  <div>{{ sum }}</div>
  <button @click="storeCount.addCount">加</button>
</template>

<script setup lang="ts" name="Home">
  import { storeToRefs } from "pinia";
  import { useCountStore } from "@/store/count";

  const storeCount = useCountStore();

  // 注意这里, 由于storeToRefs, 所以下面解构方法会报错
  const {sum} = storeToRefs(storeCount);
</script>

组件通信---props

之前写过,这里再简略写一次。

// father.vue
<template>
  <div class="index">
    <Child :name="name" />
  </div>
</template>
<script lang="ts" setup>
  import Child from '@/components/Child.vue'

  let name = 'Tom'
</script>

// Child.vue
<template>
  <div>{{ name }}</div>
</template>

<script setup lang="ts" name="Child">
  const {name} = defineProps(['name']);
</script>

组件通信---自定义事件

官方推荐:自定义事件名如果由多个单词组成,则单词以短横杠连接。例如:check-ticket

关键词:emit

// father.vue
<template>
  <div class="index">
    {/* 这里用@自定义了一个changeName事件 */}
    <Child :name="name" @changeName="changeName" />
  </div>
</template>
<script lang="ts" setup>
  import {ref} from 'vue';
  import Child from '@/components/Child.vue'

  let name = ref('Tom')

  const changeName = () => {
    name.value = 'John'
  }

// Child.vue
<template>
  <div>{{ name }}</div>
  {/* $emit用来调用事件 */}
  <button @click="emit('changeName')">改名</button>
</template>

<script setup lang="ts" name="Child">
  import { onMounted } from 'vue';
  const {name} = defineProps(['name']);
  const emit = defineEmits(['changeName'])

  {/* 代码块中不需要$符号也能用 */}
  {/* onMounted(()=>{
    emit('changeName');
  }) */}
</script>

带参写法:

// father.vue
const changeName = (value: string) => {
  name.value = value
}

// Child.vue
<button @click="emit('changeName', 'John')">改名</button>

组件通信---mitt

mitt相当于第三方。就是组件把事情都交给第三方处理,达成通信。

npm i --save mitt

mitt要全局唯一,不要生成多个实例。

在src下创建tool或utils来存放工具文件。

// utils/emitter
import mitt from "mitt";

const emitter = mitt();

export default emitter;

在vue文件中

// 组件1
import emitter from '@/utils/emitter';

const toAdd = () => {
  emitter.emit('numAdd');
}

// 组件2
import emitter from '@/utils/emitter';
import { onUnmounted } from 'vue';

emitter.on('numAdd', () => {
  console.log('add');
});

// 一定要记得卸载,否则占用内存
onUnmounted(()=>{
  emitter.off('numAdd');
});

组件通信---v-model

一般用法。v-model是语法糖,本质上是value和input的组合。

<template>
  <div>
    <input v-model="name" />
    <input :value="name" @input="name=(<HTMLInputElement>$event.target).value" />
  </div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
let name = ref('');

watch(name, ()=>{
  console.log(name.value)
})
</script>

多组件中也可以直接用v-model,还可以给v-model命名。

// 组件1
<template>
  <div class="index">
    <Child v-model:name="name" v-model:psw="psw"></Child>
  </div>
</template>
<script lang="ts" setup>
import Child from '@/components/Child.vue';
import { ref, watch } from 'vue';
let name = ref('');
let psw = ref('');

watch(name, () =>{
  console.log(name.value)
})

watch(psw, () =>{
  console.log(psw.value)
})
</script>

// 组件2
<template>
  <input :value="name" @input="emit('update:name', (<HTMLInputElement>$event.target).value)" />
  <input :value="psw" @input="emit('update:psw', (<HTMLInputElement>$event.target).value)" />
</template>

<script setup lang="ts" name="Child">
  defineProps(['name', 'psw']);
  const emit = defineEmits(['update:name','update:psw']);
</script>

组件通信---

Slot---默认插槽

我们之前用组件标签都是自闭合标签。实际上可以在标签内放置其他内容。

<template>
  <div class="index">
    <Child>
      <div>
        插槽内容
      </div>
    </Child>
  </div>
</template>

如果想让子组件中知道这部分包裹内容要放在哪里,需要一个标记,这个标记就是slot。

<template>
  <!-- <slot></slot> -->
  <slot>默认内容</slot>
</template>

slot是承接内容的区域。如果没有内容传过来,且slot中不写内容,那么就是空。

没有内容传过来但是写了默认展示的内容,则展示默认内容。

多个插槽标签,意味着有多个展示位。

Slot---具名插槽

父组件中有很多内容,想指定放置到的插槽位置,此时需要具名插槽。

未命名的插槽也是有名字的,叫undefined

<template>
  <div class="index">
    // 注意,用的是冒号
    <Child v-slot:slot1>  
      <div>
        插槽内容
      </div>
    </Child>
  </div>
</template>

<template>
  <slot name="slot1">默认内容</slot>
</template>

Slot---作用域插槽

子类插槽中的数据可以被父类获取

// father.vue
<template>
  <div class="index">
    <Child v-slot="{text}">{{ text }}</Child>
  </div>
</template>
<script lang="ts" setup>
import Child from '@/components/Child.vue';
</script>

// Child.vue
<template>
  <slot :text="name"></slot>
</template>

<script setup lang="ts" name="Child">
  import { ref } from 'vue';

  let name = ref('child');
</script>

shallowRef和shallowReactive

只处理第一层的数据,对深层的不做处理。在需要只处理第一层的时候,效率会比ref和reactive要好。

readonly和shallowReadonly

readonly和shallowReadonly都是函数,内部可以接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。

原值改变时,只读代理的值也会改变。

只有shallowReadonly返回的只读代理的深层内容才能够改变。

  import { reactive, readonly, shallowReadonly } from 'vue';

  let human = reactive({
    name: 'tom',
    age: 18,
    hobby: {
      hobby1: 'ball',
      hobby2: 'money',
      hideHobby: {
        h1: 'dance',
      }
    }
  });

  let humanConst = readonly(human);
  let humanConst2 = shallowReadonly(human);
  // humanConst.age = 20;     // age无法修改
  // humanConst2.hobby['hoby3'] = 'piano';  // 无法找到hoby3
  humanConst2.hobby.hideHobby['h1'] = 'piano';

toRaw和markRaw

toRaw函数能把一个响应式的值转为非响应式的值。

markRaw函数能把一个本身非响应式的值,永远无法成为响应式。

customRef

可以更细化地操作响应式。


更新于 2025/2/26