zhangdizhangdi

Vue3 面试题

基础

渐进式框架

在 Vue 的语境里,“渐进式”指的就是 Progressive Framework(渐进式框架),这是 Vue 的核心设计理念。

简单说:你可以 从一个很小的场景开始用 Vue(比如只给页面里的一个小按钮加点交互),然后 逐步引入更多功能(比如组件系统、路由、状态管理、构建工具……),最终甚至可以构建一个大型单页应用。

  1. 视图层开始
    • Vue 的核心库只关注 视图层(DOM → 响应式渲染)。
    • 你可以在传统后端项目(JSP、PHP、Django)里,只在一个局部引入 Vue。
  2. 组件化
    • 当项目变复杂,你可以开始用 .vue 单文件组件(SFC),更好地组织代码。
    • 这时你可能会引入 Vite 或 webpack 来编译。
  3. 生态扩展
    • vue-router → 路由系统
    • pinia / vuex → 状态管理
    • @vitejs/plugin-vue → 构建工具支持
    • SSR、服务端渲染、测试工具等

Vue3 为什么要引入 Composition API ? 与 Vue2 使用的 Options Api 有什么不同?

Vue3 性能提升主要是通过哪几方面体现的?

渲染

页面渲染流程?实例挂载的过程中发生了什么?

  1. createApp → 创建 app 实例。
  2. mount → 生成 vnode → 渲染。
  3. patch → 区分元素/组件。
  4. processComponent → mountComponent。
  5. createComponentInstance → 创建组件运行时对象。
  6. setupComponent → 初始化 props/slots/setup/data。
  7. finishComponentSetup → 确定渲染函数(render / template 编译)。
  8. setupRenderEffect → 建立渲染副作用,首次执行 render 挂载 DOM。
  9. 数据更新时 → 触发 effect → diff + patch 更新 UI。
源码详情
1. 创建应用 (createApp)
ts
const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  return app
}) as CreateAppFunction<Element>
  • ensureRenderer() 返回渲染器(renderer.ts)。
  • createApp(rootComponent) → 创建一个 app 实例。
  • app.mount(‘#app’) → 挂载根组件。
2. 挂载(mount)
ts
function mount(rootComponent, container) {
  // 创建 vnode
  const vnode = createVNode(rootComponent)
  // 渲染 vnode
  render(vnode, container)
}
  • createVNode:把组件转成虚拟节点(VNode)。
  • render(vnode, container):调用 patch(null, vnode, container) → 进入渲染流程。
3. 渲染 & patch
ts
function patch(n1, n2, container, parentComponent) {
  if (typeof n2.type === 'object') {
    // 组件类型
    processComponent(n1, n2, container, parentComponent)
  } else {
    // 普通元素
    processElement(n1, n2, container, parentComponent)
  }
}
4. 处理组件(processComponent)
ts
function processComponent(n1, n2, container, parentComponent) {
  if (!n1) {
    mountComponent(n2, container, parentComponent) // 首次挂载
  } else {
    updateComponent(n1, n2)
  }
}
5. 挂载组件(mountComponent)
ts
function mountComponent(initialVNode, container, parentComponent) {
  // 1. 创建组件实例
  const instance = createComponentInstance(initialVNode, parentComponent)

  // 2. 初始化组件 (setup props, slots, data, setup...)
  setupComponent(instance)

  // 3. 建立副作用渲染函数 (effect)
  setupRenderEffect(instance, initialVNode, container)
}
6. 创建组件实例(createComponentInstance)
ts
function createComponentInstance(vnode, parent) {
  const instance: ComponentInternalInstance = {
    vnode,
    type: vnode.type,
    parent,
    data: EMPTY_OBJ,
    props: EMPTY_OBJ,
    setupState: EMPTY_OBJ,
    ctx: {},
    isMounted: false,
    render: null,
  }
  return instance
}
7. 初始化组件(setupComponent)
ts
function setupComponent(instance) {
  const { props, children } = instance.vnode

  // 1. 初始化 props & slots
  initProps(instance, props)
  initSlots(instance, children)

  // 2. 处理 setup() 或 options API
  setupStatefulComponent(instance)
}
8. 执行 setup/data (setupStatefulComponent)
ts
function setupStatefulComponent(instance) {
  const Component = instance.type

  // 创建 proxy,用于 this 访问
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)

  const { setup } = Component
  if (setup) {
    // 1. 如果有 setup 函数
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit,
    })

    handleSetupResult(instance, setupResult)
  } else {
    // 2. 否则走 options API (data, methods, computed)
    finishComponentSetup(instance)
  }
}
9. 处理 setup 返回值 (handleSetupResult)
ts
function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    instance.render = setupResult // setup 返回渲染函数
  } else if (isObject(setupResult)) {
    instance.setupState = proxyRefs(setupResult) // 响应式数据
  }
  finishComponentSetup(instance)
}
10. 完成组件初始化 (finishComponentSetup)
ts
function finishComponentSetup(instance) {
  const Component = instance.type

  if (!instance.render) {
    if (compile && !Component.render) {
      // template -> render
      Component.render = compile(Component.template)
    }
    instance.render = Component.render
  }
}
11. 建立副作用渲染 (setupRenderEffect)
ts
function setupRenderEffect(instance, initialVNode, container) {
  instance.update = effect(
    function componentEffect() {
      if (!instance.isMounted) {
        // 首次挂载
        const subTree = instance.render.call(instance.proxy, instance.proxy)
        patch(null, subTree, container, instance)
        initialVNode.el = subTree.el
        instance.isMounted = true
      } else {
        // 更新
        const subTree = instance.render.call(instance.proxy, instance.proxy)
        patch(prevTree, subTree, container, instance)
      }
    },
    { scheduler: queueJob },
  )
}

主要生命周期有哪些?

Options API Composition API
beforeCreate setup
created setup
beforeMount OnBeforeMount
mounted OnMounted
beforeUpdate OnBeforeUpdate
updated OnUpdated
beforeUnmount OnBeforeUnmount
unmounted OnUnmounted
activated OnActivated
deactivated OnDeactivated
errorCaptured OnErrorCaptured

主要:

  • beforeCreate
    • 在组件实例初始化完成并且 props 被解析后立即调用。
    • 可以做:一般用于做一些全局变量挂载。
  • created
    • 当这个钩子被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。然而,此时挂载阶段还未开始,因此 $el 属性仍不可用。
    • 可以做:常用来做数据初始化、请求接口。
  • setup
    • 比 created 更早,是 Composition API 的入口,推荐用它替代 created 来做初始化逻辑。
    • setup 执行时,this 还不可用。
    • 可以返回 对象(响应式数据) 或 函数(渲染函数)。
  • beforeMount / onBeforeMount
    • 当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
    • render 生成 VNode 但还没 patch 到 DOM。
    • 可以在这里访问响应式数据,但不能操作 DOM,因为还没生成真实节点。
  • mounted / onMounted
    • patch 把真实 DOM 挂载到页面 之后。
    • 可以做:
      • 操作 DOM(比如获取节点尺寸)。
      • 触发需要依赖真实渲染的逻辑(比如动画、第三方库初始化)。
ts
export function applyOptions(instance: ComponentInternalInstance) {
  const { beforeCreate, created } = instance.type as ComponentOptions

  if (beforeCreate) {
    callHook(beforeCreate, instance, LifecycleHooks.BEFORE_CREATE)
  }

  // 初始化 data/props/computed/methods ...

  if (created) {
    callHook(created, instance, LifecycleHooks.CREATED)
  }
}

function setupRenderEffect(instance, initialVNode, container) {
  instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
      // beforeMount
      if (bm) {
        invokeArrayFns(bm) // 执行 onBeforeMount / beforeMount
      }

      // 渲染
      const subTree = instance.render.call(instance.proxy, instance.proxy)
      patch(null, subTree, container, instance)

      initialVNode.el = subTree.el

      // mounted
      if (m) {
        queuePostRenderEffect(m, parentSuspense)
      }

      instance.isMounted = true
    } else {
      // 更新时调用 beforeUpdate / updated ...
    }
  })
}

调用点:

  • setup → 在 setupStatefulComponent() 中调用(component.ts)。
  • beforeCreate / created → 在 applyOptions() 中调用(componentOptions.ts)。
  • onBeforeMount / beforeMount → 在 setupRenderEffect() 首次渲染之前 调用(renderer.ts)。
  • onMounted / mounted → 在 setupRenderEffect() 首次渲染之后,用 queuePostRenderEffect 延迟执行。

created 和 mounted 两个钩子之间调用时间差值受什么影响?

  1. 模板编译时间:
    • 当实例被创建时,Vue 会编译模板(或将模板转换为渲染函数),这个过程在 created 钩子之前完成。如果模板非常复杂或包含大量指令、组件,这个过程会更耗时,从而延长 created 和 mounted 之间的时间差。
  2. 虚拟 DOM 渲染时间:
    • 在 mounted 钩子调用之前,Vue 会将虚拟 DOM 渲染为实际的 DOM 元素。渲染复杂的组件树或处理大量数据绑定会增加这段时间。
  3. 异步操作:
    • 如果在 created 钩子中发起了异步操作(如 API 请求),这些操作本身不会直接影响 created 和 mounted 的时间差,但如果这些操作涉及数据更新,可能会间接增加挂载时间。
  4. 浏览器性能:
    • 浏览器的性能和设备的硬件配置也会影响模板编译和 DOM 渲染的速度,从而影响这两个钩子之间的时间差。
  5. 其他钩子执行时间:
    • 在 beforeCreate、created、beforeMount 等钩子中执行的代码也会影响到 mounted 钩子的触发时间。如果这些钩子中有大量计算或耗时操作,也会增加时间差。
源码
ts
function mountComponent(initialVNode, container, parentComponent) {
  // 1. 创建组件实例
  const instance = createComponentInstance(initialVNode, parentComponent)

  // 2. 初始化组件 (setup props, slots, data, setup...)
  setupComponent(instance)

  // 3. 建立副作用渲染函数 (effect)
  setupRenderEffect(instance, initialVNode, container)
}

function setupComponent(instance) {
  const { props, children } = instance.vnode

  // 1. 初始化 props & slots
  initProps(instance, props)
  initSlots(instance, children)

  // 2. 处理 setup() 或 options API
  setupStatefulComponent(instance)
}

function setupStatefulComponent(instance) {
  const Component = instance.type

  // 创建 proxy,用于 this 访问
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)

  const { setup } = Component
  if (setup) {
    // 1. 如果有 setup 函数
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [
        __DEV__ ? shallowReadonly(instance.props) : instance.props,
        setupContext,
      ],
    )
    handleSetupResult(instance, setupResult)
  } else {
    // 2. 否则走 options API (data, methods, computed)
    finishComponentSetup(instance)
  }
}

function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    instance.render = setupResult // setup 返回渲染函数
  } else if (isObject(setupResult)) {
    instance.setupState = proxyRefs(setupResult) // 响应式数据
  }
  finishComponentSetup(instance)
}

function finishComponentSetup(instance,isSSR) {
  const Component = instance.type

  if (!instance.render) {
    if (!isSSR && compile && !Component.render) {
      const template = ...

      if (template) {
        const finalCompilerOptions=...
        // 如果没有 render,且有 template,会用 compiler-dom 编译 template → render 函数。
        Component.render = compile(template, finalCompilerOptions)
      }
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction
  }
}

function setupRenderEffect(instance, initialVNode, container) {

}

为什么data是个函数并且返回一个对象呢?

  • 对象写法:所有组件实例共享同一个对象 → 会数据污染。
  • 函数返回对象:每次创建组件实例时调用,返回一个全新的对象 → 数据独立。
根组件
  • 可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
ts
// 1. mountComponent
// 2. setupComponent
// 3. setupStatefulComponent
// 4. handleSetupResult / finishComponentSetup
// 5. applyOptions

export function applyOptions(instance: ComponentInternalInstance): void {
  const options = resolveMergedOptions(instance)
  const publicThis = instance.proxy! as any

  const { data: dataOptions } = options // [!code highlight]

  if (dataOptions) {
    const data = dataOptions.call(publicThis, publicThis) // [!code highlight]
    if (!isObject(data)) {
      __DEV__ && warn(`data() should return an object.`)
    } else {
      instance.data = reactive(data) // 转成响应式对象  // [!code highlight]
    }
  }
}

父子组件生命周期

1. 挂载过程

父先创建,才能有子;子创建完成,父才完整。

  • Options 顺序:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
  • Composition 顺序:父 setup -> 父 onBeforeMount -> 子 setup -> 子 onBeforeMount -> 子 onMounted -> 父 onMounted
2. 更新过程
2.1 子组件更新

子组件更新 影响 父组件的情况。
顺序:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

子组件更新 不影响 父组件的情况。
顺序:子 beforeUpdate -> 子 updated

2.2 父组件更新

父组件更新 影响 子组件的情况。
顺序:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

父组件更新 不影响 子组件的情况。
顺序:父 beforeUpdate -> 父 updated

📌 无论更新是从谁引起的,最终都按“父 before → 子 before → 子 updated → 父 updated” 的顺序。
Vue 的更新是按组件树结构排序执行的,不是按“哪个响应式数据先变”来决定顺序。
所以即使子先改了父的数据,更新队列里父的 job 也会排在子前面执行。

源码角度
  • 子里 emit 改父数据,会触发 trigger → effect.run() → queueJob(parent.update)
  • parent.update 是父组件的 ReactiveEffect,加入全局更新 queue
  • queue 最终通过 flushJobs 执行时:
    • 按组件树的顺序排序(sort(queue, (a,b)=>a.id-b.id),父先于子)
    • 所以父 update 会先跑 → beforeUpdate
    • 然后在父的 render 里更新子 → 子 beforeUpdate、updated
    • 父的 updated 在最后的 postFlush 队列执行 → 子 updated 先,父 updated 后
3. 销毁过程

父 beforeUnmount -> 子 beforeUnmount -> 子 unmounted -> 父 unmounted

说说vue中的diff算法

Vue中key的作用是什么?为什么需要绑定key?为什么不建议使用index作为key?

组件间通信方式有哪些?

  1. 父子
    • props(父 → 子)
    • $emit(子 → 父)
    • v-model(父 ↔ 子)
    • ref & defineExpose(父 → 子)
  2. 祖孙
    • provide & inject(祖 → 孙)
  3. 全局
    • Pinia / Vuex
    • 事件总线
    • 自定义全局对象

$nextTick 有什么作用?

什么是虚拟DOM?VNode 有哪些属性?

h 函数有什么用?

响应式

说说 vue3 中的响应式设计原理

在哪些情况下会进行依赖收集?依赖收集的机制是什么?

当Vue中的数据对象某属性值改变时,视图的更新是即时的还是异步的?请解释其更新机制。

Vue对象或数据的属性变化监听是如何实现的?请描述其工作原理。

ref 和 reactive 有什么区别?及其使用场景?

watch与watchEffect 有什么区别,分别在什么场景下使用?

computed怎么实现的缓存?

computed 计算值为什么还可以依赖另外一个 computed 计算值?

computed 和 watch 区别

Proxy 和 Object.defineProperty 的区别是啥?Vue中使用Object.defineProperty进行数据劫持有哪些潜在缺点或限制?Vue3为什么选择使用Proxy?

ref、toRef 和 toRefs 有什么区别?

vue3 的响应式库是独立出来的,如果单独使用是什么样的效果?

在Vue中,为什么推荐使用ref而非直接操作DOM?

怎么理解 Vue3 提供的 markRaw ?

编译

模板是如何编译的?

怎么把template模版编译成render函数的?

template语法与JSX语法有何不同?它们各自的优势在哪里?

其它

scoped

说说 CSS scoped 的原理

如何打破 scope 对样式隔离的限制?

双向绑定 v-model

双向绑定和单向数据流原则是否冲突?

Vue3 中 v-model 的改进是什么?如何用 defineModel 简化代码?

keep-alive

说说你对slot的理解?slot使用场景有哪些?

异步组件

vue 是如何识别和解析指令的?

vue3中怎么设置全局变量?

怎么在 Vue 中定义全局方法?

自定义指令是什么?有哪些应用场景?

Vue常用的修饰符有哪些?分别有什么应用场景?

在 v-for 时给每项元素绑定事件需要用事件代理吗,为什么?

vue组件里写的原生addEventListeners监听事件,要手动去销毁吗?为什么?

状态管理

Vuex

Pinia

刷新浏览器后,Vuex的数据是否存在?如何解决?

Vuex状态管理与使用全局对象进行状态管理有什么本质区别和优势?

项目

Vue 项目中,你做过哪些性能优化?

如果使用Vue3.0实现一个 Modal,你会怎么进行设计?

你是怎么处理vue项目中的错误的?

大型项目中,Vue项目怎么划分结构和划分组件比较合理呢?