Vue 3 学习日志

188 阅读3分钟

Vue 的渲染机制

如何理解虚拟 DOM?

虚拟 DOM 是一种编程概念,其含义为将目标所需的 UI 通过数据结构虚拟的表示出来,保存在内存中,然后将真实的 DOM 与之保持同步,虚拟 DOM 带来的主要收益是它让开发者可以灵活、声明的创建、检查和组合所需 UI 的结构,同时只需要把具体的 DOM 操作留给渲染器去处理。

Vue 组件挂载经历了哪些阶段?

  1. 编译 Vue 模版被编译为 渲染函数,用来返回虚拟 DOM 树,此步骤可以通过构建步骤提前完成,也可以使用运行时编译器即时完成。
  2. 挂载 运行时渲染器调用 渲染函数,遍历返回的虚拟 DOM 树,并基于它创建真实的 DMO 节点,这一步会做为 响应式副作用 执行,因此它会追踪其中所用到的所有响应式依赖。
  3. 更新 当一个依赖发生变化后,副作用重新执行,创建一个更新后的虚拟 DOM 树,运行时渲染器遍历新树,将它与旧树比较,然后将必要的更新应用到真实 DOM。

Vue 编译器提高虚拟 DOM 运行性能做了哪些优化?

  1. 静态提升 Vue 编译器会自动的将完全静态的虚拟 DOM 节点提升到渲染函数之外,并在每次渲染时使用相同的节点,渲染器在比较两个虚拟 DOM 树差异时知道此部分是完全相同的,会跳过此部分。
  2. 更新类型标记 Vue 模板被编译为渲染函数时,Vue 在虚拟 DOM 节点创建时为每个元素的更新类型进行了编码,一个元素可以有多个更新类型标记,最终会合并为一个数字,运行时渲染器通过 位运算 来检查这些标记,确定相应的更新操作。
  3. 树结构打平 Vue 模板被编译为渲染函数时,引入了区块的概念,认为内部结构是稳定的一个部分可被称之为一个区块,通过 createElementBlock() 按区块将虚拟 DOM 树打平为一个个数组,包含所有动态的后代节点,当一个组件需要重新渲染时,只需遍历打平后的虚拟 DOM 树,大大减少了的需要遍历的节点数量。

Vue 组合式 API 相关知识点

setup() 钩子函数

定义

setup 钩子函数是组件式 API 的入口。

使用场景

  1. 需要在非单文件组件中使用组合式 API 时。
  2. 需要在基于选项式 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()

  1. ref() 函数接收一个任意值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
  2. 如果将一个对象赋值给 ref(),那么该对象将通过 reactive() 转为具有深层响应式的对象。
import { ref } from 'vue';

const count = ref(0);

console.log(count.value); // 0

count.value ++;
console.log(count.value); // 1

reactive()

  1. reactive() 函数接收一个对象,返回对象的响应式代理。
  2. 对象的响应式转换是深层的,会影响到所有嵌套的属性,响应式对象会解包 ref 对象,同时保持响应性。
  3. 响应式数组或 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()

  1. readonly() 函数接收一个对象或一个 ref 对象,返回原值的只读代理。
  2. 对任何嵌套属性的访问也是只读的,同时也会解包 ref 对象,解包逻辑同 reactive() 相同,解包得到的值也是只读的。
import { readonly } from 'vue';

const obj = readonly({ count: ref(1)});
console.log(obj.count);

computed()

  1. computed() 接收一个 getter 函数,返回一个只读的 ref 对象。该 ref 对象通过 .value 暴露 getter 函数的返回值。
  2. 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()

  1. watchEffect() 函数接收一个副作用函数及一个可选的配置项,并立即执行接收的副作用函数,同时响应式的追踪所依赖的属性,并在依赖更新时重新执行,副作用函数的参数也是一个函数,用来注册清理回调。
  2. 可选的配置项用来调整副作用函数的刷新时机或调试副作用的依赖。
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()

  1. watch() 函数接收三个参数,第一个参数为侦听的数据源,第二个参数为副作用函数,第三个参数为可选的配置项。
  2. 第一个参数可以是ref对象、响应式对象、一个函数(返回一个值)或者由这三个类型的值组成的数组。
  3. 第二个参数接收三个参数,分别为新值、旧值、清理副作用函数的回调函数。
  4. 可选的配置项用来调整副作用函数的刷新时机或调试副作用的依赖。
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() 的区别

  1. watch() 函数懒执行副作用函数。
  2. watch() 函数更加明确应该由哪个状态触发侦听器重新执行。
  3. watch() 函数可以访问所侦听状态的前一个值和当前值。

响应式:工具函数

  1. isRef() 检查某个值是否为 ref。
  2. unref() 如果参数是 ref,则返回内部值,否则返回参数本身。
  3. toRef() 基于响应式对象上的一个属性,创建一个对应的 ref。
  4. toRefs() 将一个响应式对象转换为一个普通对象,每个属性都是指向源对象相应属性的 ref,每个 ref 都是使用 toRef() 创建的。
  5. isProxy() 检查一个对象是否是由 reactive()readonly()shallowReactive()shallowReadonly() 创建的代理。
  6. isReactive() 检查一个对象是否是由 reactive()shallowReactive() 创建的代理。
  7. isReadonly() 检查传入的值是否为只读对象。

toRef() 与 toRefs() 的区别

  1. toRef() 函数即使源对象上的属性不存在,也会返回一个可用的 ref,在处理可选的 props 时很实用。
  2. toRefs() 函数只会为源对象上已存在的属性创建 ref。

生命周期钩子函数

  1. onBeforeMount() 注册一个钩子,在组件被挂载之前被调用。
  2. onMounted() 注册一个回调函数,在组件挂载完成后执行。
  3. onBeforeUpdate() 注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。
  4. onUpdated() 注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。
  5. onBeforeUnmount() 注册一个钩子,在组件实例被卸载之前调用。
  6. onUnmounted() 注册一个回调函数,在组件实例被卸载之后调用。
  7. onActivated() 注册一个回调函数,若组件实例是 <KeepAlive> 缓存树的一部分,当组件被插入到 DOM 中时调用。
  8. onDeactivated() 注册一个回调函数,若组件实例是 <KeepAlive> 缓存树的一部分,当组件从 DOM 中被移除时调用。

依赖注入

provide() 函数提供一个值,可以被后代组件注入,接收两个参数,第一个参数是要注入的 key,可以是一个字符串或一个 symbol,第二个参数是要注入的值。

inject()函数注入一个由祖先组件或整个应用提供的值,接收三个参数,第一个参数是要注入的 key,第二个参数是在没有匹配到 key 时使用的默认值,也可以是一个工厂函数,第三个参数当默认是是一个函数时需设置为false。

透传 Attributes

定义

透传 Attributes 指的是传递给一个组件的 attribute,没有被组件声明为 propsemits,例如 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>