对于React和Vue的单组件的Diff算法,实现方式相差不大。
一个组件的Rerender,由其 props,state,key 和标签类型等决定。
我们结合一个例子,看下什么样的 Rerender 是无意义的。
"有一个列表组件,选中列表的某一项卡片时,对其进行高亮"
下面是我们实现的一个Bad Case
App.vue
jsx
<script setup lang="ts">
import { ref, onMounted } from "vue";
import CardItem from "./components/Card/index.vue";
import { ICard } from "./components/Card/type";
const selectedId = ref("");
const cardList = ref<ICard[]>([]);
const observer = ref<IntersectionObserver>();
onMounted(() => {
let cards = [];
for (let i = 0; i < 3000; i++) {
const title = `Card title (${i})`;
const content = `This is card content (${i})`;
cards.push({
id: `${i}`,
title,
content,
render: true,
});
}
cardList.value = cards;
});
const onSelect = (id: string) => {
console.log("on select", id);
selectedId.value = id;
};
</script>
<template>
<div class="card-list" id="cardContainer">
<CardItem
v-for="item in cardList"
:observer="observer"
:key="item.id"
:item="item"
@on-select="onSelect"
:selected-id="selectedId"
/>
</div>
</template>
Card.vue
jsx
<script setup lang="ts">
import { ref, defineEmits, onUpdated } from "vue";
const cardRef = ref<HTMLDivElement>();
const props = defineProps({
item: Object,
selectedId: String,
observer: IntersectionObserver,
});
const emits = defineEmits(["on-select"]);
onUpdated(() => {
console.log("On Selected of Card: ", props.item?.id)
})
</script>
<template>
<div
class="card-item flex align-middle mb-4"
:class="{ selected: selectedId === props.item?.id }"
@click="emits('on-select', props.item?.id)"
>
<div class="card-item--right ml-8">
<div class="title">{{ props.item?.title }}</div>
<div class="content flex align-middle">
{{ props.item?.content }}
</div>
</div>
</div>
</template>
通过点击列表项,可以发现,每次的选中,都会触发列表中所有子项的更新。
在这个例子中,就出现了状态乱入,即把选中卡片的id
传递给了每个卡片,让卡片自己判断自己是否被选中(selectedId === props.item.id
),由于每次发生不同的选中,selectedId
都会发生变化,也即是每个卡片的props.selectedId
发生了变化,Diff
之后,每个卡片都需要更新。
我们称这种状态的更新是无意义的。
理论上,当一个卡片选中时,最多会有两个卡片会发生变化,一个卡片由选中变为不选中,一个卡片由不选中变为选中,而其他卡片则没有变化,也不应该发生更新。
既然明白了问题原因,我们可以调整状态的使用,对上面的例子进行优化
示例-状态提升优化
App.vue
在App组件中,遍历时计算每个卡片的选中状态,并将是否选中的bool
值作为props
传递给卡片组件
jsx
<template>
<div class="card-list" id="cardContainer">
<CardItem
v-for="item in cardList"
:observer="observer"
:key="item.id"
:item="item"
@on-select="onSelect"
:selected="item.id === selectedId"
/>
</div>
</template>
Card.vue
卡片组件通过props.selected
改变自身的状态
jsx
<script setup lang="ts">
import { defineEmits, onUpdated } from "vue";
const props = defineProps({
item: Object,
selected: Boolean,
observer: IntersectionObserver,
});
const emits = defineEmits(["on-select"]);
onUpdated(() => {
console.log("On Updated of Card: ", props.item?.id)
})
</script>
<template>
<div
class="card-item flex align-middle mb-4"
:class="{ selected: selected }"
@click="emits('on-select', props.item?.id)"
>
<div class="card-item--right ml-8">
<div class="title">{{ props.item?.title }}</div>
<div class="content flex align-middle">
{{ props.item?.content }}
</div>
</div>
</div>
</template>
<style scoped>
卡片的选中状态不再由组件自己计算,而是交给了父组件。
父组件计算完成选中状态之后,传递给卡片组件的props.selected
只会是true 或者 false
那么Diff的结果将会是:
卡片由选中变为不选中(true
-> false
)【diff props不相同,发生更新】
卡片由不选中变为选中(false
-> true
)【diff props不相同,发生更新】
卡片保持不选中(false
-> false
)【diff props相同,不发生更新】
因此,优化之后,每次的选中,最多只会使得两个组件发生更新,其他组件保持不变。
类似的场景还有很多,所以我们要理解单向数据流的好处,同时在做状态管理时,避免将无意义的状态传递给子组件或者子组件去监听很多可能自身并不完全需要的全局状态。避免无意义的渲染,以此提高渲染效率。