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 的常用函数集合(炸裂推荐、极其好用!) |
相关文档
- Vue3 中文文档(新) cn.vuejs.org/
Vue2 中文文档(旧)v2.cn.vuejs.org/- Vue3 设计理念 vue3js.cn/vue-composi...
Vue3 框架优点特点
- 首次渲染更快
- diff 算法更快
- 内存占用更少
- 打包体积更小
- 更好的 Typescript 支持
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
中需要.value
,template
中可省略
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>
ref
与reactive
区别:
- 从定义数据角度对比:
ref
用来定义基本类型数据reactive
用来定义引用类型数据,不支持简单数据类型- 此外,
ref
也可以用来定义引用类型数据 ,它内部会自动通过reactive
转为代理对象- 从原理角度对比:
ref
通过Object.defineProperty()
的get
与set
来实现响应式(数据劫持)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)
jsObject.defineProperty(data, 'count', { get () {}, set () {} })
存在问题:
- 新增属性、删除属性,界面不会更新
- 直接通过下标修改数组,界面不会自动更新
解决方案:
- 新增:
Vue.set(object, key, value)
、this.$set(object, key, value)
- 删除:
Vue.delete(target, key)
、this.$delete(target, key)
新增有两种大方向的解决方案:
-
不重新赋值原对象,在原对象基础上新增属性
- 使用
this.$set(对象,属性名,属性值)
- 使用
-
重新赋值原对象(用的较少,了解即可)
-
Object.assign()
jsthis.obj = Object.assign({}, this.obj, { color: 'red' })
-
直接定义一个新对象,这个对象包括原对象的全部属性以及新增的对象
jsthis.obj = { ...this.obj, sex: 'male' }
-
Vue3响应式原理
实现原理:
-
通过
Proxy
(代理): 拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等 -
通过
Reflect
(反射): 对源对象的属性进行操作 -
MDN文档中描述的Proxy与Reflect:
-
Proxy:MDN-Proxy
-
Reflect:MDN-Reflect
jsnew 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)
toRef
与toRefs
功能一致:
- 语法:
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定义的对象中的对象),有两种方法:
- 监听
ref定义数据.value
- 开启深度监听
{ 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 函数可以简便的实现跨级组件通信
provide
和inject
是解决跨级组件通信的方案- provide 提供后代组件需要依赖的数据或函数
- inject 注入(获取)provide 提供的数据或函数
- 官方术语:依赖注入
- App是后代组件
依赖
的数据和函数的提供者
,Child是注入
(获取)了App提供的依赖
- 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
:
- 作用:标记一个对象,使其永远不会再成为响应式对象
- 应用场景:
- 有些值不应被设置为响应式的,例如复杂的第三方类库等
- 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能
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/>
局部注册
全局注册虽然很方便,但有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫"tree-shaking")。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中
- 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 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
。相比于 Vuex
的 mutation
,Pinia
提倡使用更直接的方式来修改状态,这使得代码更加简洁和直观
除此之外:
- 语法和
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,
}
)
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.$router
或this.$route
。作为替代,我们使用useRouter
和useRoute
函数
- 通过
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>