Vue 的渲染机制
如何理解虚拟 DOM?
虚拟 DOM 是一种编程概念,其含义为将目标所需的 UI 通过数据结构虚拟的表示出来,保存在内存中,然后将真实的 DOM 与之保持同步,虚拟 DOM 带来的主要收益是它让开发者可以灵活、声明的创建、检查和组合所需 UI 的结构,同时只需要把具体的 DOM 操作留给渲染器去处理。
Vue 组件挂载经历了哪些阶段?
编译Vue 模版被编译为渲染函数,用来返回虚拟 DOM 树,此步骤可以通过构建步骤提前完成,也可以使用运行时编译器即时完成。挂载运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建真实的 DMO 节点,这一步会做为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。更新当一个依赖发生变化后,副作用重新执行,创建一个更新后的虚拟 DOM 树,运行时渲染器遍历新树,将它与旧树比较,然后将必要的更新应用到真实 DOM。
Vue 编译器提高虚拟 DOM 运行性能做了哪些优化?
静态提升Vue 编译器会自动的将完全静态的虚拟 DOM 节点提升到渲染函数之外,并在每次渲染时使用相同的节点,渲染器在比较两个虚拟 DOM 树差异时知道此部分是完全相同的,会跳过此部分。更新类型标记Vue 模板被编译为渲染函数时,Vue 在虚拟 DOM 节点创建时为每个元素的更新类型进行了编码,一个元素可以有多个更新类型标记,最终会合并为一个数字,运行时渲染器通过位运算来检查这些标记,确定相应的更新操作。树结构打平Vue 模板被编译为渲染函数时,引入了区块的概念,认为内部结构是稳定的一个部分可被称之为一个区块,通过createElementBlock()按区块将虚拟 DOM 树打平为一个个数组,包含所有动态的后代节点,当一个组件需要重新渲染时,只需遍历打平后的虚拟 DOM 树,大大减少了的需要遍历的节点数量。
Vue 组合式 API 相关知识点
setup() 钩子函数
定义
setup 钩子函数是组件式 API 的入口。
使用场景
- 需要在非单文件组件中使用组合式 API 时。
- 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
Props
setup 钩子函数的第一个参数是组件的 props,且是响应式的,在传入新的 props 时同步更新,解构后会失去响应性,可通过将 props 传入到 toRefs() 和 toRef() 函数中来保持响应性。
import { toRefs, toRef } from 'vue';
export default {
setup(props) {
const { title } = toRefs(props);
// title 是一个追踪着 props.title 的 ref 对象
console.log(title.value);
}
};
上下文
setup 钩子函数的第二个参数是一个 Setup 上下文对象,上下文对象暴露了一些在 setup 中可能用到的值,上下文对象是非响应式的,可进行解构。
export default {
setup(props, { attrs, slots, emit, expose }) {
/**
1. attrs 透传属性,等价于 $attrs
2. slots 插槽,等价于 $slots
3. emit 触发事件,等价于 $emit
4. expose 暴露公共属性
5. attrs 与 slots 是有状态的对象,会跟随自身组件的更新而更新,
应始终通过attrs.x 与 slots.x 来访问其中的属性
**/
}
};
暴露公共属性
expose() 函数用于显式的限制该组件暴露出的属性,当父组件通过模版引用访问该组件实例时,仅能访问到 expose() 函数暴露出的属性。
import { ref } from 'vue';
export default {
setup(props, { expose }) {
const publicCount = ref(0);
const privateCount = ref(0);
expose({ count: publicCount });
}
};
响应式:核心函数
ref()
ref()函数接收一个任意值,返回一个响应式的、可更改的ref对象,此对象只有一个指向其内部值的属性.value。- 如果将一个对象赋值给
ref(),那么该对象将通过reactive()转为具有深层响应式的对象。
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value ++;
console.log(count.value); // 1
reactive()
reactive()函数接收一个对象,返回对象的响应式代理。- 对象的响应式转换是深层的,会影响到所有嵌套的属性,响应式对象会解包
ref对象,同时保持响应性。 - 响应式数组或 MAP 集合不会解包
ref对象。
import { reactive } from 'vue';
const obj = reactive({ count: 0 });
console.log(obj.count);
const obj = reactive({ count: ref(0) });
console.log(obj.count);
const list = reactive([ref(0)]);
console.log(list[0].value);
const map = reactive(new Map());
map.set('count', ref(0));
console.log(map.get('count').value);
readonly()
readonly()函数接收一个对象或一个ref对象,返回原值的只读代理。- 对任何嵌套属性的访问也是只读的,同时也会解包
ref对象,解包逻辑同reactive()相同,解包得到的值也是只读的。
import { readonly } from 'vue';
const obj = readonly({ count: ref(1)});
console.log(obj.count);
computed()
computed()接收一个getter函数,返回一个只读的ref对象。该ref对象通过.value暴露getter函数的返回值。computed()也可以接收一个带有get函数和set函数的对象来返回一个可写的ref对象。
import { ref, computed } from 'vue';
const count = ref(0);
const plusOne = computed({
get: () => count.value,
set: val => count.value = val
});
plusOne.value = 1;
console.log(plusOne.value);
watchEffect()
watchEffect()函数接收一个副作用函数及一个可选的配置项,并立即执行接收的副作用函数,同时响应式的追踪所依赖的属性,并在依赖更新时重新执行,副作用函数的参数也是一个函数,用来注册清理回调。- 可选的配置项用来调整副作用函数的刷新时机或调试副作用的依赖。
import { ref, watchEffect } from 'vue';
const count = ref(0);
const stop = watchEffect(() => {
console.log(count.value)
}, {
'flush?': 'pre', // pre | post | sync // 调整回调函数的刷新时机
onTrack?: (e) => {},
onTrigger?: (e) => {}
});
count.value++;
stop(); // 停止侦听器
watch()
watch()函数接收三个参数,第一个参数为侦听的数据源,第二个参数为副作用函数,第三个参数为可选的配置项。- 第一个参数可以是ref对象、响应式对象、一个函数(返回一个值)或者由这三个类型的值组成的数组。
- 第二个参数接收三个参数,分别为新值、旧值、清理副作用函数的回调函数。
- 可选的配置项用来调整副作用函数的刷新时机或调试副作用的依赖。
import { watch } from 'vue';
const stop = watch([fooRef, barRef], ([foo, bar], [preFoo, preBar]) => {}, {
'immediate?': true, true | false // 在侦听器创建时立即触发回调
'deep?': true, true | false, // 如果数据源是对象,强制深度遍历
'flush?': 'pre', // pre | post | sync // 调整回调函数的刷新时机
onTrack?: (e) => {},
onTrigger?: (e) => {}
});
stop(); // 停止侦听器
watchEffect() 与 watch() 的区别
watch()函数懒执行副作用函数。watch()函数更加明确应该由哪个状态触发侦听器重新执行。watch()函数可以访问所侦听状态的前一个值和当前值。
响应式:工具函数
isRef()检查某个值是否为 ref。unref()如果参数是 ref,则返回内部值,否则返回参数本身。toRef()基于响应式对象上的一个属性,创建一个对应的 ref。toRefs()将一个响应式对象转换为一个普通对象,每个属性都是指向源对象相应属性的 ref,每个 ref 都是使用toRef()创建的。isProxy()检查一个对象是否是由reactive()或readonly()或shallowReactive()或shallowReadonly()创建的代理。isReactive()检查一个对象是否是由reactive()或shallowReactive()创建的代理。isReadonly()检查传入的值是否为只读对象。
toRef() 与 toRefs() 的区别
toRef()函数即使源对象上的属性不存在,也会返回一个可用的 ref,在处理可选的 props 时很实用。toRefs()函数只会为源对象上已存在的属性创建 ref。
生命周期钩子函数
onBeforeMount()注册一个钩子,在组件被挂载之前被调用。onMounted()注册一个回调函数,在组件挂载完成后执行。onBeforeUpdate()注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。onUpdated()注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。onBeforeUnmount()注册一个钩子,在组件实例被卸载之前调用。onUnmounted()注册一个回调函数,在组件实例被卸载之后调用。onActivated()注册一个回调函数,若组件实例是<KeepAlive>缓存树的一部分,当组件被插入到 DOM 中时调用。onDeactivated()注册一个回调函数,若组件实例是<KeepAlive>缓存树的一部分,当组件从 DOM 中被移除时调用。
依赖注入
provide() 函数提供一个值,可以被后代组件注入,接收两个参数,第一个参数是要注入的 key,可以是一个字符串或一个 symbol,第二个参数是要注入的值。
inject()函数注入一个由祖先组件或整个应用提供的值,接收三个参数,第一个参数是要注入的 key,第二个参数是在没有匹配到 key 时使用的默认值,也可以是一个工厂函数,第三个参数当默认是是一个函数时需设置为false。
透传 Attributes
定义
透传 Attributes 指的是传递给一个组件的 attribute,没有被组件声明为 props 或 emits,例如 id、class、style 等。
自动继承透传 attribute
当一个组件以单个元素为根作渲染时,透传 attribute 会自动被添加到根元素上。
1. 普通继承
// MyButton 组件模版
<button>click me</button>
// 使用 MyButton 组件
<MyButton class="large" />
// 渲染结果
<button class="large">click me</button>
2. 合并 class 与 style
// MyButton 组件模版
<button class="btn">click me</button>
// 使用 MyButton 组件
<MyButton class="large" />
// 渲染结果
<button class="btn large">click me</button>
3. 深度继承
// MyButton 组件模版
<BaseButton /> // 此时 <MyButton> 接收的透传 attribute 会直接继续传给 <BaseButton> 组件
禁用透传 attribute 自动继承
<script>
export default {
inheritAttrs: false
};
</script>
注:禁用透传 attribute 自动继承后,可以通过 $attrs 属性来控制透传 attribute 被如何使用。
多根节点继承
多个根节点的组件没有自动继承透传 attribute 的行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
访问透传 attribute
<script setup>
import { useAttrs } from 'vue';
const attrs = useAttrs();
</script>
<script>
export default {
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs
console.log(ctx.attrs);
}
};
</script>
插槽
定义
插槽用来在子组件中渲染父组件传进来的模版片段。
内容与出口
// MyButton 组件模版
<button>
<slot></slot> 插槽出口
</button>
<MyButton>
Click me! 插槽内容
</MyButton>
默认内容
// MyButton 组件模版
<button>
<slot>组件内部信息</slot>
</button>
<MyButton /> // 插槽内会渲染“组件内部信息”
<MyButton>组件外部信息</MyButton> // 插槽内会渲染“组件外部信息”
具名插槽
// BaseLayout 组件模版
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
<template v-slot:default>
<!-- main 插槽的内容放这里 -->
</template>
<template v-slot:footer>
<!-- footer 插槽的内容放这里 -->
</template>
</BaseLayout>
注:v-slot:header 可简写为 #header。
作用域插槽
// BaseLayout 组件模版
<div>
<header>
<slot name="header" message="hello header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer" message="hello footer"></slot>
</footer>
</div>
<BaseLayout>
<template v-slot:header="headerProps">
{{ headerProps.message }}
</template>
<template v-slot:default>
</template>
<template v-slot:footer="{ message }">
{{ message }}
</template>
</BaseLayout>