本页目录
Vue3 面试题
基础
渐进式框架
- 渐进式框架 - 官网中文
在 Vue 的语境里,“渐进式”指的就是 Progressive Framework(渐进式框架),这是 Vue 的核心设计理念。
简单说:你可以 从一个很小的场景开始用 Vue(比如只给页面里的一个小按钮加点交互),然后 逐步引入更多功能(比如组件系统、路由、状态管理、构建工具……),最终甚至可以构建一个大型单页应用。
- 视图层开始
- Vue 的核心库只关注 视图层(DOM → 响应式渲染)。
- 你可以在传统后端项目(JSP、PHP、Django)里,只在一个局部引入 Vue。
- 组件化
- 当项目变复杂,你可以开始用 .vue 单文件组件(SFC),更好地组织代码。
- 这时你可能会引入 Vite 或 webpack 来编译。
- 生态扩展
- vue-router → 路由系统
- pinia / vuex → 状态管理
- @vitejs/plugin-vue → 构建工具支持
- SSR、服务端渲染、测试工具等
Vue3 为什么要引入 Composition API ? 与 Vue2 使用的 Options Api 有什么不同?
- 组合式 API 常见问答 - 官网中文
- 组合式函数 - 官网中文
Vue3 性能提升主要是通过哪几方面体现的?
- 性能优化 - 官网中文
渲染
页面渲染流程?实例挂载的过程中发生了什么?
- createApp → 创建 app 实例。
- mount → 生成 vnode → 渲染。
- patch → 区分元素/组件。
- processComponent → mountComponent。
- createComponentInstance → 创建组件运行时对象。
- setupComponent → 初始化 props/slots/setup/data。
- finishComponentSetup → 确定渲染函数(render / template 编译)。
- setupRenderEffect → 建立渲染副作用,首次执行 render 挂载 DOM。
- 数据更新时 → 触发 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 两个钩子之间调用时间差值受什么影响?
- 模板编译时间:
- 当实例被创建时,Vue 会编译模板(或将模板转换为渲染函数),这个过程在 created 钩子之前完成。如果模板非常复杂或包含大量指令、组件,这个过程会更耗时,从而延长 created 和 mounted 之间的时间差。
- 虚拟 DOM 渲染时间:
- 在 mounted 钩子调用之前,Vue 会将虚拟 DOM 渲染为实际的 DOM 元素。渲染复杂的组件树或处理大量数据绑定会增加这段时间。
- 异步操作:
- 如果在 created 钩子中发起了异步操作(如 API 请求),这些操作本身不会直接影响 created 和 mounted 的时间差,但如果这些操作涉及数据更新,可能会间接增加挂载时间。
- 浏览器性能:
- 浏览器的性能和设备的硬件配置也会影响模板编译和 DOM 渲染的速度,从而影响这两个钩子之间的时间差。
- 其他钩子执行时间:
- 在 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?
组件间通信方式有哪些?
- 父子
- props(父 → 子)
- $emit(子 → 父)
- v-model(父 ↔ 子)
- ref & defineExpose(父 → 子)
- 祖孙
- provide & inject(祖 → 孙)
- 全局
- Pinia / Vuex
- 事件总线
- 自定义全局对象