Vue3

Vue3介绍

vue3

本文为 Vue3 快速上手介绍(示例基于setup语法糖,帮助新手快速认识 Vue3),需要一定的 Vue2 基础,深度学习还需大量的练习与查阅官方文档。路还很长,愿每一位读到本文的朋友能够只争朝夕,不负韶华!(Vue2基础入门详细版

Vue3 于 2020 年 9 月 18 日发布,2022 年 2 月 7 日开始,Vue3 被设为默认版本。Vue3 在经过一年的迭代后,越来越好用,毫无疑问,vue3 是现在也是未来!

库名称 简介
ant-design-vue PC 端组件库:Ant Design 的 Vue 实现,开发和服务于企业级后台产品
arco-design-vue PC 端组件库:字节跳动出品的企业级设计系统
element-plus PC 端组件库:基于 Vue 3,面向设计师和开发者的组件库
Naive UI PC 端组件库:一个 Vue 3 组件库,比较完整,主题可调,使用 TypeScript,快
vant 移动端组件库:一个 轻量、可靠的移动端组件库,于 2017 年开源
VueUse 基于 composition 组合式 api 的常用函数集合(炸裂推荐、极其好用!)

相关文档

  1. Vue3 中文文档(新) cn.vuejs.org/
  2. Vue2 中文文档(旧) v2.cn.vuejs.org/
  3. Vue3 设计理念 vue3js.cn/vue-composi...

Vue3 框架优点特点

  1. 首次渲染更快
  2. diff 算法更快
  3. 内存占用更少
  4. 打包体积更小
  5. 更好的 Typescript 支持
  6. Composition API 组合 API

学习 vue3 主要学习的就是 组合式API 的使用

vite 构建工具

Vite是一个现代化的前端构建工具,旨在提供开发环境的快速启动和快速重载。它以轻量、简单和快速为设计原则,具有快速的冷启动热模块替换能力

对比 webpack:

  • 基于打包器的方式启动,必须优先抓取并构建你的整个应用,然后才能提供服务
  • 更新速度会随着应用体积增长而直线下降

vite 的原理:

  • 使用原生 ESModule 通过 script 标签动态导入,访问页面的时候加载到对应模块编译并响应
  • Vite 只需要在浏览器请求源码时进行转换并按需提供源码
  • 根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理

vite 创建项目

运行创建项目命令:

bash 复制代码
# 使用npm
npm create vite@latest
# 使用yarn
yarn create vite
# 使用pnpm
pnpm create vite

使用 Vue3 时,需要安装 volar 插件,并且禁用 vetur

Vue 项目创建指令(全):

bash 复制代码
# vue-cli
vue create project-name
# 空项目(vite)
pnpm create vite
# 全部自动配置(vite)
pnpm create vue

Vue3 不同点:

  • 组件一个根节点非必需

  • 创建应用挂载到根容器

  • 入口页面,ESM 加载资源

平常组件

html 复制代码
<template>
  <div>节点1</div>
  <div>节点2</div>
</template>

main.js

JavaScript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
// 根据App组件创建一个应用实例
const app = createApp(App)
// app应用挂载(管理)index.html的 #app 容器
app.mount('#app')
// vue3 中是使用 createApp() 管理容器,不是 new Vue()

index.html

html 复制代码
<div id="app"></div>
<script type="module" src="/src/main.js"></script>

Vue3.0中对这些API做出了调整:

  • 将全局的API,即:Vue.xxx调整 到应用实例(app)上

    Vue2全局 API(Vue Vue3 实例 API (app)
    Vue.config.xxxx app.config.xxxx
    Vue.config.productionTip 移除
    Vue.component app.component
    Vue.directive app.directive
    Vue.mixin app.mixin
    Vue.use app.use
    Vue.prototype app.config.globalProperties

Composition API(核心)

组合式API概念

组合式 API 是 Vue 3 中引入的一项新特性,它旨在提供一种更灵活和强大的方式来组织和复用组件逻辑。传统的 Vue 组件通常基于选项 API,而组合式 API 则基于函数式的组合思想

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与<script setup> 搭配使用。这个 setup 属性是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用

选项式 API (Options API)

html 复制代码
<script>
export default {
  // data() 返回的属性将会成为响应式的状态
  // 并且暴露在 this 上
  data() {
    return {
      count: 0
    }
  },

  // methods 是一些用来更改状态与触发更新的函数
  // 它们可以在模板中作为事件处理器绑定
  methods: {
    increment() {
      this.count++
    }
  },

  // 生命周期钩子会在组件生命周期的各个不同阶段被调用
  // 例如这个函数就会在组件挂载完成后被调用
  mounted() {
    console.log(`The initial count is ${this.count}.`)
  }
}
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

下面是使用了组合式 API 与 <script setup> 改造后和上面的模板完全一样的组件:

html 复制代码
<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

在 setup 中通过 vue 提供的函数组织代码实现功能,就是组合式API写法。 组合式API可复用,可维护

setup函数

setup函数是组合式API的入口函数

  • setup 函数是 Vue3 特有的选项,作为组合式API的起点
  • 从组件生命周期看,它在 beforeCreate 之前执行
  • 函数中 this 不是组件实例,是 undefined
  • 如果数据或者函数在模板中使用,需要在 setup 返回
html 复制代码
<template>
  <div class="container">
    <h1 @click="say()">{{msg}}</h1>
  </div>
</template>

<script>
export default {
  setup () {
    console.log('setup执行了')
    console.log(this)
    // 定义数据和函数
    const msg = 'hello vue3!'
    const say = () => {
      console.log(msg)
    }
    // 返回给模板使用
    return { msg , say}
  },
  beforeCreate() {
    console.log('beforeCreate执行了')
  },
  created() {
    console.log('created执行了')
  }
  // ...
}
</script>

注意:

尽量不要与 Vue2 配置混用

  • Vue2配置(data、methos、computed...)中可以访问到setup中的属性、方法
  • 但setup中不能访问到Vue2配置(data、methos、computed...)
  • 如果有重名,setup优先级更高(同名属性,data中的数据优先级低于setup中的数据)

setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性

setup的参数

  • props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性
  • context:上下文对象
    • attrs:值为对象,包含:组件外部传递过来,但没有在 props 配置中声明的属性,相当于 this.$attrs
    • slots:收到的插槽内容,相当于 this.$slots
    • emit:分发自定义事件的函数,相当于 this.$emit
html 复制代码
<script>
  import { reactive } from 'vue'
  export default {
  	props: ['msg','school'],
  	emits: ['hello'],  //自定义事件需声明
  	setup(props, context){
  	  console.log(props, context)
  	  console.log(context.attrs) //相当与Vue2中的$attrs
  	  console.log(context.emit) //触发自定义事件
  	  console.log(context.slots) //插槽
  	  //数据
  	  let person = reactive({
  	  	name:'张三',
  	  	age:18
  	  })
  	  //方法
  	  function test(){
  	  	context.emit('hello',666)
  	  }
  	  //返回一个对象(常用)
  	  return {
  	  	person,
  	  	test
  	  }
  	}
  }
</script>

setup语法糖

使用 setup 语法糖后的页面结构

html 复制代码
<template>
  <div>
  </div>
</template>

<script setup>
</script>

<style lang="scss" scoped>
</style>

<script setup> 中的顶层变量都可以在模板使用,包括数据,函数,组件

reactive

通常使用它定义对象类型响应式数据(在 vue3 中,直接定义的数据不是响应式的)

  • 作用:定义一个对象类型的响应式数据
  • 语法:const 代理对象= reactive(源对象)接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)
  • reactive定义的响应式数据是深层次的
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作
html 复制代码
<template>
  <div>
    <p>姓名:{{state.name}}</p>
    <p>年龄:{{state.age}} <button @click="state.age++">age++</button></p>
  </div>
</template>

<script>
// 1. 导入函数
import { reactive } from "vue"; 
export default {
  setup() {
    // 2. 创建响应式数据对象
    const state = reactive({ name: 'jack', age: 18 })
    // 3. 返回数据,供模板使用
    return { state }
  }
};
</script>

需要注意的是:reactive不能转换简单数据类型的数据

ref

通常使用它定义响应式数据,不限类型

  • vue 中导出 ref 函数
  • setup 函数中,使用 ref 函数,传入普通数据,返回一个响应式数据。最后 setup 函数返回一个对象,包含该响应式数据即可
  • 注意:使用 ref 创建的数据,js 中需要 .valuetemplate 中可省略
html 复制代码
<template>
  <div>
      <!-- template中使用可省略.value -->
      计数器:{{ count }}
      <button @click="count++">累加1</button>
      <button @click="increase">累加10</button>
  </div>
</template>

<script>
// 1. 导入函数
import { ref } from "vue";
export default {
  setup() {
    // 2. 创建响应式数据对象
    const count = ref(0);
    const increase = () => {
      // js中使用需要.value
      count.value += 10;
    };
    // 3. 返回数据
    return { count, increment };
  }
};
</script>

refreactive区别:

  • 从定义数据角度对比:
    • ref用来定义基本类型数据
    • reactive用来定义引用类型数据,不支持简单数据类型
    • 此外,ref也可以用来定义引用类型数据 ,它内部会自动通过reactive转为代理对象
  • 从原理角度对比:
    • ref通过Object.defineProperty()getset来实现响应式(数据劫持)
    • reactive通过使用Proxy 来实现响应式(数据劫持), 并通过Reflect 操作源对象内部的数据
  • 从使用角度对比:
    • ref定义的数据:操作数据需要 .value,读取数据时模板中直接读取不需要 .value
    • reactive定义的数据:操作数据与读取数据:均不需要 .value

ref获取DOM

元素上使用 ref 属性关联响应式数据,获取 DOM 元素

html 复制代码
<template>  
  <div>  
    <input ref="myInput" type="text">  
  </div>  
</template>  
  
<script setup>
import { ref, nextTick } from 'vue';  
  // 创建一个ref来存储输入框的引用  
  const myInput = ref(null);  
  // 使用nextTick确保DOM更新完成后再访问引用的值  
  nextTick( () => {  
    console.log(myInput.value); // 输出输入框的DOM元素  
  });  
</script>

默认值是null,需要在渲染完毕后访问DOM属性

Vue2响应式原理

实现原理:

  • 对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)

  • 数组类型:通过重写更新数组的一系列方法来实现拦截(pop、shift、push、unshift、splice、reverse、sort)

    js 复制代码
    Object.defineProperty(data, 'count', {
        get () {}, 
        set () {}
    })

存在问题:

  • 新增属性、删除属性,界面不会更新
  • 直接通过下标修改数组,界面不会自动更新

解决方案:

  • 新增:Vue.set(object, key, value)this.$set(object, key, value)
  • 删除:Vue.delete(target, key)this.$delete(target, key)

新增有两种大方向的解决方案:

  1. 不重新赋值原对象,在原对象基础上新增属性

    • 使用this.$set(对象,属性名,属性值)
  1. 重新赋值原对象(用的较少,了解即可)

    • Object.assign()

      js 复制代码
      this.obj = Object.assign({}, this.obj, { color: 'red' })
    • 直接定义一个新对象,这个对象包括原对象的全部属性以及新增的对象

      js 复制代码
      this.obj = {
         ...this.obj,
         sex: 'male'
       }

Vue3响应式原理

实现原理:

  • 通过Proxy(代理): 拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等

  • 通过Reflect(反射): 对源对象的属性进行操作

  • MDN文档中描述的Proxy与Reflect:

    • Proxy:MDN-Proxy

    • Reflect:MDN-Reflect

      js 复制代码
      new Proxy(data, {
      	// 拦截读取属性值
          get (target, prop) {
          	return Reflect.get(target, prop)
          },
          // 拦截设置属性值或添加新属性
          set (target, prop, value) {
          	return Reflect.set(target, prop, value)
          },
          // 拦截删除属性
          deleteProperty (target, prop) {
          	return Reflect.deleteProperty(target, prop)
          }
      })
      proxy.name = 'jack'   

toRefs

当去解构和展开响应式数据对象使用 toRefs 保持响应式

使用 ref 创建的变量是一个独立的响应式引用。但是,当我们需要将一个响应式对象的属性解构到组件模板中使用时,需要使用 toRefs

html 复制代码
<script setup>
import { reactive } from "vue";
const { name, age } = reactive({ name: "Jack", age: 18 });
</script>

<template>
  <div>
    <p>姓名:{{ name }}</p>
    <!-- 响应式丢失 -->
    <p>年龄:{{ age }} <button @click="age++">age++</button></p>
  </div>
</template>

直接解构,解构得到的数据不是响应式

  • 使用 toRefs 处理响应式数据
js 复制代码
import { reactive, toRefs } from "vue";
const user = reactive({ name: "Jack", age: 18 });
const { name, age } = toRefs(user)

toReftoRefs功能一致:

  • 语法:const name = toRef(person,'name')
  • toRef只能处理一个属性,批量处理使用toRefs

computed

  • vue 中导出 computed 函数,在 setup 函数中,使用 computed 函数,传入一个函数,函数返回计算好的数据
  • 用法同 Vue2 中的 computed
html 复制代码
<script setup>
  import { ref, computed } from "vue";
  const arr = ref([1, 2, 3, 4, 5]);
  // 计算属性
  const sum = computed(() => arr.reduce((sum, item) => sum + item.value, 0));
</script>

<template>
  <div>
    <p>数组:{{ arr }}</p>
    <p>数组和:{{ sum }}</p>
  </div>
</template>

拓展:给 computed 函数传参

js 复制代码
const fn = computed(() => (param) => console.log(param));
fn('Hello Vue3')

watch

  • watch(需要监听的数据, 数据改变执行函数, 配置对象) 来进行数据的侦听
  • 数据:单个数据,多个数据,函数返回对象属性,属性复杂需要开启深度监听
  • 配置对象:deep 深度监听 immediate 默认执行

示例如下:

  • 使用 watch 监听一个响应式数据
html 复制代码
<script setup>
  import { ref, watch } from "vue";
  const count = ref(0);
  // watch(数据, 改变后回调函数)
  watch(count, (newValue, oldValue) => {
    console.log("count改变了", newValue, oldValue);
  });
  // 2s改变数据
  setTimeout(() => {
    count.value++;
  }, 2000);
</script>
  • 使用 watch 监听多个响应式数据
html 复制代码
<script setup>
  import { reactive, ref, watch } from "vue";
  const count = ref(0);
  const user = reactive({
    name: "tom",
    info: {
      gender: "男",
      age: 18,
    },
  });
  // watch([数据1, 数据2, ...], 改变后回调函数)
  watch([count, user], (newValue, oldValue) => {
    console.log("数据改变了", newValue, oldValue);
  });
  // 2s改变数据
  setTimeout(() => {
    count.value++;
  }, 2000);
  // 4s改变数据
  setTimeout(() => {
    user.info.age++;
  }, 4000);
</script>
  • 使用 watch 监听响应式对象数据中的一个属性(简单)
html 复制代码
<script setup>
  import { reactive, watch } from "vue";
  const user = reactive({
    name: "tom",
    info: {
      gender: "男",
      age: 18,
    },
  });
  // watch(()=>数据, 改变后回调函数)
  watch(()=>user.name, (newValue, oldValue) => {
    console.log("数据改变了", newValue, oldValue);
  });
  // 2s改变数据
  setTimeout(() => {
    user.name = 'jack';
  }, 2000);
</script>
  • 使用 watch 监听响应式对象数据中的一个属性(复杂),配置深度监听
html 复制代码
<script setup>
  import { reactive, watch } from "vue";
  const user = reactive({
    name: "tom",
    info: {
      gender: "男",
      age: 18,
    },
  });
  // watch(()=>数据, 改变后回调函数, {deep: true})
  watch(
    () => user.info,
    () => { console.log("数据改变了"); },
    { deep: true }// 开启深度监听,由于监视的是reactive定义的对象中的某个属性,所以deep配置有效
  );
  // 2s改变数据
  setTimeout(() => {
    user.info.age = 60;
  }, 2000);
</script>
  • 使用 watch 监听,配置默认执行
js 复制代码
{
  // 开启深度监听
  deep: true,
  // 默认执行一次
  immediate: true
}

如若要使用watch监听ref定义的引用类型数据中的引用类型数据(可以理解成监听ref定义的对象中的对象),有两种方法:

  1. 监听ref定义数据.value
  2. 开启深度监听{ deep: true }

watchEffect

watchEffect 是 Vue 3 组合式 API 中用于监听响应式数据变化的钩子。它会立即执行一次回调函数(相当于 watch 加了{ immediate: true }),并在依赖的响应式数据发生变化时再次执行回调函数

watchEffect不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性

watchEffect有点类似于computed

  • computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值
  • watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值
html 复制代码
<template>
  <p>计数器:{{ counter }}</p>
  <button @click="increment">增加</button>
</template>

<script setup>
import { ref, watchEffect } from 'vue';

// 定义计算属性
const counter = ref(0);

// 监听计算属性的变化
watchEffect(() => {
  console.log('计数器变化:', counter.value);
  // 执行其他逻辑
});

// 增加计数器的方法
const increment = () => {
  counter.value++;
};
</script>

defineExpose

  • 使用 <script setup> 的组件是默认关闭的,组件实例使用不到顶层的数据和函数
  • 需要配合 defineExpose 暴露给组件实例使用,暴露的响应式数据会自动解除响应式
  • 类似于 Vue2 中的 $refs

父组件:

html 复制代码
<script setup>
import { ref } from 'vue'
// vue3 中使用组件只需导入无需注册
import Form from './components/Form.vue'

// 提供一个ref
const formRef = ref(null)
// 使用组件组件和方法
// 配合 defineExpose 暴露数据和方法,ref获取的组件实例才可以使用
const fn = () => {
  console.log(formRef.value.count)
  formRef.value.validate()
}
</script>

<template>
  <Form ref="formRef"></Form>
</template>

子组件:

html 复制代码
<script setup>
import { ref } from 'vue'

const count = ref(0)
const validate = () => {
  console.log('表单校验方法')
}

// 暴露属性给外部组件使用
defineExpose({count, validate})
</script>

<template>
  <h3>我是Form组件</h3>
</template>

defineProps(父传子)

Vue3 中用于实现组件通信中的父传子组件通信

  • 父组件提供数据
  • 父组件将数据传递给子组件
  • 子组件通过 defineProps 进行接收
  • 子组件渲染父组件传递的数据

父组件:

html 复制代码
<script setup>
import { ref } from 'vue'
import ChildCom from './components/ChildCom.vue'

const name = ref('Jack')
const age = ref(18)
</script>

<template>
  <div>
    <ChildCom :name="name" :age="age"></ChildCom>
  </div>
</template>

子组件:(setup 语法糖)

html 复制代码
<script setup>
import { computed } from 'vue'

// defineProps: 接收父组件传递的数据
const props = defineProps({ name: String, age: Number })

// 使用props
console.log(props.name)
</script>

如果使用 defineProps 接收数据,这个数据只能在模板中渲染。如果想要在 script 中也操作 props 属性,应该接收返回值(使用 props 变量接受)prop 进一步校验可以直接参考官网

子组件:(常规写法)

html 复制代码
<script>
import { ref, onMounted } from 'vue';

export default {
  props: {
    name: String,
    age: Number
  },
  setup(props) {
    const name = ref(props.name); // 通过 ref() 创建响应式变量
    const age = ref(props.age);

    onMounted(() => {
      console.log(name.value); // 访问 props 的 name 属性
      console.log(age.value); // 访问 props 的 age 属性
    });

    return {
      name,
      age
    };
  }
}
</script>

defineEmits(子传父)

  • 子组件通过 defineEmits获取 emit 函数
  • 子组件通过 emit 触发事件,并且传递数据
  • 父组件提供方法
  • 父组件通过自定义事件的方式给子组件注册事件

子组件:

html 复制代码
<script setup>
defineProps({
  name: String,
  age: Number,
})

// 得到emit函数,显性声明事件名称
const emit = defineEmits(['changeAge'])
const change = () => {
  emit('changeAge', 1)
}
</script>

父组件:

html 复制代码
<script setup>
import { ref } from 'vue'
import ChildCom from './components/ChildCom.vue'

const name = ref('Jack')
const age = ref(18)
const changeAge = (num) => {
  age.value = age.value + num
}
</script>

<template>
  <div>
    <ChildCom :name="name" :age="age" @changeAge="changeAge"></ChildCom>
  </div>
</template>

defineEmits 获取 emit 函数,且组件需要触发的事件需要显性声明出来

provide&inject(跨级组件通信)

通过 provide 和 inject 函数可以简便的实现跨级组件通信

  • provideinject是解决跨级组件通信的方案
    • provide 提供后代组件需要依赖的数据或函数
    • inject 注入(获取)provide 提供的数据或函数
  • 官方术语:依赖注入
    • App是后代组件依赖的数据和函数的提供者,Child是注入(获取)了App提供的依赖

祖先组件:App.vue

html 复制代码
<script setup>
import { provide, ref } from 'vue';
import ParentCom from './ParentCom.vue';

// 1. app组件数据传递给child
const count = ref(0);
provide('count', count);

// 2. app组件函数传递给child,调用的时候可以回传数据
const updateCount = (num) => {
  count.value += num;
};
provide('updateCount', updateCount);
</script>

<template>
  <div
    class="app-page"
    style="border: 10px solid #ccc; padding: 50px; width: 600px">
    app 组件 {{ count }} updateCount
    <ParentCom />
  </div>
</template>

父级组件:ParentCom.vue

html 复制代码
<script setup>
import ChildCom from './ChildCom.vue';
</script>

<template>
  <div class="parent-page" style="padding: 50px">
    parent 组件
    <hr />
    <ChildCom />
  </div>
</template>

子级组件:ChildCom.vue

html 复制代码
<script setup>
const count = inject('count');
const updateCount = inject('updateCount');
</script>

<template>
  <div class="child-page" style="padding: 50px; border: 10px solid #ccc">
    child 组件 {{ count }} <button @click="updateCount(100)">修改count</button>
  </div>
</template>

vue3 中的 inject 函数与 vue2 中的 inject 函数有所不同,一次只能获取一条数据或函数

其它新特性

生命周期函数

  • 先从 vue 中导入以on开头的生命周期钩子函数
  • setup函数中调用生命周期函数并传入回调函数
  • 生命周期钩子函数可以调用多次
选项式API下的生命周期函数使用 组合式API下的生命周期函数使用 函数描述
beforeCreate 可以省略 组件实例被创建之前调用
created 可以省略 组件实例创建完成后调用,可以在这个钩子函数中进行数据初始化和对外部资源的请求
beforeMount onBeforeMount 在组件挂载到 DOM 之前调用,此时模板编译完成,但组件尚未插入到 DOM 中
mounted onMounted 在组件挂载到 DOM 后调用,此时组件已经被插入到 DOM 中,可以进行 DOM 操作
beforeUpdate onBeforeUpdate 数据更新前调用,当响应式数据被修改时触发,但 DOM 尚未更新
updated onUpdated 数据更新后调用,DOM 已经更新完成
beforeDestroyed onBeforeUnmount 在组件卸载之前调用,此时组件还没有被卸载
destroyed onUnmounted 在组件卸载之后调用,组件已经被完全卸载,可以进行一些清理操作
activated onActivated 当组件被激活时调用
deactivated onDeactivated 当组件被失活时调用

例如 onMounted 的使用:

  • 选项式 Api
html 复制代码
<script>
export default  {
  mounted(){
    console.log('挂载完成')
    // 执行其他逻辑
  }
}
</script>
  • 组合式 Api
html 复制代码
<script>
import { onMounted } from 'vue';

export default {
  setup() {
    onMounted(() => {
      console.log('挂载完成');
      // 执行其他逻辑
    });
  },
};
</script>
  • setup 语法糖
html 复制代码
<script setup>
import { onMounted } from "vue";
onMounted(()=>{
  console.log('挂载完成')
  // 执行其他逻辑
})
</script>

自定义hook函数

hook本质是一个函数,把setup函数中使用的Composition API进行了封装

  • 类似于 Vue2 中的 mixin

  • 自定义 hook 的优势:复用代码,让 setup 中的逻辑更清楚易懂

下面是一个简单的示例:

  • src/hooks/useSquareHook.js
js 复制代码
// useSquareHook.js
import { ref, computed } from 'vue';

export function useSquareHook() {
  const number = ref(0);

  const square = computed(() => number.value * number.value);

  const setNumber = (value) => {
    number.value = value;
  };

  return {
    number,
    square,
    setNumber,
  };
}

在组件中使用这个自定义 hook:

html 复制代码
<template>
  <div>
    <input type="number" v-model="number" @input="setNumber" />
    <p>数字的平方是: {{ square }}</p>
  </div>
</template>

<script>
import { defineComponent } from 'vue';
import { useSquareHook } from './useSquareHook';

export default defineComponent({
  setup() {
    const { number, square, setNumber } = useSquareHook();

    return {
      number,
      square,
      setNumber,
    };
  },
});
</script>

shallowReactive与shallowRef

  • shallowReactive:只处理对象最外层属性的响应式(浅响应式)

  • shallowRef:只处理基本数据类型的响应式,不进行对象的响应式处理

  • 使用场景:

    • 如果有一个对象数据,结构比较深,但变化时只是外层属性变化(shallowReactive)
    • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是用新的对象来替换(shallowRef)
html 复制代码
<template>
  <div>
    修改shallowRef:{{ dataRef }}
    <button @click="add">count++</button>
    <br>
    修改shallowReactive:{{ dataReactive }}
    <button @click="change">change</button>
  </div>
</template>

<script setup>
  import { shallowRef,shallowReactive } from 'vue';
  const dataRef = shallowRef({ count: 0 });
  const add = () => {
    dataRef.value.count++;
    console.log(dataRef.value)
  }
  const dataReactive = shallowReactive({ nested: { count: 0 } });
  const change = () => {
    dataReactive.nested.count++;
    console.log(dataReactive)
  }
</script>

上述代码数据均不为响应式

readonly与shallowReadonly

  • readonly:让一个响应式数据变为只读的(深只读),依旧是返回的代理对象
  • shallowReadonly:让一个响应式数据变为只读的(浅只读)
  • 应用场景:不希望数据被修改时

toRaw与markRaw

toRaw

  • 作用:将一个由reactive生成的响应式对象 转为普通对象
  • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新

markRaw

  • 作用:标记一个对象,使其永远不会再成为响应式对象
  • 应用场景:
    1. 有些值不应被设置为响应式的,例如复杂的第三方类库等
    2. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能

customRef

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

  • 实现防抖效果:(来自官网)

    html 复制代码
    <template>
      <input type="text" v-model="keyword">
      <h3>{{keyword}}</h3>
    </template>
    
    <script>
      import {ref,customRef} from 'vue'
      export default {
      	name:'Demo',
      	setup(){
      	  // let keyword = ref('hello') //使用Vue准备好的内置ref
      	  //自定义一个myRef
      	  function myRef(value,delay){
      	  	let timer
      	  	//通过customRef去实现自定义
      	  	return customRef((track,trigger)=>{
      	  	  return{
      	  	  	get(){
      	  	  	  track() //告诉Vue这个value值是需要被"追踪"的
      	  	  	  return value
      	  	  	},
      	  	  	set(newValue){
      	  	  	  clearTimeout(timer)
      	  	  	  timer = setTimeout(()=>{
      	  	  	  	value = newValue
      	  	  	  	trigger() //告诉Vue去更新界面
      	  	  	  },delay)
      	  	  	}
      	  	  }
      	  	})
      	  }
      	  let keyword = myRef('hello',500) //使用程序员自定义的ref
      	  return {
      	  	keyword
      	  }
      	}
      }
    </script>

响应式数据的判断

  • isRef:检查一个值是否为一个 ref 对象
  • isReactive:检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly:检查一个对象是否是由 readonly 创建的只读代理
  • isProxy:检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

Fragment

  • Vue2中:组件必须有一个根标签
  • Vue3中:组件可以没有根标签,内部会将多个标签包含在一个 Fragment 虚拟元素中
  • 好处:减少标签层级,减小内存占用

Teleport

Teleport 是一种能够将我们的组件html结构移动到指定位置的技术,使用Teleport,你可以将组件的内容渲染到DOM树中的任意位置,而不必修改组件的父组件层次结构或使用复杂的技巧

示例:

  • 在组件模板中使用<teleport>标签来包裹你想要渲染到的目标位置。这个目标位置可以是任意的DOM元素或选择器
html 复制代码
<template>
  <div>
    <!-- other content -->
    <teleport to="#target">
      <div>Teleport content</div>
    </teleport>
  </div>
</template>
  • 在同一个组件内部,在目标位置的外部创建一个具有唯一ID的DOM元素,这将作为Teleport的渲染目标
html 复制代码
<template>
  <div>
    <!-- other content -->
    <teleport to="#target">
      <div>Teleport content</div>
    </teleport>
  </div>
  <div id="target"></div>
</template>

此时,Teleport就会将<div>Teleport content</div>渲染到具有ID为target的DOM元素中,而不是在父组件的位置

Teleport只会将内容渲染到目标位置,不会移动组件的其他属性或行为。这使得Teleport非常适合在模态框、对话框、弹出菜单等情况下使用

Vue3组件注册

本节内容源自官网

全局注册

  • 我们可以使用 Vue 应用实例app.component() 方法,让组件在当前 Vue 应用中全局可用
js 复制代码
import { createApp } from 'vue'

const app = createApp({})

app.component(
  // 注册的名字
  'MyComponent',
  // 组件的实现
  {
    /* ... */
  }
)
  • 如果使用单文件组件,你可以注册被导入的 .vue 文件
js 复制代码
import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)
  • app.component() 方法可以被链式调用
js 复制代码
app
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)
  • 全局注册的组件可以在此应用的任意组件的模板中使用
js 复制代码
<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>

局部注册

全局注册虽然很方便,但有以下几个问题:

  1. 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫"tree-shaking")。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中
  2. 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性

相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好

在使用 <script setup> 的单文件组件中,导入的组件可以直接在模板中使用,无需注册(Vite 支持自动全局注册组件,前提是要将组件写在components目录中)

html 复制代码
<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
  <ComponentA />
</template>

如果没有使用 <script setup>,则需要使用 components 选项来显式注册(建议直接使用 setup 语法糖形式)

js 复制代码
import ComponentA from './ComponentA.js'

export default {
  components: {
    ComponentA
  },
  setup() {
    // ...
  }
}

局部注册的组件在后代组件中并不可用 。在这个例子中,ComponentA 注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用

Pinia

基本介绍

Pinia 是一个状态管理工具,它和 Vuex 一样为 Vue 应用程序提供共享状态管理能力。Pinia核心概念包括state(状态)、actions(修改状态,包括同步和异步)、getters(计算属性)

Pinia 中,状态的修改是通过直接操作 store 中的状态属性来实现的,而不是通过 mutation。相比于 VuexmutationPinia 提倡使用更直接的方式来修改状态,这使得代码更加简洁和直观

除此之外:

  • 语法和 Vue3 一样,它实现状态管理有两种语法:选项式API组合式API
  • 它也支持 Vue2 也支持 devtools,当然它也是类型安全的,支持 TypeScript
  • Pinia 相比 Vuex4,对于 Vue3的兼容性更好,具备完善的类型推荐(同样支持 vue 开发者工具)

Pinia的数据流转图:

  • Pinia 可以创建多个全局仓库,不用像 Vuex 一个仓库嵌套模块,结构简单
  • 管理数据简单,提供数据和修改数据的逻辑即可,无需记忆过多的 API

使用Pinia

基本使用

  • 安装 Pinia
bash 复制代码
yarn add pinia
# 或者使用 npm
npm install pinia
  • main.js 中挂载 pinia
js 复制代码
import { createApp } from 'vue'
import App from './App.vue'

import { createPinia } from 'pinia'
const pinia = createPinia()

createApp(App).use(pinia).mount('#app')

Vue2 使用 Pinia 见官网

  • 新建文件 store/counter.js
js 复制代码
import { defineStore } from 'pinia'
// 创建store,命名规则: useXxxxStore(一个符合组合式函数风格的约定)
// 参数1:store的唯一表示,也被用作 id,是必须传入的,Pinia 将用它来连接 store 和 devtools
// 参数2:可接受两类值:Setup 函数或 Option 对象

// Option Store 写法:
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})
export default useCounterStore

// Setup Store 写法:
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})
export default useCounterStore
  • 在组件中使用
vue 复制代码
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量
const store = useCounterStore()
</script>

<template>
  <h1>根组件---{{ store.count }}</h1>
</template>

<style></style>

store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value,就像 setup 中的 props 一样,如果你写了,我们也不能解构它

actions与getters的使用

如上文所说,在 pinia 中没有 mutations,只有 actions,不管是同步还是异步的代码,都可以在actions中完成

  • 在 actions 中提供方法并且修改数据,在 getters 中提供计算属性
js 复制代码
import { defineStore } from 'pinia'
// 创建store
const useCounterStore = defineStore('counter', {
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {
    double() {
      return this.count * 2
    }
  },
  actions: {
    increment() {
      this.count++
    },
    incrementAsync() {
      setTimeout(() => {
        this.count++
      }, 1000)
    }
  }
})
// 导出 useCounterStore
export default useCounterStore
  • 在组件中使用 actions 与 getters
html 复制代码
<script setup>
import useCounterStore from './store/counter'
const counter = useCounterStore()
</script>

<template>
  <h1>根组件---{{ counter.count }}</h1>
  <h3>{{ counter.double }}</h3>
  <button @click="counter.increment">加1</button>
  <button @click="counter.incrementAsync">异步加1</button>
</template>

storeToRefs的使用

如果直接从 pinia 中解构数据,会丢失响应式, 使用 storeToRefs 可以保证解构出来的数据保持响应式

html 复制代码
<script setup>
import { storeToRefs } from 'pinia'
import useCounterStore from './store/counter'

const counter = useCounterStore()
// 直接从 pinia 中解构数据,会丢失响应式
const { count, double } = counter

// 使用 storeToRefs 可以保证解构出来的数据也是响应式的
const { count, double } = storeToRefs(counter)
</script>

组件外使用Pinia

Pinia store 依靠 pinia 实例在所有调用中共享同一个 store 实例。大多数时候,只需调用你定义的 useStore() 函数,完全开箱即用。例如,setup() 中,你不需要再做任何事情。但在组件之外,情况就有点不同了。 实际上,useStore() 给你的 app 自动注入了 pinia 实例。这意味着,如果 pinia 实例不能自动注入,你必须手动提供给 useStore() 函数。 你可以根据不同的应用,以不同的方式解决这个问题

这段话的意思是:在setup()中,你可以随便使用useStore,隐含意思就是在js或别的文件中,就不能随便用了。这是由于script setup 是一个特殊的语法状态,它会在 JS 前置执行

官网举例:

如果你不做任何 SSR(服务器端渲染),在用 app.use(pinia) 安装 pinia 插件后,对 useStore() 的任何调用都会正常执行:

js 复制代码
import { useUserStore } from '@/stores/user'
import { createApp } from 'vue'
import App from './App.vue'

// ❌ 失败,因为它是在创建 pinia 之前被调用的
const userStore = useUserStore()

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

// ✅ 成功,因为 pinia 实例现在激活了
const userStore = useUserStore()

为确保 pinia 实例被激活,最简单的方法就是将 useStore() 的调用放在 pinia 安 装后才会执行的函数中。

让我们来看看这个在 Vue Router 的导航守卫中使用 store 的例子

js 复制代码
import { createRouter } from 'vue-router'
const router = createRouter({
  // ...
})

// ❌ 由于引入顺序的问题,这将失败
const store = useStore()

router.beforeEach((to, from, next) => {
  // 我们想要在这里使用 store
  if (store.isLoggedIn) next()
  else next('/login')
})

router.beforeEach((to) => {
  // ✅ 这样做是可行的,因为路由器是在其被安装之后开始导航的,
  // 而此时 Pinia 也已经被安装。
  const store = useStore()

  if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})

最简单方法是延迟调用 useStore(),方法是将它们放在安装 pinia 后始终运行的函数中,万不得已可以放入定时器中延迟调用

Pinia模块化

复杂项目中,不可能多个模块的数据都定义到一个 store 中,一般来说会一个模块对应一个 store,最后通过一个根 store 进行整合

  • 新建store/user.js文件
js 复制代码
import { defineStore } from 'pinia'

const useUserStore = defineStore('user', {
  state: () => {
    return {
      name: 'Jack',
      age: 18
    }
  }
})
export default useUserStore
  • 新建文件store/counter.js文件
js 复制代码
import { defineStore } from 'pinia'

const useCounterStore = defineStore('counter', {
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {
    double() {
      return this.count * 2
    }
  },
  actions: {
  }
})
export default useCounterStore
  • 新建store/index.js文件
js 复制代码
// 写法一
import useUserStore from './user'
import useCounterStore from './counter'

// 统一导出 useStore 方法
export default function useStore() {
  return {
    user: useUserStore(),
    counter: useCounterStore(),
  }
}

// 写法二
export useUserStore from './user'
export useCounterStore from './counter'
  • 在组件中使用
js 复制代码
// 写法一使用
<script setup>
import { storeToRefs } from 'pinia'
import useStore from './store'
const { counter } = useStore()

// 使用storeToRefs可以保证解构出来的数据也是响应式的
const { count, double } = storeToRefs(counter)
</script>

// 写法二使用
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore, useCounterStore } from './store'
const counter = useCounterStore()

// 使用 storeToRefs 可以保证解构出来的数据也是响应式的
const { count, double } = storeToRefs(counter)
</script>

Pinia持久化

使用 pinia-plugin-persistedstate 可以实现 pinia 仓库状态持久化

  • 安装 pinia-plugin-persistedstate
bash 复制代码
# pnpm
pnpm i pinia-plugin-persistedstate
# npm
npm i pinia-plugin-persistedstate
# yarn
yarn add pinia-plugin-persistedstate
  • main.js/main.ts 中注册(将插件添加到 pinia 实例上)
js 复制代码
import persist from 'pinia-plugin-persistedstate'
const app = createApp(App)

app.use(createPinia().use(persist))
  • 创建 Store 时,将 persist 选项设置为 true
js 复制代码
// 选项式 Store 语法
import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  state: () => {
    return {
      someState: '你好 pinia',
    }
  },
  persist: true,
})

// 组合式 Store 语法
import { defineStore } from 'pinia'

export const useStore = defineStore(
  'main',
  () => {
    const someState = ref('你好 pinia')
    return { someState }
  },
  {
    persist: true,
  }
)

基本配置完成后,你的整个 Store 将使用默认持久化配置保存,更多配置请参考 插件中文官网

vue-router4

vue3 之后,配套的 vue-router 也升级为 vue-router@4.x 版本

vue-router4 的语法和 vue-router3 的版本语法基本一致,但是有一些细微的修改

基本使用

  • 安装 vue-router
bash 复制代码
yarn add vue-router
  • 创建组件 Home.vue 和 Login.vue
  • 创建文件router/index.js
js 复制代码
import {
  createRouter,
  createWebHashHistory,
  createWebHistory,
} from 'vue-router'

// 创建路由
const router = createRouter({
  // 创建history模式的路由
  // history: createWebHistory(),
  // 创建hash模式的路由
  history: createWebHashHistory(),
  // 配置路由规则
  routes: [
    { path: '/home', component: () => import('../pages/Home.vue') },
    { path: '/login', component: () => import('../pages/Login.vue') },
  ]
})

export default router
  • main.js 中引入
js 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')
  • App.vue 中使用
html 复制代码
<template>
  <ul>
    <li>
      <router-link to="/home">首页</router-link>
    </li>
    <li>
      <router-link to="/login">登陆</router-link>
    </li>
  </ul>

  <!-- 路由出口 -->
  <router-view></router-view>
</template>

route与router

因为我们在 setup 里面没有访问 this,所以我们不能再直接访问 this.$routerthis.$route。作为替代,我们使用 useRouteruseRoute 函数

  • 通过 useRoute() 可以获取 route 信息
js 复制代码
<script>
import { useRoute } from 'vue-router'

// 在模板中我们仍然可以访问 $router 和 $route,所以不需要在 setup 中返回 router 或 route
export default {
  setup() {
    const route = useRoute()
    console.log(route.path)
    console.log(route.fullPath)
  },
}
</script>
  • 通过 useRouter() 可以获取 router 信息
html 复制代码
<script>
import { useRouter } from 'vue-router'

export default {
  setup() {
    const router = useRouter()
    const login = () => {
      router.push('/home')
    }
    return {
      login
    }
  }
}
</script>
相关推荐
程序媛小果7 分钟前
基于java+SpringBoot+Vue的桂林旅游景点导游平台设计与实现
java·vue.js·spring boot
喵叔哟27 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web