跳到主要内容

组件✅

本主题包含对 Vue 组件概念的深入考察,涵盖组件的定义、类型、生命周期、通信策略等方面的内容。

Vue 的组件有哪些类型?

答案

组件的类型 Vue 下组件按照有无状态,加载方式分为

  1. 函数式组件 函数组件无任何状态就是一个纯函数,接受属性等配置,返回一个 UI 片段,函数组件不支持各种 hook
    • Vue 2.x 中单文件组件通过定义 functional 属性,或者在配置中添加 functional: true 来申明为函数组件,注意该策略在 Vue 3.x 中不再支持。
    • Vue 3.x 中函数式组件直接是一个 function
  2. 状态组件(Stateless Component)Vue 3.x 中非函数组件都可以称为状态组件,状态组件支持各种 hook 和生命周期函数。可以采用选项式 API 或组合式 API 来定义。
  3. 异步组件 采用 defineAsyncComponent 来定义的组件,异步组件会在需要时才加载,异步组件可以是函数式组件或状态组件。
  4. 动态组件 通过 <component> 标签来定义的组件,动态组件会根据 is 属性的值来加载不同的组件。动态组件可以是函数式组件或状态组件。

示例说明

<script setup>
import { ref } from 'vue'
import FunctionalComponent from './FunctionalComponent.vue'
import StatefulComponent from './StatefulComponent.vue'
import DynamicComponent from './DynamicComponent.vue'
import AsyncComponent from './AsyncComponent.vue'

const showAsync = ref(false)
</script>

<template>
  <h2>1. 函数组件</h2>
  <FunctionalComponent msg="Hello from App" />

  <h2>2. 状态组件</h2>
  <StatefulComponent />

  <h2>3. 动态组件</h2>
  <DynamicComponent />

  <h2>4. 异步组件</h2>
  <button @click="showAsync = !showAsync">
    {{ showAsync ? '隐藏' : '加载' }}异步组件
  </button>
  <div v-if="showAsync">
    <AsyncComponent />
  </div>

组件生命周期?

答案

见官方生命周期钩子

Vue 组件生命周期

核心钩子说明

选型式API组合API时机备注
beforeCreate采用 setup 模拟,注意 setup 在 beforeCreate 之前触发props 解析完成组件实例完成初始化触发接口调用、副作用绑定等
created采用 setup 模拟,注意 setup 在 beforeCreate 之前触发组件实例完成初始化,还未挂载触发接口调用、副作用绑定等
beforeMountonBeforeMount()组件挂载前,DOM 还未渲染挂载前准备工作
mountedonMounted()组件挂载后,DOM 已渲染DOM 操作
beforeUpdateonBeforeUpdate组件数据更新,DOM 树更新前保存之前dom状态,例如滚动回到之前
updatedonUpdated组件数据更新,DOM 树更新后-
beforeUnmountonBeforeUnmount组件卸载前清理资源或事件
unmountedonUnmounted组件卸载后完成清理工作、性能或组件销毁打点

此外还支持如下钩子

选型式API组合API时机备注
errorCapturedonErrorCaptured()捕获后代组件传递的错误时调用,注意无法捕获 setup 中的异步错误错误监控与处理
renderTrackedonRenderTracked()响应式依赖被组件的渲染作用追踪后调用调试依赖追踪,仅 dev 可用
renderTriggeredonRenderTriggered()响应式依赖被组件触发了重新渲染之后调试渲染触发,仅 dev 可用
activatedonActivated()组件实例是 <KeepAlive> 缓存树的一部分,当组件被插入到 DOM 中时调用。缓存组件激活逻辑
deactivatedonDeactivated()组件实例是 <KeepAlive> 缓存树的一部分,当组件从 DOM 中被移除时调用。缓存组件停用逻辑
serverPrefetchonServerPrefetch()当组件实例在服务器上被渲染之前要完成的异步函数。服务端数据预取,仅 SSR 可用

示例说明

<template>
  <div>
    <h1>生命周期钩子验证</h1>
    <button @click="toggleComponent">切换组件</button>
    <component :is="toggle ? OptionHook : CompositionComponent" />
  </div>
</template>

<script setup>
  import OptionHook from "./OptionHook.vue";
  import CompositionComponent from "./CompositionHook.vue";
  import { ref } from "vue";

  const toggle = ref(false);

  function toggleComponent() {
    toggle.value ^= 1;
  }
</script>

组件包含哪些核心选项?

答案

详见 官方 options api

分类选项描述
状态选项data定义组件的响应式数据。
props定义组件的外部属性。
computed定义计算属性,基于其他响应式数据计算得出。
methods定义组件的方法。
watch监听响应式数据的变化并执行回调。
emits定义组件可以抛出的事件。
expose定义组件实例暴露的属性或方法。
渲染选项template定义组件的模板。
render定义组件的渲染函数。
compilerOptions配置模板编译选项。
slots定义插槽内容。
生命周期beforeCreatecreatedbeforeMountmounted组件的创建和挂载阶段的生命周期钩子。
beforeUpdateupdated组件更新阶段的生命周期钩子。
beforeUnmountunmounted组件卸载阶段的生命周期钩子。
errorCapturedrenderTrackedrenderTriggered错误捕获和渲染追踪相关的生命周期钩子。
activateddeactivated<KeepAlive> 缓存组件的激活和停用钩子。
serverPrefetch服务端渲染时的预取数据钩子,仅 SSR 可用。
组合选项provide,inject提供数据给后代组件消费
mixins混入复用的组件选项,注意 mixin 是 merge 操作。
extends继承另一个组件的选项。
其他杂项name定义组件的名称。
inheritAttrs是否继承未被显式声明的属性。
components注册局部组件。
directives注册局部指令。
组件实例$data$props$el$options组件实例的属性。
$parent$root父组件和根组件实例。
$slots$refs插槽内容和子组件引用。
$attrs未被声明的特性集合。
$watch()$emit()$forceUpdate()$nextTick()实例方法,用于监听、触发事件、强制更新和延迟执行回调。

什么是单文件组件, 如何使用?

答案
  1. 单文件组件是 Vue.js 中一种用于组织组件的文件格式,它将组件的模板、脚本和样式封装在一个文件中,通常以 .vue 为扩展名。通过单文件组件,可以更方便地管理和维护组件的结构和样式。
  2. 单文件组件支持如下语言块
    • <template>:定义组件的 HTML 模板,使用 Vue 的模板语法。做多只能包含一个
    • <script>:定义组件的 JavaScript 逻辑,使用 Vue 的选项式 API 或组合式 API。最多只能包含一个,如果使用 <script setup> 则每种只能存在一个
    • <style>:定义组件的 CSS 样式,可以使用 scoped 属性来限制样式的作用范围,可以包含多个块
    • 自定义块, 比如 <docs> 等,自定义块可以通过 Vite 或 Webpack 的 loader 来处理自定义块,详见 官方说明
  3. 单文件中个语言块支持如下功能
    1. lang 配置不同语言块的预处理器,比如
      1. <script lang="ts">:使用 TypeScript 作为脚本语言
      2. <style lang="scss">:使用 SCSS 作为样式语言
      3. <template lang="pug">:使用 Pug 作为模板语言
    2. src 支持从外部导入文件,比如
      1. <script src="./utils.js">:导入外部 JavaScript 文件
      2. <style src="./styles.css">:导入外部 CSS 文件
      3. <template src="./template.html">:导入外部 HTML 模板
      4. <unit-test src="./unit-test.js"> 自定义块也支持 source 导入

有用过 <script setup> 吗, 它和 option 方式有什么区别?

答案

<script setup> 是 Vue 3.x 中引入的一种新的语法糖,用于简化采用 Composition API 的组件编写方式。相比普通的 <script> 标签,优势如下

  1. 更简洁的语法<script setup> 省略了 export defaultsetup() 函数的定义,以及需要手动申明 components 状态等逻辑,编写方式更加简单直观。
  2. 更好的类型推导<script setup> 提供了更好的 TypeScript 支持,自动推导 props 和 emits 的类型,减少了手动定义的工作量。
  3. 更好的性能 <script setup> setup 中的内容会编译为同一作用域的函数,相比采用 options render 需要使用代理消费 $data 等属性,性能更优,这也是为什么模版可以直接使用 setup 中定义的变量、函数、组件而无需显示绑定的原因。具体示例可参看 sfc playground 中编译的输出即可理解。
  4. setup 支持顶层异步 await, 这会被编译为 async setup 函数

setup 中提供了一系列工具方法,来简化类型等定义,具体如下

|方法|功能| defineProps<T>()|定义组件的 props,支持类型推导| withDefaults(defineProps<T>(), defaultValues)|定义组件的 props 的默认值| defineEmits<T>()|定义组件的抛出的事件,支持类型推导| defineModel<T>()|简化双向绑定的 prop 定义, 3.4+ 支持| defineExpose()|用来暴露组件的属性| defineOptions()|暴露组件其他配置,3.3+ 支持,解决之前需要单独定义 script 配置 name、inheritAttrs等问题| defineSlots<T>()|定义插槽类型,和 useSlots 返回一致| useSlots() 和 useAttrs()|访问插槽内容和 attrs, 和 setupContext.slots 和 setUpContext.attrs 等价|

延伸阅读

<style scope> 是怎么做的样式隔离的

答案

<style scoped> 是 Vue 单文件组件中的一个特性,用于限制样式的作用范围,使其仅应用于当前组件的元素。这样可以避免样式冲突和全局污染。

Vue 在编译带有 scoped 属性的 <style> 标签时,会按照以下步骤处理样式隔离:

  1. ID 属性生成:Vue 为每个带有 scoped 属性的组件生成一个唯一的作用域 ID, 绑定在组件对象 __scopeId 属性上(如data-v-abcdef)。
  2. 组件元素绑定 ID:组件模板的所有元素上都会添加一个属性 data-v-abcdef
  3. 选择器绑定:会在选择器末尾追加属性选择器,以确保样式仅应用于当前组件的元素。例如,如果 CSS 规则是 .button { color: red; },并且作用域 ID 是 data-v-abcdef,那么该规则会被转换成 .button[data-v-abcdef] { color: red; }

可以参考 Vue SFC Playground 查看示例编译后的 JS 和 CSS 。

什么是函数组件,和 JSX、渲染函数有什么关系?

答案
  1. 函数组件 是一个纯函数,通常用于创建简单的、无状态的组件。由于组件无状态,所以没有生命周期钩子,也没有响应式数据。

  2. JSX 是一种 JavaScript 语法扩展,它允许你在 JavaScript 中编写类似 HTML 的代码。JSX 语法通常与 React 一起使用,但 Vue 也支持 JSX。Vue 组件可以使用 JSX 来描述组件的结构和样式。JSX 语法会被 loader 转换为渲染函数。

  3. 渲染函数 动态创建组件的函数,内部通常采用 [h()](https://cn.vuejs.org/api/render-function.html#h) 函数,本质上模版编写的内容也会被转换为渲染函数。

    // 渲染函数完整参数如下
    function h(
    type: string | Component,
    props?: object | null,
    children?: Children | Slot | Slots
    ): VNode

总结函数组件用来定义无状态的组件。渲染函数,通过消费 h 手动接创建 vnode 节点,JSX 和单文件中的模版本质是简化渲染函数编写的语法糖。

提示

此外对于有状态的组件,Vue 支持如下 渲染选项 来提供手动编写渲染函数能力。

  • render 动态创建组件的配置属性,本质上采用 SFC 编写 template 片段最终都会被转换渲染函数
  • template 选项是一个静态字符串。注意如果组件同时配置了 templaterender 选项,render 选项会覆盖 template 选项。 此外 template 选项是在运行时编译的,而非构建阶段
  • slots 实际上插槽也是一系列渲染函数,其中每个 key 对应这个插槽的名称,值是一个渲染函数

此外 setup 也支持返回一个渲染函数

示例说明

延伸阅读

  • 渲染机制 官方文档了解整个渲染流程
  • 渲染函数 官方文档了解渲染函数的使用
  • 模版预览 可以通过该站点查看模版被编译为渲染函数后的结果

JSX 和采用单文件组件编写有哪些区别?

答案
  1. vue 模版指令需要转换为 jsx 表达式,自定义指令采用 withDirectives 进行包裹,此外 html 属性可以直接传递不需要转换为驼峰式命名,比如 classfor
模版语法转换
template
<div v-if="isShow">hello world</div>
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
<div @click="handleClick">hello world</div>
<div @click.capture="handleCapture">hover capture</div>
jsx
<div>{isShow ? 'hello world' : ''}</div>
<div>{list.map(item => <div key={item.id}>{item.name}</div>)}</div>
<div onClick={handleClick}>hello world</div>
<div onClickCapture={handleCapture}>hover capture</div>
  1. 插槽采用 slots 替换
插槽语法转换
template
<!-- ChildComponent 定义插槽 -->
<div>
<slot></slot>
<slot name="namedSlot" :data="data"></slot>
</div>

<!-- 消费 ChildComponent -->
<template>
<ChildComponent>
<template #default>
<div>Default Slot Content</div>
</template>
<template #namedSlot="{ data }">
<div>Named Slot Content: {{ data }}</div>
</template>
</ChildComponent>
</template>
jsx
// ChildComponent 定义插槽
<div>
{slots.default()}
{slots.namedSlot({ data })}
</div>

// 消费 ChildComponent
<template>
<ChildComponent>
{{
default: () => <div>Default Slot Content</div>,
namedSlot: ({ data }) => <div>Named Slot Content: {data}</div>
}}
</ChildComponent>
</template>
  1. 内置组件不在是全局,需要导入后使用例如 KeepAlive 等, 需要导入 import { KeepAlive } from 'vue'
  2. v-model 语法糖需要转换为 jsx 属性和时间注入
v-model 语法转换
template
<template>
<input v-model="value" />
</template>
jsx
<template>
<input value={value} onInput={e => (value = e.target.value)} />
</template>

延伸阅读

什么是异步组件,是如何实现的?

答案
  1. 异步组件 是为一个组件提供的包装器,来让被包装的组件可以进行懒加载。这通常用作减少构建后的 .js 文件大小的一种方式,通过将它们拆分为较小的块来按需加载。

  2. 如何使用 采用 defineAsyncComponent 配合构建工具实现一步组件加载

    import { defineAsyncComponent } from 'vue'

    const AsyncComp = defineAsyncComponent(() =>
    import('./components/MyComponent.vue')
    )

    此外 defineAsyncComponent 还支持加载中加载是被等控制

       const AsyncComp = defineAsyncComponent({

    // 加载函数
    loader: () => import('./Foo.vue'),

    // 加载异步组件时使用的组件
    loadingComponent: LoadingComponent,
    // 展示加载组件前的延迟时间,默认为 200ms
    delay: 200,

    // 加载失败后展示的组件
    errorComponent: ErrorComponent,
    // 如果提供了一个 timeout 时间限制,并超时了
    // 也会显示这里配置的报错组件,默认值是:Infinity
    timeout: 3000
    })
  3. 异步组件的实现原理 采用 import() 动态导入组件,返回一个 Promise 对象,Promise 对象解析后返回组件对象。Vue 3.x 中 defineAsyncComponent 会在组件被加载时自动调用 import() 函数来加载组件。

示例说明

<script setup>
import { ref, defineAsyncComponent } from 'vue'

// 定义一个状态变量
const msg = ref('Hello World!')

// 异步组件加载成功的情况
const AasyncComp = defineAsyncComponent({
   loader: () => import('./AsyncComp.vue'), // 正确路径
   loadingComponent: {
      template: '<div>Loading...</div>',
   },
   errorComponent: {
      template: '<div>Failed to load component.</div>',
   },
   delay: 200, // 延迟显示加载组件
   timeout: 3000, // 超时时间
   onError(error, retry, fail, attempts) {
      console.error('Error loading component:', error)
      if (attempts <= 3) {
         retry() // 重试加载
      } else {
         fail() // 失败
      }
   },
})

// 异步组件加载失败的情况
const BrokenAsyncComp = defineAsyncComponent({
   loader: () => import('./NonExistentComp.vue'), // 错误路径
   loadingComponent: {
      template: '<div>Loading...</div>',
   },
   errorComponent: {
      template: '<div>Failed to load component.</div>',
   },
   delay: 200,
   timeout: 3000,
   onError(error, retry, fail, attempts) {
      console.error('Error loading component:', error)
      if (attempts <= 3) {
         retry()
      } else {
         fail()
      }
   },
})
</script>

<template>
   <h2>正常加载异步组件:</h2>
   <AasyncComp />
   <h2>加载失败的异步组件:</h2>
   <BrokenAsyncComp />

组件通信策略和方法有哪些?

答案
通信方式描述适用场景
props父组件通过 props 向子组件传递数据,子组件通过声明 props 接收数据。父组件向子组件传递数据,单向数据流。
$emit子组件通过 $emit 触发自定义事件,父组件监听事件并接收数据。子组件向父组件传递数据,常用于组件封装。
$attrs子组件通过 $attrs 获取父组件传递的未声明属性,Vue3 剔除了 $listners 属性子组件需要获取父组件额外绑定的信息
Provide/Inject父组件通过 provide 提供数据,后代组件通过 inject 注入数据。跨层级组件通信,常用于高阶组件或组件库。
Scoped Slots,expose子组件通过在 slot 上绑定属性,或者 expose 申明属性,父组件通过 v-slot 和直接引用子组件实例暴露的属性表格等提供自定义修改能力的场景
$refs父组件通过 ref 获取子组件实例,直接调用子组件方法或访问属性。父组件需要直接操作子组件实例时使用,注意避免直接修改子组件数据。
Event Bus使用一个空的 Vue 实例作为事件总线,通过 $emit$on 实现任意组件之间的通信。非父子关系组件之间的通信,适用于简单的全局事件管理。
Vuex,Pinia使用全局状态管理函数复杂应用中需要共享状态时使用,适用于全局状态管理。

示例说明

<script setup>
import PropsEmitDemo from './PropsEmitDemo.vue'
import AttrsDemo from './AttrsDemo.vue'
import ProvideInjectDemo from './ProvideInjectDemo.vue'
import ScopedSlotDemo from './ScopedSlotDemo.vue'
import ExposeDemo from './ExposeDemo.vue'
import RefsDemo from './RefsDemo.vue'
import EventBusDemo from './EventBusDemo.vue'

import { ref } from 'vue'
const emitMsg = ref('')
function handleEmit(val) {
  emitMsg.value = val
}
</script>

<template>
  <h2>父子通信(props / emit)</h2>
  <PropsEmitDemo msg="父传子数据" @update="handleEmit" />

  <h2>父子通信($attrs)</h2>
  <AttrsDemo style="color:red" title="父组件 title" />

  <h2>跨层级通信(provide/inject)</h2>
  <ProvideInjectDemo />

  <h2>插槽通信(scoped slot)</h2>
  <ScopedSlotDemo />

  <h2>实例通信(expose/$refs)</h2>
  <ExposeDemo />
  <RefsDemo />

  <h2>全局事件通信(EventBus)</h2>
  <EventBusDemo />

Vue 有哪些内置组件?

答案

内置组件包括

组件描述
<Transition>单个元素或组件提供动画过渡效果。
<TransitionGroup>为列表中的多个元素或组件提供动画过渡效果。
<KeepAlive>缓存动态切换的组件,保留其状态。
<Teleport>将插槽内容渲染到 DOM 的其他位置。
<Suspense>管理组件树中嵌套的异步依赖,加载时显示备用内容。

内置的特殊元素包括

元素描述
<slot>定义插槽,允许父组件向子组件传递内容。
template定义一个模板块,可以在组件中使用。
component动态组件,允许在运行时切换组件。
提示

<component>、<slot>、<template> 具有类似组件的特性,也是模板语法的一部分。但它们并非真正的组件,在模板编译期间会被编译掉。因此,它们通常在模板中用小写字母书写。

示例说明

<template>
  <div>
    <h2>&lt;Transition&gt; 示例</h2>
    <button @click="show = !show">切换</button>
    <Transition name="fade">
      <p v-if="show">淡入淡出内容</p>
    </Transition>

    <h2>&lt;TransitionGroup&gt; 示例</h2>
    <button @click="addItem">添加</button>
    <button @click="removeItem">移除</button>
    <TransitionGroup name="list" tag="ul">
      <li v-for="item in items" :key="item">{{ item }}</li>
    </TransitionGroup>

    <h2>&lt;KeepAlive&gt; 示例</h2>
    <button @click="tab = 'A'">组件A</button>
    <button @click="tab = 'B'">组件B</button>
    <KeepAlive>
      <component :is="tabComponent"></component>
    </KeepAlive>

    <h2>&lt;Teleport&gt; 示例</h2>
    <Teleport to="body">
      <div class="teleport-box">这是被 Teleport 到 body 的内容</div>
    </Teleport>

    <h2>&lt;Suspense&gt; 示例</h2>
    <Suspense>
      <template #default>
        <AsyncComp />
      </template>
      <template #fallback>
        <div>加载中...</div>
      </template>
    </Suspense>

    <h2>&lt;slot&gt; 示例</h2>
    <SlotDemo>
      <template #default>插槽内容</template>
    </SlotDemo>

    <h2>&lt;template&gt; 示例</h2>
    <ul>
      <template v-for="(n) in 2">
        <li>模板循环项 {{ n }}</li>
      </template>
    </ul>

    <h2>&lt;component&gt; 示例</h2>
    <component :is="dynamicComp"></component>
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent, computed } from 'vue'
import CompA from './CompA.vue'
import CompB from './CompB.vue'
import SlotDemo from './SlotDemo.vue'

const show = ref(true)
const items = ref([1, 2, 3])
const addItem = () => items.value.push(items.value.length + 1)
const removeItem = () => items.value.pop()

const tab = ref('A')
const tabComponent = computed(() => (tab.value === 'A' ? CompA : CompB))

const AsyncComp = defineAsyncComponent(() =>
  new Promise(resolve => setTimeout(() => resolve(CompA), 1000))
)

const dynamicComp = ref('CompA')

</script>

<style scoped>
.fade-enter-active, .fade-leave-active { transition: opacity .5s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.list-enter-active, .list-leave-active { transition: all .5s; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateY(30px);}
.teleport-box { position: fixed; top: 10px; right: 10px; background: #eee; padding: 10px;}
</style>

Teleport 组件功能?

答案
  1. <teleport> 是 Vue 内置的一个特殊组件,用于将组件的模板内容渲染到指定的 DOM 节点,而不是在组件自身的位置渲染。
  2. <teleport> 允许将组件内容渲染到页面的任意位置,而不受组件层次结构的限制。这使得它非常适合用于模态框、通知、工具提示等需要脱离父组件 DOM 层次的场景。
  3. <teleport> 的核心功能包括
    1. 通过 to 属性指定目标 DOM 节点,可以是一个 CSS 选择器或一个 DOM 元素。
    2. 通过 disabled 属性可以禁用 <teleport> 的功能,使其在开发或调试时仍然在组件的原始位置渲染。
    3. Vue 3.5+ 后支持 defer 属性,延后挂载,注意延后的挂载点必须和 teleport 的挂载点一致, 类似同一个渲染周期后 mounted 后挂载
  4. 注意事项
    1. 事件冒泡<teleport> 内部的事件会按照正常的 DOM 事件冒泡机制传播到目标位置的父元素中。确保在目标位置正确处理事件。
    2. 样式隔离<teleport> 的内容可能会受到目标位置样式的影响。使用 CSS 模块化或命名空间来避免样式冲突。
    3. 响应式数据<teleport> 内部的响应式数据仍然依赖于组件的上下文,确保数据在目标位置能正确更新。
    4. 目标元素存在性:确保目标 DOM 节点在 <teleport> 渲染时已经存在,否则内容可能无法正确挂载。

示例说明

<script src="https://unpkg.com/vue@3"></script>

<div id="app">
   <h2>Teleport 属性功能演示</h2>
   <label>
      <input type="checkbox" v-model="disabled" />
      禁用 teleport(disabled)
   </label>
   <label style="margin-left:16px;">
      <input type="checkbox" v-model="defer" />
      延后挂载(defer, 仅 Vue 3.5+)
   </label>
   <div id="teleport-target" style="margin-top: 20px; border: 1px solid #ccc; padding: 10px;">
      Teleport 目标区域
   </div>
   <teleport :to="disabled ? '#app' : '#teleport-target'" :disabled="disabled" :defer="defer">
      <button @click="onButtonClick" style="margin-top: 10px;">
         Teleport 按钮(点击会冒泡到目标区域)
      </button>
   </teleport>
   <div style="margin-top: 10px;">
      <em>当前 teleport 属性:to="#teleport-target" | disabled={{ disabled }} | defer={{ defer }}</em>
   </div>
   <div style="margin-top: 10px; color: gray;">
      <p><strong>说明:</strong></p>
      <ul>
         <li><code>defer</code><code>true</code> 时,Teleport 的内容会延后挂载到目标区域,直到目标区域存在为止。</li>
         <li>这在目标区域可能动态生成的场景中非常有用。</li>
         <li>例如:如果目标区域是通过异步加载或条件渲染生成的,启用 <code>defer</code> 可以确保 Teleport 的内容不会丢失。</li>
      </ul>
   </div>
</div>

<script>
   const { createApp, ref } = Vue;

   createApp({
      setup() {
         const disabled = ref(false);
         const defer = ref(false);

         function onButtonClick() {
            alert('按钮点击事件触发(事件会冒泡到 teleport 目标区域)');
         }

         return {
            disabled,
            defer,
            onButtonClick
         };
      }
   }).mount('#app');
</script>

延伸阅读

KeepAlive 组件功能?

答案
  1. <keep-alive> 是 Vue.js 提供的一个抽象组件,用于缓存动态组件,从而避免组件的重复销毁和重建,提高性能。
  2. <keep-alive> 是实现组件复用和性能优化的关键工具,适用于需要频繁切换的动态组件场景,如标签页、路由视图等。
  3. 核心功能
    1. 组件缓存:被 <keep-alive> 包裹的组件会被缓存到内存中,而不是每次切换时销毁和重建。
    2. 激活与停用
      • 当组件被移出视图时,不会销毁,而是触发 deactivated 钩子。
      • 当组件重新进入视图时,不会重新创建,而是触发 activated 钩子。
    3. 缓存控制
      • 通过 includeexclude 属性,指定需要缓存或排除缓存的组件。
      • 通过 max 属性,限制缓存的组件数量,超出限制时会移除最久未使用的组件。
  4. 注意事项
    1. 生命周期钩子:被缓存的组件不会触发 beforeUnmountunmounted 钩子,而是触发 deactivatedactivated 钩子。
    2. 缓存清理:当缓存数量超过 max 时,最久未使用的组件会被移除。
    3. 动态切换includeexclude 支持动态更新,但需要确保组件名称与 name 属性一致。

示例说明

<script src="https://unpkg.com/vue@3"></script>

<div id="app">
   <h2>Keep-Alive 功能演示</h2>
   <ul style="display: flex; list-style: none; padding: 0; margin: 0; border-bottom: 1px solid #ccc;">
      <li 
         v-for="tabName in tabs" 
         :key="tabName" 
         @click="tab = tabName" 
         :style="{
            padding: '8px 16px',
            cursor: 'pointer',
            borderBottom: tab === tabName ? '2px solid blue' : 'none',
            fontWeight: tab === tabName ? 'bold' : 'normal'
         }"
      >
         {{ tabName === 'ComponentA' ? '组件 A' : '组件 B' }}
      </li>
   </ul>
   <keep-alive :include="['ComponentA', 'ComponentB']" :max="2">
      <component :is="tab"></component>
   </keep-alive>
</div>

<script>
   const { createApp, ref, defineComponent } = Vue;

   const ComponentA = defineComponent({
      name: 'ComponentA',
      template: `
         <div>
            <p>组件 A</p>
            <p>计数器:{{ counter }}</p>
            <button @click="increment">增加计数</button>
         </div>
      `,
      data() {
         return {
            counter: 0
         };
      },
      methods: {
         increment() {
            this.counter++;
         }
      },
      mounted() {
         console.log('ComponentA mounted');
      },
      unmounted() {
         console.log('ComponentA unmounted');
      },
      activated() {
         console.log('ComponentA activated');
      },
      deactivated() {
         console.log('ComponentA deactivated');
      }
   });

   const ComponentB = defineComponent({
      name: 'ComponentB',
      template: `
         <div>
            <p>组件 B</p>
            <p>计数器:{{ counter }}</p>
            <button @click="increment">增加计数</button>
         </div>
      `,
      data() {
         return {
            counter: 0
         };
      },
      methods: {
         increment() {
            this.counter++;
         }
      },
      mounted() {
         console.log('ComponentB mounted');
      },
      unmounted() {
         console.log('ComponentB unmounted');
      },
      activated() {
         console.log('ComponentB activated');
      },
      deactivated() {
         console.log('ComponentB deactivated');
      }
   });

   createApp({
      components: { ComponentA, ComponentB },
      setup() {
         const tab = ref('ComponentA');
         const tabs = ['ComponentA', 'ComponentB'];

         return {
            tab,
            tabs
         };
      }
   }).mount('#app');
</script>

component 元素了解么,它是如何实现动态挂载的?

答案
  1. <component> 是 Vue 提供的内置元素,用于动态渲染不同的组件。通过 is 属性指定要渲染的组件,可以是组件名称的字符串或组件选项对象。它支持动态切换组件,适用于需要根据条件动态加载和渲染的场景。
  2. 采用 is 属性来指定要渲染的组件,可以是字符串(组件名称)或组件选项对象。<component> 会根据 is 的值动态加载和渲染对应的组件。
    1. is 的值是字符串时,Vue 会查找注册的组件名称,并渲染对应的组件实例。
    2. is 的值是组件选项对象时,Vue 会直接渲染该组件。
    3. <component> 还支持 v-bind 指令来动态绑定组件的属性和事件,无法使用 v-model, 需要手动绑定
  3. <component> 的核心是通过 is 属性动态解析组件实例。Vue 在内部会根据 is 的值查找对应的组件选项对象,并通过虚拟 DOM 渲染出对应的组件实例。组件切换时,Vue 会销毁旧组件实例并挂载新组件实例,确保状态隔离。

示例说明

<script src="https://unpkg.com/vue@3"></script>
<script type="text/x-template" id="ComponentA">
  <div>
     <p>组件 A</p>
     <p>计数器:{{ counter }}</p>
     <button @click="increment">增加计数</button>
  </div>
</script>
<script>
  const ComponentA = {
    template: "#ComponentA",
    data() {
      return {
        counter: 0,
      };
    },
    methods: {
      increment() {
        this.counter++;
      },
    },
  }
</script>
<script type="text/x-template" id="ComponentB">
  <div>
     <p>组件 B</p>
     <p>计数器:{{ counter }}</p>
     <button @click="increment">增加计数</button>
  </div>
</script>
<script>
  const ComponentB = {
    template: "#ComponentB",
    data() {
      return {
        counter: 0,
      };
    },
    methods: {
      increment() {
        this.counter++;
      },
    },
  }
</script>

<div id="app">
  <h2>Dynamic Component 示例</h2>
  <ul
    style="
      display: flex;
      list-style: none;
      padding: 0;
      margin: 0;
      border-bottom: 1px solid #ccc;
    "
  >
    <li
      v-for="tabName in tabs"
      :key="tabName"
      @click="tab = tabName"
      :style="{
            padding: '8px 16px',
            cursor: 'pointer',
            borderBottom: tab === tabName ? '2px solid blue' : 'none',
            fontWeight: tab === tabName ? 'bold' : 'normal'
         }"
    >
      {{ tabName === 'ComponentA' ? '组件 A' : '组件 B' }}
    </li>
  </ul>
  <component :is="tab"></component>
</div>

<script>
  const { createApp, ref } = Vue;

  createApp({
    components: { ComponentA, ComponentB },
    setup() {
      const tab = ref("ComponentA"); // 动态切换的组件名称
      const tabs = ["ComponentA", "ComponentB"]; // 可切换的组件列表

      return {
        tab,
        tabs,
      };
    },
  }).mount("#app");
</script>

讲解一下插槽的使用?

答案
  1. 插槽 是 Vue 提供的一种机制,用于在组件中定义占位符,以便父组件可以在这些占位符中插入内容。插槽允许组件的使用者在组件的特定位置插入自定义内容,从而实现更灵活的组件组合。
  2. 插槽的使用 主要分为两种类型:默认插槽具名插槽
    1. 默认插槽:在组件中定义一个 <slot> 元素,父组件可以在该位置插入内容。默认插槽可以有多个,但只有一个会被渲染。
    2. 具名插槽:在组件中定义多个 <slot name="xx"> 元素,并为每个插槽指定一个名称。父组件可以通过 v-slot:xx 指令或 #xx 符号来指定要插入的内容。
    3. 条件插槽 结合 v-if 指令通过 $slots.xx 来判断插槽是否存在来确定是否渲染
    4. 动态插槽 父组件支持 v-slot:[xx] 的模式基于变量动态渲染插槽元素
    5. 作用域插槽:子组件通过 <slot var1="data"> 绑定变量,父组件通过 <Children v-slot="{var1}"> 的方式引用组件
  3. 插槽的原理:Vue 在编译组件时,会将插槽内容解析为虚拟 DOM,并在渲染时将这些虚拟 DOM 插入到组件的指定位置。插槽的内容会被视为父组件的作用域,因此可以访问父组件的数据和方法。

示例说明

<script src="https://unpkg.com/vue@3"></script>
<div id="app">
  <h2>插槽演示</h2>

  <slot-demo>
    <!-- 默认插槽内容 -->
    <template #default>
      <p>这是默认插槽的内容。</p>
    </template>

    <!-- 具名插槽 -->
    <template #header>
      <h3>具名插槽:header</h3>
    </template>

    <!-- 条件插槽 -->
    <template #conditional>
      <p v-if="showConditional">这是条件插槽的内容。</p>
    </template>

    <!-- 动态插槽 -->
    <template v-slot:[dynamicName]>
      <p>这是动态插槽 [{{ dynamicName }}] 的内容。</p>
    </template>

    <!-- 作用域插槽 -->
    <template #scoped="{ message }">
      <p>作用域插槽传入的 message:{{ message }}</p>
    </template>
  </slot-demo>

  <button @click="toggle">切换条件插槽显示</button>
  <button @click="toggleName">切换动态插槽名称</button>
</div>

<script>
  const { createApp, ref, defineComponent, h } = Vue;

  const SlotDemo = defineComponent({
    template: `
        <div style="border: 1px solid #ccc; padding: 10px; margin-top: 20px;">
          <slot name="header"></slot>

          <hr />
          <slot></slot>

          <hr />
          <slot name="conditional"></slot>

          <hr />
          <slot name="dynamic1"></slot>
          <slot name="dynamic2"></slot>

          <hr />
          <slot name="scoped" :message="'来自子组件的数据'"></slot>
        </div>
      `,
  });

  createApp({
    components: { SlotDemo },
    setup() {
      const showConditional = ref(true);
      const dynamicName = ref("dynamic1");

      function toggle() {
        showConditional.value = !showConditional.value;
      }

      function toggleName() {
        dynamicName.value =
          dynamicName.value === "dynamic1" ? "dynamic2" : "dynamic1";
      }

      return {
        showConditional,
        dynamicName,
        toggle,
        toggleName,
      };
    },
  }).mount("#app");
</script>

延伸阅读

  • 插槽 官方文档讲解插槽

了解 Vue 组件的整个渲染流程么,如何处理挂载和更新的?

答案

整个关键流程

  1. 编译(compile) 输入为 .vue 的 SFC 文件,输出为组件的对象,其中 template 片段被转换为 render 函数
  2. 挂载 (mount) 首次挂载会直接基于 render 生成的 vdom ,转换为对应宿主环境对应的 dom tree
  3. 补丁 (patch) 当组件的响应式数据发生变化时,会触发 update ,此时会比较 dom tree 上挂载的旧版 vdom 节点和新的 vdom 差异,触发增量更新。

详细参考下图

延伸阅读

Vue3 是怎么实现 template 支持多个根节点的?

答案

在 Vue 组件编译时会识别出组件类型是否为 Fragment, 识别出 Fragment 节点后,就会触发正常的节点渲染流程。 核心代码逻辑包括

Vue 的 DOM diff 算法是如何实现的?

答案

diff 算法的本质是比对两个虚拟 DOM 树的差异,以最小的成本更新真实 DOM 元素,整个 diff 基于如下核心原则

  1. 尽可能实现节点的复用,会通过 key 或者 ,type 来判断节点是否可以复用
  2. 如果不存在复用可能对于 type 不一样的类型直接挂载

基于上面原则,vue 目前采用的 diff 核心步骤包括

  1. 基于更新节点上的 ._vnode 和新生成的 vnode 节点,调用 patch 方法
  2. 会根据节点类型做比对,这里重点关注节点类型一样,子元素都是 children 的情况
  3. 会基于是否存在 key 做对比,这里重点关注有 key 的情况,核心处理步骤
    1. 预处理,先快速对比完收尾 key 相同的节点
    2. 预处理完成后会找出遍历一遍新节点,和旧节点,存储 newIndexToOldIndexMap 结构用来处理后续移位的判断
  4. 会基于 newIndexToOldIndexMap 计算最长递增子序列求出 increasingNewIndexSequence
  5. 基于 increasingNewIndexSequence 和 newIndexToOldIndexMap, 倒序遍历新节点,处理移位和跟新逻辑

延伸阅读

SFC 是如何处理的

答案
  1. loader 预编译
    1. webpack 会使用 vue-loader 处理 SFC 文件。会通过追加 query 字符串的方式来处理不同的 loader
      1. template 片段会被转换为 import { render } from '/project/foo.vue?vue&type=template&id=xxxxxx' 转换为 render 函数
      2. script 会被转换为 import script from '/project/foo.vue?vue&type=script', 后续基于配置的 lang ,经过 vue-loader 处理后在转换为后续 loader 处理
      3. style 会被转换为 import '/project/foo.vue?vue&type=style&index=0&id=xxxxxx',会基于配置的 lang,经过 vue-loader 处理后在转换为后续 loader 处理
      4. 自定义片段原理同上
    2. vite 会用 @vitejs/plugin-vue
  2. 这里重点讲解 template 片段的处理,分为如下三个阶段
    1. parser 通过 @vue/compiler-dom 输入 template 片段,输出模版 AST 语法树
    2. transform 处理 AST 语法树,转换为 render 函数的 AST 语法树
    3. generate 基于 render 函数的 AST 语法树,生成 render 函数的代码

延伸阅读

你是如何编写一个组件的?

答案

开放性问题,主要考察候选人对组件的理解和设计能力。笔者的经验如下

编写组件前,形成一致的风格约束。这里包括

  1. 规则约束 通过 lint 等工具规范组件的命名、属性、事件等编写规范
  2. 设计约束 通过统一的最佳实践来约束组件的设计和实现

在完成了上述约束的基础上,编写组件的步骤包括

  1. 确定组件的类型 判断组件是否需要管理自己的状态,明确组件是无状态的全渲染组件,还是有状态的逻辑组件,尽可能选择无状态组件最少知识原则,只暴露必要的接口
  2. 定义组件的输入输出 再确定组件的类型后,重点是明确组件的输入和输出,输入包括 props、插槽、事件等,输出包括事件、插槽等,这确定了组件的交互形态,这里的核心原则是
  3. 实现组件的逻辑
    1. UI 稿实现 按照功能粒度拆分 UI, 核心原则只有一条,单一职责,每个区块负责独立的功能
    2. 逻辑实现 通过 setup 函数实现组件的逻辑,核心原则是尽量使用组合式 API 来实现组件的逻辑
    3. 样式实现 css 有多种范式,遵循一种即可
  4. 测试组件 如果时间够的话,通过单元测试和集成测试来验证组件的功能和性能,确保组件在不同场景下的表现

延伸阅读

55%