Vue3.0笔记
前言
不知不觉中,使用 Vue3
开发挺长一段时间了,经历过大大小小的项目,其中项目形式主要涉及两种,要么是 Vue3+Webpack
要么就是 Vue3+Vite
,而对于 TS
并不会每个项目都会使用,像比较小且急的项目就不会考虑使用 TS
,感觉没啥必要,增加 TS
只会徒增编译时间、开发难度、开发成本等等。呃...总的来说,还是要看实际的场景考虑吧。
然后对于为什么会有两种项目形式的选择,也就是 Webpack
与 Vite
的比拼,这其中考虑的因素有很多,但最重要的一个衡量点觉得还是在于项目的规模,对于项目规模太大的项目,还是不太敢上 Vue3+Vite
形式,怕其中带来的后果承担不起>_<。
Vite
可用插件还不是很多。Vite
开发环境与生产环境打包器不同,线上出现bug不容易定位问题。- 可能你会想,用上Vue3不用Vite不是白用了?直接用vue2不就行了?Vue3的优点有很多,可不止
Vite
,像代码体积更小,底层性能更佳等等。
Vue3优点
1.源码体积小:Vue2
压缩后的体积大约有20kb,Vue3
压缩后的体积只有10kb。Vue3
中移除了部分API,如 filter
、$on
、$off
、$once
、$destory
、按键修饰符全部改用按键名,不再支持按键值。
2.响应式系统升级:将原来的 Object.defineProperty
替换成了ES6中的 Proxy
,它是通过在目标对象前加了一层拦截,代理的是对象而不是对象的属性,比原来的颗粒度变大了。现在可以监听到对象的新增与删除属性的操作、数组的索引赋值操作、数组的 length
属性操作。
3.更友好的静态类型支持:Vue2
需要额外采用 Flow 作为静态类型检查,它是由 Facebook 出品的 JavaScript
静态类型检查工具;而 Vue3
直接全部采用 TS
进行开发,对 TS
支持更友好。
4.性能提升:Vue3
重写了虚拟 DOM
和底层的 Diff
算法,拥有更好的 tree-shaking
支持,整体性能得很大的一个提升。
5.CompositionAPI:Vue3
最大的一个变动应该就是推出了 CompositionAPI
,可以说它受ReactHook
启发而来;它我们编写逻辑更灵活,便于提取公共逻辑,代码的复用率得到了提高,也不用再使用 mixin
担心命名冲突的问题。
6.更先进的组件:Teleport
- 传送/瞬移组件、Suspense
- 等待异步组件过程中可以先提前渲染占位内容、Fragment
- 多根性质等等。
7.自定义渲染器:Vue3
支持创建自定义的渲染器,能实现跨平台,通过改写 Vue
底层渲染逻辑,如渲染成小程序形式等等。
一、初始化项目
初始化 Vue3+Vite
项目:
npm init vue@latest
// or
npm init vite@latest projectName
初始化 Vue3+Webpack
项目:
vue create projectName
二、setup
setup()
函数是 Vue3
新增的一个选项,它是组合式 API
的统一入口,简单来说,就是所有的 CompositionAPI
新特性都应该写在这个函数内部。
<script>
export default {
setup(props, context) {
return {}
}
}
</script>
它接收两个参数:
props
:这个还是和Vue2
使用的组件之间通信的props
一样。context
:定义上下文,这个参数身上有一些我们比较常用的属性,比如context.emit
:等同于Vue2
的this.$emit
。context.slots
:等同于Vue2
的this.$slots
。context.attrs
:等同于Vue2
的this.$attrs
。context.expose()
:当前组件对外暴露属性的函数。
expose
关于 context.expose()
函数,下面写两个例子,看完你应该就能明白了。
// parent.vue
<template>
<child ref="childRef" />
</template>
<script>
import { ref, onMounted } from 'vue'
import child from './components/child.vue'
export default {
components: { child },
setup() {
let childRef = ref(null)
onMounted(() => {
console.log(child.value.name)
console.log(child.value.age)
})
return { childRef }
}
}
</script>
//child.vue
<template>
<div>子组件</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
let name = ref('橙某人')
let age = ref(18)
return { name, age };
}
}
</script>
从上面例子中,我们可以看到子组件在 setup()
函数中返回的所有东西都可以被父组件直接访问,也就是子组件所有方法或数据等都是公开的。
但如果你想开发一个开源的组件或库,你有可能想保持一些内部方法的私有性,并不想它能被父类所调用,那么这个时候应该怎么做呢?我们来看看下面这个例子,给子组件添加 context.expose()
函数。
//child.vue
<template>
<div>子组件</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup(props, context) {
let name = ref('橙某人')
let age = ref(18)
context.expose({
name: 'ydydydq',
})
return { name, age };
}
}
</script>
可以看到,父组件已经无法访问 age
属性了,而且 name
属性值也是我们重新规定的值了,并不会访问到组件内部的数据。
这就是 context.expose()
函数的作用,它更加直观的允许组件暴露规定的一切方法和数据。
三、生命周期
Vue3
生命周期相比 Vue2
做了一些改动,如下:
Vue3
用setup()
函数替代了beforeCreate
和create
钩子。Vue3
生命周期钩子都以on+xxx
开头,并且需要手动导入且只能在setup()
函数内部使用。<script> import { onMounted }from 'vue'; export default { setup(props, context) { onMounted(() => { console.log('onMounted'); }) return {} } } </script>
卸载组件过程的两个钩子名称改变,
beforeDestroy
变成onBeforeUnmount
、destroyed
变成onUnmounted
。
vue2 | 说明 | vue3 | 说明 |
---|---|---|---|
beforeCreate | 在实例初始化之后、进行数据侦听和事件/侦听器的配置之前同步调用。 | setup() | setup 函数会比 beforeCreate 与 created 先执行,记住它是入口,会最先跑。 |
created | 在实例创建完成后被立即同步调用,这个阶段挂载还没开始,所以 $el 还不能使用。 | setup() | 同上 |
beforeMount | 在挂载开始之前被调用,相关的render 函数首次被调用,$el 依旧还不能用。 | onBeforeMount | 相同 |
mounted | 在实例挂载完成后被调用,这个时候 $el 能使用,但是这个阶段不能保证所有的子组件都挂载完成,如果你希望等待整个视图渲染完毕,可以使用 $nextTick 。 | onMounted | 相同 |
beforeUpdate | 在数据发生改变后,DOM 被更新之前被调用(使用该钩子要注意死循环)。 | onBeforeUpdate | 相同 |
updated | 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用(使用该钩子要注意死循环)。 | onUpdated | 相同 |
beforeDestroy | 实例销毁之前调用,但在这一步,实例仍然完全可用。 | onBeforeUnmount | 相同 |
destroyed | 实例销毁后调用,包括子实例也会被销毁。 | onUnmounted | 相同 |
最后我们放一张钩子执行的对比图,可以发现 Vue3
的生命周期钩子是比 Vue2
优先执行的。
四、ref与reactive
ref 与 reactive 是 Vue3
新推出的主要 API
之一,它们主要用于响应式数据的创建。
既然它们俩都是用于创建响应式数据,那么它们又有什么区别呢?
我们带着这个疑问往下看,先来看看它们俩的基本使用:
<template>
<div>
<div>count1: {{count1}}</div>
<div>count2: {{count2.val}}</div>
<div>count3: {{count3.val}}</div>
<button @click="add">按钮</button>
</div>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
const count1 = ref(0);
const count2 = ref({ val: 0 });
const count3 = reactive({ val: 0 }); // reactive只能接收引用类型, Object、Array...
function add() {
count1.value++;
count2.value.val++;
count3.val++;
}
return { count1, count2, count3, add }
}
}
</script>
上面写了一个很简单的计数叠加例子,从中可以得到一些结论:
template
模板中使用的数据和方法,都需要通过setup
函数return
出去才可以被使用。ref
函数创建的响应式数据,在模板中可以直接被使用,在JS
中需要通过.value
的形式才能使用。ref
函数可以接收原始数据类型与引用数据类型。reactive
函数只能接收引用数据类型。
这里我们就知道它们俩的区别了!但是既然这样为啥还需要 reactive
,全部使用 ref
一把梭不就行了吗?难道是存在什么情况是 reactive
能做,但是 ref
做不了的?
答案:没有这种情况,反而是 reactive
能做的,ref
都能胜任,并且 ref
底层还是使用 reactive
来做的!!!
通过 Vue3
的源码是可以证实这个结论的,具体过程感谢兴趣的小伙伴可以自行查阅,这里就不做过多阐述了。
反正简单来说就是,
ref
是在reactive
上在进行了封装,增强了其能力,使它支持了对原始数据类型的处理,所以在Vue3
中reactive
能做的,ref
也能做,reactive
不能做的,ref
也能做。
五、toRef与toRefs
toRef
官网对它的解释是可以用来为源响应式对象上的某个 property
新创建一个ref
。然后,ref
可以被传递,它会保持对其源 property
的响应式连接。 按我的理解就是把 reactive
创建的响应对象中的某个属性变成一个单独的 ref
,但这个新 ref
与原对象是保持响应式连接的。
概念不理解没关系,我们直接来看看例子:
<template>
<div>
<div>姓名: {{person.name}}</div>
<div>年龄: {{person.age}}</div>
<div>新年龄: {{newAge}}</div>
<button @click="update">按钮</button>
</div>
</template>
<script>
import { reactive, toRef } from 'vue';
export default {
setup() {
const person = reactive({
name: '橙某人',
age: 18
});
const newAge = toRef(person, 'age');
function update() {
// person.age = 20 一样会同步响应
newAge.value = 20
}
return { person, newAge, add }
}
}
</script>
从上面效果图可以看到,通过 toRef
创建的变量是具有响应式的并与原对象是保持响应式连接的。
toRefs
与前者相比多了一个 "s",你可以认为它是 toRef
的批量操作,它的主要作用,觉得有这两方面:
- 将
reactive
创建的响应对象中每个属性变成单独的ref
。 - 结合
ES6
的解构,可以使reactive
对象的属性在模板中直接被使用,再也不需要通过xx.属性
的形式。
需要注意的是,若直接对
reactive
创建的对象解构,会失去响应式!
<template>
<div>昵称: {{name}}</div>
<div>年龄: {{age}}</div>
<button @click="update">按钮</button>
</template>
<script>
import { reactive, toRef, toRefs } from 'vue';
export default {
setup() {
const person1 = reactive({
name: '橙某人',
});
const person2 = reactive({
age: 18,
});
function update() {
person1.name = 'YDYDYDQ'
person2.age = 20
}
return {
...toRefs(person1),
...person2, // 直接解构, 会失去响应式
add
};
}
}
</script>
从效果图可以看到 person2.age
是没有响应变化的。
person1 的类型:
{
name: Ref<string>,
}
person2 的类型:
{
age: number,
}
六、响应式对象判断
isRef
从名称可以直接看出这个方法的作用,它用于检查值是否为一个 ref
对象。
<script>
import { ref, isRef } from 'vue'
export default {
setup() {
const name = ref('橙某人');
const age = 18;
console.log(isRef(name)); // true
console.log(isRef(age)); // false
}
}
</script>
unref
如果参数是一个ref
,则返回内部值,否则返回参数本身。这个方法可能平常使用到的几率会比较少,它本质是 isRef
的三元判断语法糖,等同 val = isRef(val) ? val.value : val
。
<script>
import { ref, unref } from 'vue'
export default {
setup() {
const name = ref('橙某人');
const age = 18;
console.log(unref(name)); // 橙某人
console.log(unref(age)); // 18
}
}
</script>
你可以认为
name.value
===unref(name)
。
isReactive
检查一个对象是否是由reactive
或shallowReactive创建的代理。
<script>
import { ref, reactive, isReactive } from 'vue'
export default {
setup() {
const name = ref('橙某人');
const person = reactive({});
console.log(isReactive(name)); // false
console.log(isReactive(person)); // true
}
}
</script>
isProxy
检查一个对象是否是由reactive
、readonly
、shallowReactive
或shallowReadonly
创建的代理。
<script>
import { ref, reactive, isProxy } from 'vue'
export default {
setup() {
const name = ref('橙某人');
const person = reactive({});
const age = 18;
console.log(isProxy(name)); // false
console.log(isProxy(person)); // true
console.log(isProxy(age)); // false
}
}
</script>
isReadonly
检查一个对象是否是由readonly
或shallowReadonly
创建的代理。
<script>
import { ref, reactive, isProxy } from 'vue'
export default {
setup() {
const name = ref('橙某人');
const person = reactive({});
const student = readonly({}); // 接受一个对象(不论是响应式还是普通的)或是一个 ref, 返回一个原值的只读代理.
const age = 18;
console.log(isReadonly(name)); // false
console.log(isReadonly(person)); // false
console.log(isReadonly(student)); // true
console.log(isReadonly(age)); // false
}
}
</script>
七、computed
computed() 与 Vue2
中的 computed
作用基本一致,它可以接收一个函数或对象作为参数,会返回一个只读的 ref
对象。
<template>
<div>{{info}}</div>
</template>
<script>
import { computed, reactive } from 'vue'
export default {
setup() {
const person = reactive({
name: '橙某人',
age: 18
});
const info = computed(() => `姓名:${person.name},年龄${person.age}`)
return { info }
}
}
</script>
修改计算属性
如果你尝试修改计算属性的返回值,控制台会有出现提示信息并且修改不会成功。
info.value = '修改computed()返回值'
但假如你一定要修改 computed()
的返回值,我们可以使用它的对象参数来间接修改,该对象需要提供 set
与 get
方法。
<template>
<div>{{info}}</div>
</template>
<script>
import { computed, reactive } from 'vue'
export default {
setup() {
const person = reactive({
name: '橙某人',
age: 18
});
let info = computed({
set: newValue => {
person.name = newValue
},
get: () => `姓名:${person.name},年龄${person.age}`
});
info.value = '修改computed()返回值'
return { info }
}
}
</script>
八、watch
Vue3
的 watch() 与 Vue2
的 watch
选项和 this.$watch API 完全等效。
我们先来看看它的基本两种基本用法:
监听单一源
<template>
<div>昵称: {{name}}</div>
<div>年龄: {{person.age}}</div>
<button @click="update">按钮</button>
</template>
<script>
import { ref, reactive, watch } from 'vue';
export default {
setup() {
// 直接监听一个ref
const name = ref('橙某人');
watch(name, (newValue, oldValue) => {
console.log('新name:', newValue, oldValue);
});
// 监听对象单个属性
const person = reactive({
age: 18
})
watch(() => person.age, (newValue, oldValue) => {
console.log('新age:', newValue, oldValue);
});
function update() {
name.value = 'YDYDYDQ'
person.age = 20
}
return { name, person, add }
}
}
</script>
监听多个源
<template>
<div>昵称: {{name}}</div>
<div>年龄: {{age}}</div>
<button @click="update">按钮</button>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
const name = ref('橙某人');
const age = ref(18);
watch([name, age], (newValue, oldValue) => {
console.log(newValue, oldValue)
});
function update() {
name.value = 'YDYDYDQ';
age.value = 20;
}
return { name, age, add }
}
}
</script>
watch()
可以使用数组形式同时监听多个源,并且得到的 newValue
与 oldValue
也是以数组的形式返回的。
当我们只修改
name
得到的结果:
如果是想监听 reactive
对象的多个属性,可以这么写:
const person = reactive({
name: '橙某人',
age: 18
});
watch([() => person.name, () => person.age], (newValue, oldValue) => {
console.log(newValue, oldValue); // [...], [...]
});
immediate与deep参数
Vue
中 watch
选项还有 immediate 与 deep 参数,在 Vue3
中一样还能继续使用。
import { reactive, watch } from 'vue';
export default {
setup() {
const person = reactive({
name: '橙某人',
age: 18
});
watch(() => person, (newValue, oldValue) => {
console.log(newValue, oldValue)
}, {
deep: true,
immediate: true
});
function add() {
person.value.name = 'YDYDYDQ';
person.value.age = 20;
}
return { add };
}
}
但是这里有两个小坑需要小伙伴们注意,都是针对 oldValue
参数:
- 开启 immediate 参数后,初次监听的
oldValue
值是undefined
。 - 监听一整个 引用类型 变化时,
oldValue
值与newValue
值一样 (用ref
来定义也是一样)。
停止监听
当我们在setup
函数内创建 watch
监听,它会在当前组件被销毁的时候自动停止。但如果你想要手动地停止某个 watch
,可以调用watch
函数的返回值即可。
<script>
import { ref, watch } from 'vue';
export default {
setup() {
const name = ref('橙某人');
const stopWatch = watch(name, (newValue, oldValue) => {});
// 停止watch
stopWatch();
stopWatch
}
}
</script>
九、watchEffect
watchEffect 是 Vue3
的新函数,你可以认为它是 watch
的升级版或加强版,它功能和 watch
相似,但却更加强大。
它接收一个函数作为参数,并会立即执行传入的函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
定义看不懂-.-,没关系,我们一样先来看看它的使用方式:
<template>
<button @click="update1">按钮1</button>
<button @click="update2">按钮2</button>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const name = ref('橙某人');
const age = ref(18);
watchEffect(() => {
console.log('**********');
console.log(name.value);
console.log(age.value);
});
function update1() {
name.value = 'YDYDYDQ';
}
function update2() {
name.value = 20;
}
return { add1, add2 }
}
}
</script>
从上图中可以看到,watchEffect
会被立即执行(可以认为 immediate: true
),并且不管是修改 name
还是 age
都会触发 watchEffect
执行。
你可以认为
watchEffect
内部使用到什么响应数据,就监听了什么响应数据,只要这些响应数据发生变化,就会触发watchEffect
。
停止监听
当watchEffect
在组件的setup函数或 生命周期钩子 被调用时,监听器会被链接到该组件的生命周期,并在组件卸载时自动停止。当然它和 watch
一样,也能被手动停止。
const stopWatchEffect = watchEffect(() => {});
// 停止WatchEffect
stopWatchEffect();
清除副作用
首先,什么是副作用呢?
在纯函数中,副作用指的是如果一个函数在输入和输出之外还做了其他的事情,那么这个函数额外做的事情就被称为 副作用,而产生副作用的函数被称为 副作用函数。
纯函数:你可以简单认为,我调用一个函数,给它传入参数,会得到一个结果,但这个函数在调用过程中不会对程序中的其他变量进行修改,这种函数即可称之为纯函数。
watchEffect(effect)
的回调函数(effect
)就是一个副作用函数,因为我们使用watchEffect
就是为了监听响应数据变化后做一些其他操作。
一旦副作用函数被执行时,它势必会对程序带来一些影响。有时副作用函数会执行一些异步的副作用,而异步则会带来一些响应(副作用)是"失效"的,我们需要及时清除这些响应。
而 watchEffect((onInvalidate) => {})
监听器可以接收一个onInvalidate
函数作为入参,用来注册清理失效时的回调。
当以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时。
- 监听被停止。(如果在
setup
或生命周期钩子函数中使用了watchEffect
,则在组件卸载时)
看不懂!!!Em...那我们继续来看例子:
<template>
<div>count: {{count}}</div>
<button @click="update">按钮</button>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(1);
watchEffect(onInvalidate => {
const timer = setTimeout(() => {
console.log('发请网络请求')
}, 1000);
onInvalidate(() => {
clearTimeout(timer)
});
console.log(count.value);
});
function update() {
count.value++;
}
return { count, update }
}
}
</script>
从上面效果图可以看到,当我们不断点击去修改 count
的值时(修改过程时间超过 1s
),会频繁触发 watchEffect
,但是其中的 setTimeout
却不会在规定时间内被执行,而是当停止更改后 1s
后才被执行。
从这个现象有没有让你想起什么呢?没错,就是它,节流函数。
我们把 DEMO
修改修改:
<template>
<input v-model="inputValue" />
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const inputValue = ref('');
watchEffect(onInvalidate => {
const timer = setTimeout(() => {
console.log('输入结束200ms才去请求接口')
}, 200);
onInvalidate(() => {
clearTimeout(timer)
});
console.log('输入框的值:', inputValue.value)
})
return { inputValue }
}
}
</script>
从这个例子里有没有让你悟出些什么呢?
十、provide与inject
provide
与 inject
是一种跨层级组件(祖孙)通信方式。当组件多层嵌套时,不需要将数据一层一层的向下传递,通过它俩可以实现跨层级组件通信。
provide
注入一个值,可以被后代组件接收。
它接受两个参数:
- 第一个参数是要注入的
key
,可以是一个字符串或者一个symbol
。 - 第二个参数是要注入的值。
<template>
<button @click="update">按钮</button>
<PChild />
</template>
<script>
import { ref, provide, reactive } from 'vue'
import PChild from './PChild.vue';
export default {
components: { PChild },
setup() {
// 静态值
let name = '橙某人';
provide('nameKey', name);
// 响应式值
let age = ref(18);
provide('ageKey', age);
// 响应式值
let person = reactive({
sex: '男'
});
provide('personKey', person);
function update() {
name = 'YDYDYDQ';
age.value = 20;
person.sex = '女'
}
return { update };
}
}
</script>
inject
接收一个由祖先组件或整个应用 (通过app.provide()
) 注入的值。
它接受两个参数:
第一个参数是注入的
key
,找不到对应的key
,则返回undefined
或默认值。Vue
会遍历父组件链,通过匹配key
来确定所提供的值。如果父组件链上多个组件对同一个key
提供了值,那么离得更近的组件将会 "覆盖" 链上更远的组件所提供的值。如果没有能通过key
匹配到值,inject
将返回undefined
,除非提供了一个默认值。第二个参数是默认值,非必填,也可以是一个工厂函数。
如果默认值本身就是一个函数,那么你必须将
false
作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。const fn = inject('function', () => {}, false)
<template>
<h3>第二层子组件</h3>
<div>昵称: {{name}}</div>
<div>年龄: {{age}}</div>
<div>性别:{{person.sex}}</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const name = inject('nameKey');
const age = inject('ageKey');
const person = inject('personKey');
return { name, age, person };
}
}
</script>
从效果图可以看到,修改静态值视图是不更新的,而修改响应式值视图是可以同步更新的。
后代组件修改注入的值
Vue
是单向数据流,子组件直接修改父组件传递的数据是不被允许的,这样容易造成数据混乱,来源不明等问题。
然而,有时我们确实需要在子组件修改注入的数据,这种情况下我们需要怎么做呢?我们可以在父组件再注入一个方法,提供给后代组件们调用。
父组件:
<script>
import { ref, provide } from 'vue'
export default {
setup() {
let name = ref('橙某人');
provide('nameKey', name);
function updateName(newName) {
name.value = newName;
}
provide('updateNameKey', updateName);
}
}
</script>
后代组件:
<template>
<div>昵称: {{name}}</div>
<button @click="updateName('YDYDYDQ')">按钮</button>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const name = inject('nameKey');
const updateName = inject('updateNameKey');
return { name, updateName }
}
}
</script>
禁止后代组件修改注入的值
如果要确保注入的数据不会被后代组件更改,我们可以使用 readonly 来加上这个保证。
<script>
import { ref, provide, readonly } from 'vue'
export default {
setup() {
let name = ref('橙某人');
provide('nameKey', readonly(name)); // readonly
function updateName(newName) {
name.value = newName;
}
provide('updateNameKey', updateName);
}
}
</script>
增加 readonly
后,如果你尝试去修改 inject
接收的值,则控制台会报出提醒。
当然,即使增加了
readonly
,name
的响应式可不会消失哦,一样还是响应式的!
十一、全局API
十二、其余不常用API
readonly
readonly 接受一个对象 (不论是响应式还是普通的) 或是一个ref
,返回一个原值的只读代理。
<script>
import { readonly } from 'vue'
export default {
setup() {
const student = readonly({
name: '橙某人'
});
student.name = 'YDYDYDQ'; // 不可修改
}
}
</script>
当你尝试修改 readonly
创建的对象时,控制台会有提示:
shallowRef与shallowReactive
shallowRef 是 ref()
的浅层作用形式。说白了就是把对象的第一层数据变成响应式的,深层的数据不会变成响应式的。
shallowRef
如果用来定义原始数据类型,那么它和 ref
是等同的。
<template>
<div>{{person}}</div>
<button @click="update1">按钮一</button>
<button @click="update2">按钮二</button>
</template>
<script>
import { shallowRef, shallowReactive } from 'vue'
export default {
setup() {
const person = shallowRef({
name: '橙某人',
});
function update1() {
person.value.name = 'YDYDYDQ';
console.log(person.value); // {name: 'YDYDYDQ'}
}
function update2() {
person.value = {
name: 'ydydydq'
};
console.log(person.value); // {name: 'ydydydq'}
}
return { person, update1, update2 };
}
}
</script>
点击按钮一:
点击按钮二:
你会发现点击 "按钮一" 数据虽然更改了,但是视图却没有更新,这就是 shallowRef
的作用。
而 shallowReactive 与 shallowRef
类似,就不多说了。
shallowRef()
的作用常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。
triggerRef
triggerRef 用于强制触发依赖于一个浅层 ref的副作用,这通常在对浅引用的内部值进行深度变更后使用。说人话就是替 shallowRef
去更新视图。
<template>
<div>{{person}}</div>
<button @click="update1">按钮一</button>
<button @click="update2">按钮二</button>
</template>
<script>
import { shallowRef, triggerRef } from 'vue';
export default {
setup() {
const person = shallowRef({
name: '橙某人'
});
function update1() {
person.value.name = 'YDYDYDQ';
}
function update2() {
triggerRef(person)
}
return { person, update1, update2 }
}
}
</script>
你是否在想有 triggerRef
那应该就有 triggerReactive
吧?还真没有。
customRef
customRef 用于创建一个自定义的 ref
,显式声明对其依赖追踪和更新触发的控制方式。
可能前半句你看懂了,但后半句直接就懵逼了吧(-.-)。没事,概念这种东西也不是非记住不可,只要知道有那么一回事(可以自定义一个 ref
)就可以啦(✪ω✪)。
来看看它具体是如何使用的:
<template>
<div>姓名:{{ name }}</div>
<button @click="update">按钮</button>
</template>
<script>
import { customRef } from 'vue';
export default {
setup() {
function myRef(value) {
return customRef((track, trigger) => ({
get() {
track();
return value;
},
set(newVal) {
value = newVal;
trigger();
}
}))
}
const name = myRef('橙某人');
function update() {
name.value = 'YDYDYDQ';
}
return { name, update };
}
}
</script>
上面的例子中,myRef()
方法的效果就等同于 Vue
里面的 ref
。而其中的关键代码应该是 track()
与 trigger()
方法了,可惜在 文档 中只有寥寥数语的介绍。
一般来说,
track()
应该在get()
方法中调用,而trigger()
应该在set()
中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。
不过,问题不大,猜,我们都能大概猜出,执行这两个方法是要干些什么事情了(^ω^) 。
track()
:跟踪数据,收集数据被使用的情况(依赖),后续数据被更改了才知道去更新哪里的视图。trigger()
:更新视图,当数据被更改了,需要手动通知Vue
去更新视图。
shallowReadonly
shallowReadonly 是 readonly的浅层作用形式。
这个API很简单,我们直接看个例子就算了。
<template>
<button @click="update">按钮</button>
</template>
<script>
import { shallowReadonly } from 'vue';
export default {
setup() {
const person = shallowReadonly({
name: '橙某人',
other: {
age: 18
},
arr: [1, 2, 3]
})
function update() {
person.name = 'YDYDYDQ';
person.other.age = 20;
person.arr = [];
console.log(person); // { name: 'YDYDYDQ', other: { age: 20 }, arr: [1, 2, 3] }
}
return { update }
}
}
</script>
需要注意的是,Vue
并不希望我们把它用在嵌套的响应式对象中。
谨慎使用
浅层数据结构应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。
toRaw
toRaw 根据一个 Vue
创建的代理返回其原始对象。
toRaw()
可以返回由reactive()
、readonly()
、shallowReactive()
或者shallowReadonly()
创建的代理对应的原始对象。
以上是官方文档的解释,看着有点懵,我们直接来看个例子:
<template>
<div>{{ reactivePerson }}</div>
<div>{{ toRawPerson }}</div>
<button @click="update">按钮</button>
</template>
<script>
import { reactive, toRaw } from 'vue';
export default {
setup() {
const person = { name: '橙某人', age: 18 };
const reactivePerson = reactive(person);
const toRawPerson = toRaw(reactivePerson);
console.log(person === toRawPerson); // true
function update() {
toRawPerson.name = 'YDYDYDQ';
console.log(person, reactivePerson, toRawPerson);
}
return { reactivePerson, toRawPerson, update };
}
}
</script>
点击按钮后,可以发现数据被更改了,但是视图并没有更新。
这就是 toRaw
想要表达的一个作用。
因为平常我们修改 ref
或 reactive
等的数据,每次都会自动同步去更新视图,这在某些场景下是对性能的损耗。有时,我们单纯只希望修改数据而已,并不希望视图同样也跟着去更新,这个时候你就可以使用 toRaw
方法拿到它的原生数据,对原生数据进行修改,这个操作过程数据不会被追踪,不会去更新视图,这对性能有一定的优化作用。
markRaw
markRaw 将一个对象标记为不可被转为代理。返回该对象本身。
说人话就是标记一个对象,使其永远不会再成为响应式对象。
<template>
<div>{{ reactivePerson }}</div>
<button @click="update">按钮</button>
</template>
<script>
import { markRaw, reactive } from 'vue';
export default {
setup() {
let person = { name: '橙某人' };
person = markRaw(person);
const reactivePerson = reactive(person);
function update() {
reactivePerson.name = 'YDYDYDQ'; // 视图不会更新
console.log(reactivePerson);
}
return { reactivePerson, update }
}
}
</script>
它和 toRaw
相似,但也不全一样,可不要搞混了哦。
十三、多根性质-Fragment
<!-- Vue2这么写会报错 -->
<template>
<div>昵称: </div>
<div>年龄: </div>
<div>性别:</div>
</template>
相信应该有小伙伴在 Vue2
中会遇到这么一个报错:
这报错提示大概的意思是说组件只能有一个根节点,很多时候我们都会手动包裹一个 div
作为根节点。
<template>
<div>
<div>昵称: </div>
<div>年龄: </div>
<div>性别:</div>
</div>
</template>
虽然这可能不是一个多大的问题,但这并不友好,大多数情况下我们可能并不需要这个根节点。
Vue2
为什么不引入Fragments
?
Vue2
限制组件只能有一个根节点, 主要原因是虚拟 DOM Diff 算法依赖于具有单个根的组件,如果允许Fragments
需要对该算法进行重大更改,不仅要使其正常工作,而且必须使其具有高性能,这是一项非常艰巨的任务。
Vue3
中解决了这个问题,它新增了一个类似 DOM
的标签元素 <Fragment />
。如果在组件中出现多元素节点,那么在编译时 Vue
会自动为这些元素节点包裹一个<Fragment />
标签,并且该标签不会出现在 DOM
树中。
<!-- Vue3允许这么写 -->
<template>
<div>昵称: </div>
<div>年龄: </div>
<div>性别:</div>
</template>
Vue2
中可以使用 vue-fragments 库来实现<Fragment />
。
十四、Teleport-传送组件
Teleport 是 Vue3
新增的一个内置组件,它能将其插槽内容渲染到 DOM
中的另一个位置。因为它的性质,我们也常常称呼它为瞬移组件、传送组件、传送门等等。
我们举个栗子来说明(✪ω✪):
<template>
<div id="box">
<p>id选择器</p>
</div>
<div class="box">
<p>class选择器</p>
</div>
<div data-box>
<p>class选择器</p>
</div>
<!--teleport-->
<teleport :to="target">
<h2>橙某人</h2>
</teleport>
<div>
<button @click="update('body')">传送到body中</button>
<button @click="update('#box')">传送到id选择器中</button>
<button @click="update('.box')">传送到class选择器中</button>
<button @click="update('[data-box]')">传送到属性选择器中</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const target = ref('#app'); // 初始化渲染的位置
function update(selector) {
target.value = selector;
}
return { target, update }
}
}
</script>
通过上面的例子,可以看到使用 teleport
的 to
属性,我们可以很方便、随意的控制其插槽内容渲染的位置,to
属性可以接收各种 选择器或实际元素。
<teleport />
挂载时,传送的to
目标必须已经存在于DOM
中。理想情况下,这应该是整个Vue
应用DOM
树外部的一个元素。如果目标元素也是由Vue
渲染的,你需要确保在挂载<Teleport>
之前先挂载该元素。
借用 teleport
组件的这种性质,现在就能很容易解决 Vue2
中组件内部嵌套 Modal
或 Toast
组件的定位、 z-index
层级的问题了。
禁用 Teleport
<teleport />
标签一共接收两个属性,除了 to
属性,还有一个 disabled
属性,顾名思义,用于禁用功能的。
还是上面的例子,我们给它添加 disabled
属性:
<template>
...
<!--teleport-->
<teleport :to="target" :disabled="true">
<h2>橙某人</h2>
</teleport>
...
</template>
可以发现,<teleport />
会直接就渲染插槽的内容,并且点击任何按钮的切换是没有效果的,也就是相当于 to
属性完全不生效了。
十五、Suspense组件
Vue3
内置组件一共增加了两个,除了 <teleport />
组件,另一个就是 <suspense />
组件了。
Suspense 它用于协调对组件树中嵌套的异步依赖的处理。看定义应该有点懵,我们还是一样,实践出真理,举个栗子先。
当然,官网说这还是一个实验性功能!!!
<Suspense>
是一项实验性功能,它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。
新建 AsyncCom.vue
组件:
<template>
<h1>我是一个异步组件</h1>
</template>
<script>
export default {
async setup() {
const res = new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 3000);
});
return await res;
}
}
</script>
在 App.vue
中使用:
<template>
<h1>Suspense</h1>
<Suspense>
<template v-slot:default>
<AsyncCom />
</template>
<template v-slot:fallback>
<h3>Loading...</h3>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue'
const AsyncCom = defineAsyncComponent(() => import('./AsyncCom.vue'));
export default {
components: { AsyncCom }
}
</script>
<Suspense />
组件实际上是一个提升用户友好度的组件,当如果你在渲染时遇到异步依赖项 (异步组件或者具有async setup()
的组件),它将等到所有异步依赖项解析完成时再显示默认插槽。
可以先详细看看下面的异步组件内容,再回过头来看
<Suspense />
组件,会有更好的理解。
十六、异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue
允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue
只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
这是 Vue2
中对于异步组件的定义介绍,看了这个定义可能你和一样懵,那么我们就先来看看如何定义、使用一个异步组件。
我们先在 main.js
中定义异步组件:
import Vue from 'vue';
import App from './App.vue';
// 定义一个异步组件
Vue.component('first-async-com', (resolve, reject) => {
setTimeout(() => {
const comConfig = {
render: h => h('p', '我是第一个异步组件,我五秒后才加载!!!')
};
resolve(comConfig);
}, 5000)
});
Vue.config.productionTip = false;
new Vue({
render: h => h(App)
}).$mount('#app');
然后在 App.vue
中使用:
<template>
<div>
<h1>异步组件</h1>
<first-async-com />
</div>
</template>
从效果图中可以看到,我们的异步组件会在五秒后才被挂载到 DOM
树上,这对页面性能来说是一个非常好的优化。当然,我们也可以把 comConfig
单独写在一个文件中,丢到服务器上,再通过请求去获取,这也是一个优化项目体积的手段。
总得来说,异步组件的作用是对于性能的优化。
如果你注册子组件模板想使用
template
配置项,那么你可能会遇到上方的报错。Vue.component('first-async-com', (resolve, reject) => { setTimeout(() => { const comConfig = { template: '<p>我是第一个异步组件,我五秒后才加载!!!</p>', // 报错 }; resolve(comConfig); }, 1000) });
这是因为
Vue
有两种形式的代码compiler
模式和runtime
模式,Vue
模块的package.json
的main
字段默认为runtime
模式,指向了dist/vue.runtime.common.js
位置。解决方式:
import Vue from 'vue/dist/vue.js'; new Vue({ el: '#app', template: '<App/>', components: { App }, })
我们重新引入
Vue
的compiler
模式的包,再修改一下Vue
初始化的形式,那么子组件的注册即可使用template
配置项了。
配合webpack实现异步组件
Vue.component('async-webpack-example', (resolve, reject) => {
// webpack会内置实现 require() 方法
require(['@/components/my-async-component'], resolve)
})
上面这个例子是 官方 介绍的另一种结合 webpack
实现异步组件的方式。
通过这个特殊的 require
语法将会告诉 webpack
自动将你的构建代码切割成多个包,这些包会通过 Ajax
请求加载。
局部注册异步组件
前面我们介绍的是全局注册异步组件,接下来就看看如何在局部中注册异步组件。
先创建一个新组件:
<template>
<div>我是第二个异步组件</div>
</template>
在 App.vue
中局部注册异步组件:
<template>
<div>
<second-async-com />
</div>
</template>
<script>
export default{
components: {
'SecondAsyncCom': () => import('@/components/SecondAsyncCom.vue')
}
}
</script>
从上图中,可以看到该组件会被单独异步加载获取。
这是借用 import()
语法来实现的导入,你也可以直接提供一个返回Promise
的函数,例如:
<script>
export default {
components: {
'SecondAsyncCom': () => new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
template: '<div>我是第二个异步组件</div>'
})
}, 1000)
})
}
}
</script>
最后,我们把例子改成同步的形式来进行对比:
<script>
import SecondAsyncCom from '@/components/SecondAsyncCom.vue';
export default {
components: {
SecondAsyncCom
}
}
</script>
可以发现同步形式并没有单独加载组件,它会被打包到一起,并同步加载、顺序渲染。
Vue3的异步组件
上面罗里吧嗦回顾了一下 Vue2
异步组件的内容,那么 Vue3
中的异步组件又是如何的呢?
整体来说,没啥改变,可能更多的是在语法方面的改变而已。
我们直接来看看,在 Vue3
项目的 App.vue
文件中:
<template>
<AsyncCom />
</template>
<script>
import { defineAsyncComponent } from 'vue';
export default {
components: {
'AsyncCom': defineAsyncComponent(() => import('./components/AsyncCom.vue'))
},
}
</script>
同样,你也可以直接提供一个返回 Promise
的函数即可:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
十七、Vue3响应式原理
文章字数有点多了,滚动起来总是一卡顿一卡顿的,原理这块只能单独再写一篇文章了,写完会回来补上链接。
十八、路由
vue-router 相信使用过 Vue
的小伙伴都不陌生了,Vue
通过它来实现对路由的管理,而在 Vue3
中使用 vue-router
的版本至少需要为 4.x.x
以上,下面我们就来看看它的具体使用过程。
安装
npm install vue-router@4 --save
基本使用
基础配置与使用
创建 router/index.js
文件:
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
const routes = [
{ path: '/', name: 'Main', component: ()=> import('../views/main.vue') },
{ path: '/login', name: 'login', component: ()=> import('../views/login.vue') },
];
const router = createRouter({
history: createWebHashHistory(), // createWebHashHistory() || createWebHistory()
routes
})
export default router;
在 main.js
中引入:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
const app = createApp(App)
app.use(router)
app.mount('#app')
在 App.vue
中使用:
<template>
<router-link to="/">主页</router-link> |
<router-link to="/login">登陆页</router-link>
<router-view />
</template>
具体业务使用router
<script>
import { useRouter, useRoute } from 'vue-router';
export default {
setup() {
const router = useRouter();
const route = useRoute();
// 获取路由携带的参数信息
console.log(route.query)
// 路由跳转
function toMainPage() {
router.push({
path: '/',
query: {
a: 1
}
})
}
return { toMainPage }
}
}
</script>
十九、EventBus与mitt
Vue2
中我们使用 EventBus
来实现跨组件之间的一些通信,它依赖于 Vue
自带的 $on/$emit/$off
等方法,这种方式使用非常简单方便,但如果使用不当也会带来难以维护的毁灭灾难。
而 Vue3
中移除了这些相关方法,这意味着 EventBus
这种方式我们使用不了, Vue3
推荐尽可能使用 props/emits
、provide/inject
、vuex
等其他方式来替代。
当然,如果 Vue3
内部的方式无法满足你,官方建议使用一些外部的辅助库,例如:mitt。
优点
- 非常小,压缩后仅有
200 bytes
。 - 完整
TS
支持,源码由TS
编码。 - 跨框架,它并不是只能用在
Vue
中,React
、JQ
等框架中也可以使用。 - 使用简单,仅有
on
、emit
、off
等少量实用API。
安装
npm install mitt --save
基本使用
新建 bus.js
文件:
import mitt from 'mitt';
const emiter = mitt();
export default emiter;
在 main.vue
组件中引入并监听:
<script>
import emitter from '../utils/bus.js'
export default {
setup() {
emitter.on('EventName', res => {
console.log(res)
});
}
}
</script>
在 login.vue
组件中引入并发送信息:
<template>
<button @click="send">点击发送信息</button>
</template>
<script>
import { onBeforeUnmount } from 'vue';
import emitter from '../utils/bus.js'
export default {
setup() {
function send() {
emitter.emit('EventName', '我发送个信息')
}
onBeforeUnmount(() => {
emitter.off('EventName');
});
return { send }
}
}
</script>
源码
export default function mitt(all) {
all = all || new Map();
return {
all: all,
on: function (type, handler) {
var handlers = all.get(type);
if (handlers) {
handlers.push(handler);
} else {
all.set(type, [handler]);
}
},
off: function (type, handler) {
var handlers = all.get(type);
if (handlers) {
if (handler) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1); // 无符号右移运算符
} else {
all.set(type, []);
}
}
},
emit: function (type, evt) {
var handlers = all.get(type);
if (handlers) {
handlers.slice().map(function (handler) {
handler(evt);
});
}
handlers = all.get("*");
if (handlers) {
handlers.slice().map(function (handler) {
handler(type, evt);
});
}
},
};
}
以上是精简之后的 JS
核心源码,整体来说并不是很难吧(✪ω✪),它围绕着发布订阅者模式而展开。
可能唯一让人感到陌生的代码是无符号右移运算符(>>>),关于它的作用小伙伴们就要自行去了解啦。
二十、Pinia
Pinia 是 Vue
官方团队成员专门开发的一个全新状态管理库,并且 Vue
的官方状态管理库已经更改为了 Pinia
。在 Vuex 官方仓库中也介绍说可以把 Pinia
当成是不同名称的 Vuex 5
,这也意味不会再出 5
版本了。
当然,
Pinia
可不是Vue3
专属,它同样也是适用于Vue2
的。
优点
- 更加轻量级,压缩后提交只有
1.6kb
。 - 完整的
TS
的支持,Pinia
源码完全由TS
编码完成。 - 移除
mutations
,只剩下state
、actions
、getters
。 - 没有了像
Vuex
那样的模块镶嵌结构,它只有store
概念,并支持多个store
,且都是互相独立隔离的。当然,你也可以手动从一个模块中导入另一个模块,来实现模块的镶嵌结构。 - 无需手动添加每个
store
,它的模块默认情况下创建就自动注册。 - 支持服务端渲染(
SSR
)。 - 支持
Vue DevTools
。 - 更友好的代码分割机制,传送门。
安装
npm install pinia --save
// or
npm i pinia -S
基本使用
初始化并挂载Pinia
我们还是根据老传统,在src
文件夹下,新建 store/index.js
文件:
import { createPinia } from 'pinia';
const pinia = createPinia();
export default pinia;
在 main.js
文件中进行全局挂载:
import { createApp } from 'vue';
import App from './App.vue';
import pinia from './store';
const app = createApp(App);
app.use(pinia);
app.mount('#app');
创建store
完成 Pinia
的初始化后,就可以来定义 store
进入具体使用了,而定义 store
一共有三种不同形式的写法,下面我们一一列举。
创建 src/store/modules/user.js
文件:
import { defineStore } from 'pinia';
// options API模式
export const usePersonStore = defineStore('person', {
state: () => {
return {
name: '人类'
}
},
actions: {
updateName(newName) {
this.name = newName;
}
},
getters: { // getters基本与Vue的计算属性一致
getFullName() {
return 'Full' + this.name;
}
}
})
// 对象形式
export const useTeacherStore = defineStore({
id: 'teacher',
state: () => {
return {
name: '老师'
}
},
actions: {
updateName(newName) {
this.name = newName;
}
},
getters: {
getFullName() {
return 'Full' + this.name;
}
}
})
// setup模式
import { computed, ref } from 'vue';
export const useStudentStore = defineStore('student', () => {
const name = ref('学生');
function updateName(newName) {
name.value = newName;
}
const getFullName = computed(() => 'Full' + name.value);
return { name, updateName, getFullName }
})
上面例子中,我们列举了同个 DEMO
不同的三种写法,它们效果都一样,第一和第二种类似,比较简单;第三种适合 Vue3
的 script setup
的形式。
需要我们注意的是其中的 id
是必填且需要唯一的。
具体业务使用store
在 App.vue
中使用:
<template>
<div>
<h1>Pinia</h1>
<div>state: {{personStore.name}} ****** getter: {{personStore.getFullName}}</div>
<div>state: {{studentStore.name}} ****** getter: {{studentStore.getFullName}}</div>
<div>state: {{teacherStore.name}} ****** getter: {{teacherStore.getFullName}}</div>
<button @click="updateName">点击</button>
</div>
</template>
<script>
import { usePersonStore, useStudentStore, useTeacherStore } from '@/store/modules/user.js';
export default {
setup() {
const personStore = usePersonStore();
const studentStore = useStudentStore();
const teacherStore = useTeacherStore();
function updateName() {
personStore.updateName('人类-updated')
studentStore.updateName('学生-updated')
teacherStore.updateName('老师-updated')
}
return {
personStore,
studentStore,
teacherStore,
updateName
}
}
}
</script>
上面我们通过 actions
来修改 state
,当然你也可以直接就去 state
,但是更推荐使用前者。actions
支持同步与异步,也支持 await
形式。
第三种修改state方法-$patch()
在 Pinia
中,一共有三种修改 state
的方式:
- 直接修改
state
。 - 通过
actions
修改。 - 通过
$patch
批量修改。
新建 src/store/modules/count.js
文件:
import { defineStore } from 'pinia';
export const useCountStore = defineStore('count', {
state: () => {
return {
count1: 0,
count2: 0,
count3: 0,
}
},
})
在 App.vue
中使用:
<template>
<div>
<h1>Pinia</h1>
<div>count1: {{countStore.count1}}</div>
<div>count2: {{countStore.count2}}</div>
<div>count3: {{countStore.count3}}</div>
<button @click="updateName">点击</button>
</div>
</template>
<script>
import { useCountStore } from '@/store/modules/count.js';
export default {
setup() {
const countStore = useCountStore();
function updateName() {
// 直接修改
// countStore.count1++;
// countStore.count2++;
// countStore.count3++;
// 通过$patch批量修改
countStore.$patch(state => {
state.count1++;
state.count2++;
state.count3++;
});
// $patch的另外一种形式
// countStore.$patch({
// count1: 2,
// count2: 2,
// count3: 2
// })
}
return { countStore, updateName }
}
}
</script>
上面我们展示了直接修改与通过 $patch()
方法进行批量修改的两种形式,它们效果一样。那它们有些什么更具体的区别呢?从网上的一些文章看到说通过 $patch()
修改 state
更具性能优势,也就是$patch()
方法是经过性能调优的,但是在 官网 却好像并没有找到相关介绍。如果小伙伴们有找到这方面的东西,欢迎给我留留言告知,感谢感谢。
Pinia解构方法-storeToRefs
上面的例子中,我们都是通过 .xxx
来访问数据,但是当很多数据被使用时,为了更简洁的使用这些数据,我们都会采用解构的方式来优化。
ES6
中普通的解构形式虽然能获取到值,但是却会失去响应式。
Pinia
为我们提供了 storetorefs API 方便一次性获取所有的数据,并且它们依旧是具备响应式的。
新建 src/store/modules/info.js
文件:
import { defineStore } from 'pinia';
export const useInfoStore = defineStore('info', {
state: () => {
return {
name: '橙某人',
age: 18
}
}
})
在 App.vue
文件中使用:
<template>
<div>
<h1>Pinia</h1>
<div>{{ name }}</div>
<div>{{ age }}</div>
<button @click="update">点击</button>
</div>
</template>
<script>
import { useInfoStore } from '@/store/modules/info.js';
import { storeToRefs } from 'pinia';
export default {
setup() {
const infoStore = useInfoStore();
const { name, age } = storeToRefs(infoStore); // 解构
function update() {
infoStore.name = 'YDYDYDQ';
infoStore.age = 20;
}
return { name, age, update }
}
}
</script>
store互相调用
新建 src/store/modules/other.js
文件:
import { defineStore } from 'pinia';
export const useOtherStore = defineStore('other', {
state: () => {
return {
hobby: ['吃饭', '睡觉', '打豆豆']
}
}
})
在 src/store/modules/info.js
文件中引入:
import { defineStore } from 'pinia';
import { useOtherStore } from './other';
export const useInfoStore = defineStore('info', {
state: () => {
return {
name: '橙某人',
age: 18
}
},
getters: {
getHobby() {
return useOtherStore().hobby // 获取爱好列表
}
}
})
数据持久化
项目中的状态管理一般我们需要把它进行数据持久化,否则一刷新就会造成数据丢失。而 Pinia
配套有个插件 pinia-plugin-persist 可以帮助我们来做这件事情。
安装
npm i pinia-plugin-persist -S
初始化
import { createPinia } from 'pinia';
import piniaPluginPersist from 'pinia-plugin-persist';
const pinia = createPinia();
pinia.use(piniaPluginPersist);
export default pinia;
在对应的 store
中开启缓存:
// info.js
import { defineStore } from 'pinia';
import { useOtherStore } from './other';
export const useInfoStore = defineStore('info', {
state: () => {
return {
name: '橙某人',
age: 18
}
},
getters: {
getHobby() {
return useOtherStore().hobby
}
},
persist: {
enabled: true, // 开启缓存
}
})
开启缓存后,默认数据是缓存在 sessionStorage
里面的。
也可以指定成 localStorage
作为缓存:
// info.js
import { defineStore } from 'pinia';
import { useOtherStore } from './other';
export const useInfoStore = defineStore('info', {
state: () => {
return {
name: '橙某人',
age: 18
}
},
getters: {
getHobby() {
return useOtherStore().hobby
}
},
persist: {
enabled: true, // 开启缓存
strategies: [
{
key: 'infoStore', // 修改缓存的key
storage: localStorage, // 指定localStorage作为缓存
paths: ['name'] // 只缓存name
}
]
}
})