在日常的Vue 3开发中,大家往往对 ref、reactive、watch、computed 等基础API信手拈来。然而,Vue 3的生态中其实隐藏着不少"宝藏"方法。这些API虽然相对冷门,但在特定的业务场景下,它们能极大地优化代码结构、提升应用性能,甚至解决一些令人头疼的痛点。
无脑使用ref和reactive?看看shallowRef和shallowReactive
只有一层的监听,对象内部所有属性的变化不会触发更新。
例如:接口查到列表数据并展示在页面上,逻辑是这样写的:
js
const list = ref([])
async function getList() {
const res = await apiGetData() // 调用接口获取数据
list.value = res?.list || []
}
我们发现,针对list的改动,都是对list.value整体进行赋值,因此,我们可以使用shallowRef替换ref,这样Vue就不会去监听里面的每一层属性,从而提升性能。
js
const list = shallowRef([])
async function getList() {
list.value = await apiGetList() // 调用接口获取数据
}
对reactive的处理也是类似:
js
const userObj = shallowReactive({
user: {
name: '',
age: ''
},
loginFlag: false
})
async function getUserData() {
const res = await apiUserList() // 调用接口获取数据
userObj.user = res.user
userObj.loginFlag = true
}
在这里,我们的 userObj.user是统一更新的,因此可以使用shallowReactive来提升性能。
js
// 执行这个方法,数据变了,页面没变
function addItem() {
let len = list.value.length
list.value.push({
label: `测试数据${len + 1}`,
value: `${len + 1}`
})
}
// 执行这个方法,数据变了,页面没变
function addAge() {
userObj.user.age = userObj.user.age + 1
}
// 执行这个方法,数据变了,页面同步变化,并且上面两个方法执行后导致的数据与页面不同步,会在此时同步更新
function triggerLogin() {
userObj.loginFlag = !userObj.loginFlag
}
不需要被响应:markRaw
js
import { reactive, markRaw } from 'vue'
import * as echarts from 'echarts'
const rawChart = markRaw(echarts.init(document.getElementById('chart')))
原理是给markRaw的数据添加上__v_skip标记。
js
function markRaw(value) {
if (Object.isExtensible(value)) {
def(value, "__v_skip", true);
}
return value;
}
在创建响应式对象的时候,会忽略带标记的数据:
js
function getTargetType(value) {
return value["__v_skip"] || !Object.isExtensible(value) ? 0 /* INVALID */ : targetTypeMap(toRawType(value));
}
function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) {
if (!shared.isObject(target)) {
{
warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) {
return target;
}
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const targetType = getTargetType(target);
if (targetType === 0 /* INVALID */) {
return target;
}
const proxy = new Proxy(
target,
targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers
);
proxyMap.set(target, proxy);
return proxy;
}
不想使用pinia?直接使用provide和inject进行跨组件通信
使用方式就和状态管理工具mobx类似,
根组件定义provide:
js
const flag = ref(false)
provide('finished', flag)
子组件访问:
js
const status = inject('finished'); // 访问父组件的flag,子组件的
子组件修改这个共享变量,修改后每一个用到的地方都会同步修改并且更新页面。
js
const status = inject('finished')
function triggerFinished() {
status.value = !finished.value
}
Vue组件内部的数据转换:toRefs、toRef、toRaw
js
// 假设这里有A-G七科成绩,需要展示出来
const score1 = ref({
A: 100,
B: 99,
C: 98,
D: 97,
E: 96,
F: 95,
G: 94
})
const score2 = reactive({
A: 100,
B: 99,
C: 98,
D: 97,
E: 96,
F: 95,
G: 94
})
我们只能通过score1.value.A、score2.A去访问,如果数据很多或者层级很深,我们访问元素属性的链路就会特别长。
html
<div>A:{{ score1.A }} </div>
<div>B:{{ score1.B }} </div>
<div>C:{{ score1.C }} </div>
<div>D:{{ score1.D }} </div>
<div>E:{{ score1.E }} </div>
<div>F:{{ score1.F }} </div>
<div>G:{{ score1.G }} </div>
<div> </div>
<div>A:{{ score2.A }} </div>
<div>B:{{ score2.B }} </div>
<div>C:{{ score2.C }} </div>
<div>D:{{ score2.D }} </div>
<div>E:{{ score2.E }} </div>
<div>F:{{ score2.F }} </div>
<div>G:{{ score2.G }} </div>
我们可以使用toRefs去把用ref或reactive定义的对象进行解构,会生成一个普通对象,普通对象的每一个属性都是响应式对象。把这些对象的属性单独生成响应式对象。
js
const { A, B, C, D, E, F, G } = toRefs(score1.value)
const { A, B, C, D, E, F, G } = toRefs(unref(score1))
const { A, B, C, D, E, F, G } = toRefs((score2))
A.value = 90 // 同步发生变化
如果我们不需要处理所有的属性,可以使用toRef进行单个属性处理
js
const GG = toRef(score1.value, 'G')
const GG = toRef(unref(score1), 'G')
const GG = toRef(score2, 'G')
GG.value = 80 // 同步发生变化
使用场景:某些场景下可以当computed用
js
const GG = computed(() => score2.G)
但是我们一般不会把computed放在v-model里,为了确保触发数据变化的来源唯一。
这种情况下,就适合使用toRef了,如果对象层次非常深,可读性更好。
html
<a-input v-model="GG"></a-input>
<a-input v-model="score2.G"></a-input>
toRaw:把响应式对象转成原始对象
js
const originData = toRaw(score1.value)
const originData = toRaw(unref(score1))
const originData = toRaw(score2)
originData.A++ // 原始数据改变,数据会变,但是页面不更新,等到下次有触发页面更新是,会将此次改动同步,实际上我们不会这样操作,只是演示一下这样操作的后果,减少出现难以理解的问题。
toRaw 的作用:我们知道,Vue3是通过proxy对数据进行的劫持,因此对proxy对象的读取(get)、修改(set)、删除(deleteProperty)等操作都会触发一些响应式逻辑。处理大量数据或者复杂操作时,使用toRaw处理后,我们可以直接访问原始对象,避免不必要的响应式开销,从而提升性能。
事实上,大部分的场景是使用cloneDeep方法把对象复制一份,进行处理,避免出现隐式操作不小心改了响应式对象。
h函数
h函数处理dom:
js
const style = { 'text-align': 'left', 'text-overflow': 'ellipsis', 'white-space': 'nowrap', overflow: 'hidden' };
customRender: ({ record }) => {
return h('div', { style }, record.nd);
}
h函数处理组件:
js
customRender: ({ record }) => {
if (!Reflect.has(record, 'pendingStatus')) {
record.pendingStatus = false
}
return h(Switch, {
checked: record.status == '1',
checkedChildren: '已启用',
unCheckedChildren: '已停用',
loading: record.pendingStatus,
onChange(checked, e) {
e.stopPropagation() // 直接点击按钮的时候避免触发选中事件
record.pendingStatus = true;
const newStatus = checked ? '1' : '0';
changeStatus({
status: newStatus,
id: record.id
}).then(() => {
record.status = newStatus;
}).finally(() => {
record.pendingStatus = false;
});
},
});
}
嵌套处理:
html
<a-button @click.stop="handlePreview(text)">
<Icon icon="bi:eye" />
<span>{{ text.length }}</span>
</a-button>
js
let h1 = h(Icon, {
icon: 'bi:eye'
})
let h2 = h('span', {}, record[item.zdm].length)
return h(Button, { onClick: () => handlePreview(record[item.zdm]) }, [h1, h2])