VUE 3 · 16 个高频点
每一项改动,都是在还债
Vue3 不是推倒重来,它是在解决 Vue2 留下的具体问题:
Proxy 还的是响应式的债,编译优化还的是运行时全量 diff 的债,
Composition API 还的是按选项组织代码的债。
看懂这个对应关系,面试问到"为什么",你就有话可说了。
01 先猜再看 看到代码先自己在心里跑一遍输出,再看下面的结果。
02 解释"为什么" 能说出机制,才算真的会,光记住现象很快会忘。
03 连上 Vue2没看过
Vue2 那份 的话,先看它,这里的对照会更有意义。
01
Proxy:一次解决四个缺陷★
intercept the object, not the key
Vue2 是给每个属性 装钩子,所以"新加的属性"装不上。
Vue3 改成给整个对象 套一层 Proxy,所有操作都要穿过它,
新增、删除、索引赋值全都能拦到。
✕ Vue2 拦截不到(这四件事)
state.newKey = 1
delete state.key
list[0] = 'x'
list.length = 0
✓ Vue3 全部拦截到
set 拦截新增与索引赋值
deleteProperty 拦截删除
length 变化也走 set
不需要 $set,也不需要重写数组方法
为什么一定要用 Reflect? 三个原因,最后一个最容易被忽略:
receiver 要传下去 :Reflect.get(target, key, receiver) 里第三个参数让 getter 里的 this 指向代理对象而不是原始对象,否则原型链上的 getter 会拿到错误上下文。
返回值语义统一 :Reflect.set 返回布尔值,而 target[key] = v 永远是 undefined。代理的 set 必须返回布尔值,否则严格模式下会抛错。
失败时不静默 :操作不成功会返回 false 而不是无声失败,方便上层判断。
通过 proxy 访问 state.user
→
get 拦截 track 记录"谁用了 user"
→
通过 proxy 修改 state.user = {}
→
set 拦截 trigger 找出依赖并更新
面试这样答
Vue3 用 Proxy 代理整个对象,而不是给每个属性定义 getter/setter。因为所有读写删除操作都必须经过代理,所以新增属性、删除属性、数组索引赋值和 length 修改都能被拦截,不需要 Vue.set 和重写数组方法。配合 Reflect 是为了正确传递 receiver,让 getter/setter 里的 this 指向代理对象,同时保证返回值语义正确。代价是 Proxy 无法被 polyfill,所以 Vue3 不支持 IE。
02
惰性代理:启动速度从哪来
lazy, cached by WeakMap
Vue2 初始化时要递归遍历整个 data ,把所有属性一次性装好钩子。
Vue3 只代理第一层,访问到哪个嵌套对象才代理哪个 ,并且用 WeakMap 缓存,
不会重复创建。
这意味着什么
启动更快 :初始化成本只跟"顶层有几个 key"有关,跟对象有多深无关
内存更省 :没访问到的深层对象不会产生 proxy
注意 :用 WeakMap 而不是 Map,是为了让原对象被回收时缓存能自动释放
"惰性"不等于"不响应" :只是把代理的时机推迟到访问时。访问之后照样是完整响应式的。
WeakMap 是必须的 :如果用 Map,原对象已经没人用了,proxy 还挂在 Map 里,就内存泄漏了。
这也是 console.log(state) 看起来奇怪的原因 :打印出来是一个 Proxy,DevTools 里要点开才能看到 target。
面试这样答
Vue2 在 initData 阶段递归遍历整个 data 树,为每个属性 defineProperty,所以初始化开销和对象深度成正比,深层大对象会明显拖慢启动。Vue3 改成惰性代理:reactive 只代理最外层,在 get 拦截里发现取到的是对象才递归创建代理,并用 WeakMap 缓存,保证同一个原对象只会有一个代理实例。这样初始化的代价只跟顶层属性数量有关,深层属性等到真被访问时才付出成本。
03
ref 和 reactive 到底怎么选★
value 访问器 vs Proxy
区别来自实现方式:ref 是把值包进
{ value },用 get/set 访问器 拦截;
reactive 是直接套 Proxy 。
Proxy 只能代理对象,所以原始值必须走 ref。
模板里的差异(容易踩)
模板中 顶层 ref 会自动解包 :{{ count }} 不用写 .value
但嵌套在对象里不会:{{ obj.count }} 里如果 count 是 ref,需要 obj.count.value
在 JS 代码里 永远要写 .value ,模板的自动解包只发生在编译后的渲染上下文
ref 可以整体替换 :count.value = 100 没问题。reactive 对象不能 state = {},那样会丢掉响应性(详见下一节)。
ref 解构不丢响应性 :const { value } = count 这种写法本身没意义,但 ref 作为函数参数传递时不会断链。
返回值不同 :ref 是 RefImpl 实例,reactive 是原对象的 Proxy。
项目里的实际选择 :多数团队统一用 ref,因为可以避免"忘记能不能解构""能不能整体替换"这类心智负担;只有明确要一组关联数据时才用 reactive。
用 ref 更稳的场景
原始值:数字、字符串、布尔
需要整体替换的数据:list.value = res.data
需要传给 composable 并解构返回的
用 reactive 更顺的场景
一组相关的表单字段
不需要整体替换、只做属性级修改
对象层级的逻辑封装(避免满屏 .value)
面试这样答
ref 和 reactive 的差别来自底层实现:Proxy 只能代理对象,处理不了原始值,所以 ref 把值包成一个带 value 访问器的对象,通过 get value / set value 实现拦截;reactive 直接返回原对象的 Proxy,因此只能接收对象。使用上 ref 可以装任何类型、可以整体替换,但要写 .value,模板顶层会自动解包;reactive 访问更自然但不能整体替换,解构会丢响应性。实际项目里为了统一心智模型,多数场景用 ref 就够了。
04
解构为什么丢响应性★
get trap is the whole point
reactive 的响应性全部在 Proxy 的 get 拦截里 。
解构就是"走一次 get 把值取出来存进普通变量",之后那个变量和原对象
再无关系 。要保住关系,用 toRefs。
所以 toRefs 干的不是"深拷贝一份",而是
给你一个通往原属性的管道 。读它 = 读原对象,写它 = 写原对象,
但经过了 ref 的访问器,所以响应性还在。
另一个同样隐蔽的坑:整体替换
let state = reactive({ count: 0 });
state = reactive({ count: 1 }); ← 组件里引用的还是旧的 proxy
正确做法: Object.assign(state, newObj) 或改用 ref
△ 可交互:解构 vs toRefs
直接解构 count
0
点 +1,界面不会动
count++
toRefs 后的 count
0
点 +1,界面会跟着变
count.value++
state(原对象)
state.count = 0
重置
这就是 composable 返回对象时要用 toRefs 的原因 :调用方一解构,响应性就全断了。
storeToRefs 是 Pinia 里的同类工具,专门用来从 store 解构出响应式引用。
ref 不怕解构 :因为它本身就是对象,解构出来的还是那个 ref 对象,读取仍然走 .value。
面试这样答
reactive 的响应式依赖 Proxy 的 get 拦截来做依赖收集,解构相当于执行一次 get 把值取出来赋给普通变量,这个变量和原对象就没有任何关系了,后续修改不会触发更新。解决方式是用 toRefs 把每个属性转成 ref,ref 的 get value 会去原对象上读取、set value 会写回原对象,相当于保留了一条通路。同样的道理,直接给 reactive 变量重新赋值也会丢响应性,应该用 Object.assign 合并。
05
Composition API 到底解决了什么
by feature, not by option
它不是一个"新语法",而是把代码的组织维度 从"按选项"换成"按功能"。
同一个功能的状态、计算、监听、生命周期,终于能写在一起了。
对比 mixins 的三个改进 :名字可见(return 了什么就是什么)、来源可见(从哪个函数来的)、依赖可见(函数参数就是依赖)。
类型推导友好 :普通函数返回值能被 TS 准确推断,mixin 的 this 扩展很难推导。
不是必须用 :小页面用 Options API 反而更短。Vue3 两种写法可以混用,<script setup> 里也能用 defineOptions 配 options。
逻辑组合 :composable 之间可以互相调用嵌套,像函数一样组合,mixins 做不到这么自然。
面试这样答
Options API 是按选项组织代码的,一个功能的数据在 data、逻辑在 methods、监听在 watch、初始化在 mounted,功能复杂后这些代码被拆得支离破碎。Composition API 改成按功能组织,把同一个功能用到的 ref、computed、watch、生命周期都收敛到一个函数里,通过 return 显式暴露。它解决了 mixins 的三个问题:来源不透明、命名冲突、隐式依赖。而且普通函数更容易做类型推导,也更容易嵌套组合。
06
setup 和生命周期的变化
created 被吃掉了
setup 在 beforeCreate 之前执行,
所以它替代了 beforeCreate 和 created 。
而且 setup 里没有 this。
setup 替代 beforeCreate + created
→
onBeforeMount 原 beforeMount
→
onMounted 原 mounted
→
onBeforeUpdate 原 beforeUpdate
↓
onUpdated 原 updated
→
onBeforeUnmount 原 beforeDestroy
→
onUnmounted 原 destroyed
改名的两个 :beforeDestroy → onBeforeUnmount、destroyed → onUnmounted。语义更准确——组件是被卸载,不是被"销毁"。
<keep-alive> 的两个 :onActivated / onDeactivated,用法不变。
新增调试钩子 :onRenderTracked / onRenderTriggered,开发时能看到"这次渲染是哪个依赖触发的"。
setup 不能是 async :async 会让返回值变成 Promise,模板拿不到。要异步就用 <Suspense> 配合或者在里面发请求。
面试这样答
setup 在 beforeCreate 之前执行,所以它取代了 beforeCreate 和 created 这两个钩子,组件初始化相关的逻辑都写在 setup 里。setup 接收 props 和 context 两个参数,context 包含 attrs、slots、emit。setup 里没有 this,因为函数组件不再依赖实例上下文,需要访问实例可以用 getCurrentInstance。Vue3 还把 beforeDestroy 和 destroyed 改名为 onBeforeUnmount 和 onUnmounted,因为组件实际上是被卸载而不是销毁。
07
watch、watchEffect、computed
declare the source, or let it find it
computed 算出一个值;watch 你告诉它看什么;
watchEffect 让它自己去找——执行过程中读到了谁,就依赖谁。
watch 的优势
能拿到 oldVal
可以 immediate / deep
可以控制 flush 时机
依赖关系是显式 的,一眼看得懂
watchEffect 的优势
依赖自动收集,不用列数组
写起来短,适合"一堆依赖做一件事"
立即执行一次,天然像"初始化"
但依赖不显式,复杂逻辑里容易过度追踪
flush 的三个值 :'pre'(默认,组件更新前)、'post'(更新后,可以读新 DOM)、'sync'(同步,慎用)。
监听 reactive 对象本身会自动 deep :watch(state, cb) 隐含深度监听,开销大。尽量监听具体属性。
watchEffect 会追踪所有读到的响应式数据 :哪怕只是在里面打了个 console.log,也算依赖。
computed 不能有副作用 :不要在 computed 里发请求或改别的 ref,它只负责算。
面试这样答
computed 用于派生值,有缓存、必须同步返回;watch 用于副作用,需要显式指定监听源,能拿到新旧值,支持 immediate、deep 和 flush 配置;watchEffect 会立即执行一次,并在执行过程中自动收集所有被读到的响应式依赖,之后依赖变化就重新运行,但拿不到旧值,也可以返回 stop 手动停止。选择上,计算值用 computed,需要旧值或精确控制依赖用 watch,依赖多又懒得列举时用 watchEffect。
08
Patch Flag:把"比什么"编译进去★
runtime diff → compile-time hint
Vue2 在运行时对比两个 VNode 的全部属性,但它其实不可能知道 哪些会变。
Vue3 在编译时就知道——于是把"这个节点只有文本会变"直接写成数字标记,
运行时按标记跳过无关比较。
常见 PatchFlag 值(记几个就够)
1 TEXT —— 只有文本内容会变
8 PROPS —— 有动态属性,并给出了具体是哪些
16 FULL_PROPS —— 动态 key 的属性(比如 v-bind="obj"),要全比
64 STABLE_FRAGMENT —— 顺序稳定的 fragment
-1 HOISTED —— 静态提升,patch 时直接跳过
-2 BAIL —— 放弃优化(比如手写 render 函数),退回全量 diff
PatchFlag 是负数表示"不用比" ,正数表示"要比但只比这些"。
事件处理函数也会被缓存 :onClick 编译成缓存的引用,避免每次渲染生成新函数导致子组件更新。
静态节点字符串化 :一大段纯静态 HTML 会被编译成一个字符串,用 innerHTML 一次性设置,省去逐节点创建。
这些优化只对模板生效 :手写 render 函数或用 JSX 时拿不到,会标 BAIL 退回全量 diff。
面试这样答
Vue2 的 diff 在运行时对比 VNode 的所有属性,但它无法区分哪个属性是动态的,只能全比。Vue3 在编译模板时就能分析出每个节点的动态部分:纯静态节点直接提升到 render 函数外面只创建一次,带动态文本的标 PatchFlag 1,带动态属性的标 8 并附上具体是哪些 prop。运行时 diff 看到标记就知道只需要比较那一小块,静态部分完全跳过。本质是把运行时的计算成本转移到了编译期。
09
Block Tree:只遍历动态节点
flat list of dynamic children
光有 PatchFlag 还不够——你还是得把整棵树走一遍 才能找到这些标记。
Block Tree 解决的就是这个:每个 Block 额外维护一个
dynamicChildren 数组,直接把动态节点拍平收集起来 。
span :class="cls" 8 PROPS
dynamicChildren = [ p(msg), span(:class) ]
→ diff 只遍历这 2 个,静态的 p 和层级结构完全不碰
原因很直接:Block 的前提是"子节点的结构稳定 ",这样拍平的数组顺序才不会错乱。
v-if 会整段出现或消失,v-for 的数量会变,
结构不稳定 → 所以它们内部要开新 Block,父 Block 只把"这个 Block 本身"记进数组。
Block 用 openBlock() / createBlock() 包裹 ,编译产物里能看到。
收益随树规模放大 :一棵 1000 个节点的树,只有 10 个是动态的 → 从遍历 1000 降到遍历 10。
这是 Vue3 相比 Vue2 最大的运行时性能提升 ,比 Proxy 那部分影响大得多(Proxy 主要改善的是初始化)。
面试这样答
PatchFlag 只标记了节点哪里会变,但如果不对树做处理,diff 还是得递归遍历整棵树才能找到这些标记。Block Tree 的思路是在编译时把动态节点拍平收集到当前 Block 的 dynamicChildren 数组里,diff 时只遍历这个数组,静态结构和静态节点完全跳过,复杂度从"树的节点数"降到"动态节点数"。v-if、v-for 这类结构不稳定的地方会单独开启新的 Block,保证拍平数组的顺序可靠。
10
最长递增子序列:少挪几次 DOM★
minimize real DOM moves
列表 diff 真正的成本不是"比较",而是"移动真实 DOM 节点 "。
Vue3 先用 key 建映射,再求最长递增子序列 ——这一串节点顺序本来就是对的,
不用动;只移动剩下的那些。
① 掐头 从头找相同前缀
→
② 去尾 从尾找相同后缀
→
③④ 增删 处理长度差异
→
⑤ LIS 只移动不在递增序列里的
✓ 为什么这个思路对
LIS 里的节点相对顺序本身就正确 ,把它们当作"锚点"不动,其他节点插到它们中间即可。DOM 移动次数因此最少。
✕ Vue2 的双端比较做不到
它靠四个指针的局部匹配,遇到"中间乱序 + 跨位置移动"时可能多做几次 insertBefore。功能对,但不是最优。
时间复杂度 O(n log n) :二分查找那一步是关键;朴素的 DP 是 O(n²)。
注意 LIS 求的是"下标序列" ,不是值序列——返回的下标用来决定"哪些旧节点不用移动"。
只有中间乱序部分才走 LIS :纯头插、纯尾插在阶段①②就被处理掉了,根本不会进到这里。
没有 key 时走 unkeyed 分支 :按位置一一对应 patch,不建映射也不求 LIS。
面试这样答
Vue3 的列表 diff 分五步:先从头部同步相同前缀,再从尾部同步相同后缀,这样能 O(1) 处理掉头尾增删;然后处理长度差异带来的新增和卸载;最后只剩中间乱序部分时,用 key 建立旧节点索引映射,求新顺序在旧数组中的最长递增子序列。LIS 代表"相对顺序已经正确的那批节点",把它们固定住,只移动不在序列里的节点,就能把真实 DOM 的移动次数降到最少。整个算法是 O(n log n),其中二分查找是关键。
11
Fragment / Teleport / Suspense
three new built-ins
三个都是"Vue2 里做起来别扭、Vue3 变成内建能力"的东西:
多根节点、把内容传送到别处、异步加载的占位。
代价是:多根组件无法通过 $attrs 自动透传属性 ,
因为 Vue 不知道该把 class 加到哪个根上,会给出警告。
需要手动指定:<Header v-bind="$attrs" />。
解决的是层叠上下文 问题:弹窗如果写在深层嵌套的容器里,
父级的 overflow: hidden、transform、
z-index 都会把它裁掉或压住。Teleport 让 DOM 位置和组件层级解耦,
但事件冒泡、props、provide/inject 仍然按组件树走 。
Suspense 依赖组件的 async setup :setup 返回 Promise 时,Suspense 会等它 resolve 再渲染 default 内容。
Teleport 支持 disabled :<Teleport to="body" :disabled="true"> 可以临时取消传送。
Suspense 目前仍标记为实验性 ,API 在后续版本有调整,面试说到这点会显得你关注过细节。
面试这样答
Fragment 让组件支持多根节点,不再需要额外包一层 div,代价是多根组件无法自动透传 attrs。Teleport 把内容的 DOM 渲染到指定容器,常用于弹窗、toast,解决深层嵌套导致的 overflow 裁剪和 z-index 问题,但组件逻辑关系不变,事件冒泡和 provide/inject 仍按组件树走。Suspense 用来包裹异步依赖,配合 async setup 在数据返回前显示 fallback,避免手动维护 loading 状态。
12
v-model 和 .sync 的合并
modelValue + update:modelValue
Vue2 里一个组件只能有一个 v-model,想多个就得用
.sync。Vue3 把
.sync 删了,统一成 v-model:propName。
为什么改 value → modelValue :value 这个 prop 名太容易和原生属性冲突(比如 input 的 value),改成专用名字更清晰。
自定义修饰符 :Vue3 支持 v-model.trim 这类,子组件能拿到 modelModifiers 自己处理。
原生元素上的 v-model :Vue3 里 v-model 会自动根据元素类型选 prop 和事件(text 用 value/input,checkbox 用 checked/change)。
面试这样答
Vue2 的 v-model 在组件上默认展开成 value prop 加 input 事件,一个组件只能有一个,多值绑定要用 .sync 修饰符。Vue3 把默认 prop 改成 modelValue、事件改成 update:modelValue,同时支持带参数的 v-model:xxx,用参数区分多个绑定,所以 .sync 被移除了。这个改动也统一了语法:不管多少个双向绑定,都是 v-model 加参数的形式。
13
Tree-shaking:不用的就别打包
esm + on-demand built-ins
Vue2 是一个完整的构造函数,你只用 .data(),
但 keep-alive、transition、编译器全都在包里。
Vue3 全部模块化,没 import 的就被摇掉 。
体积大致情况(gzip 后,基线对比)
Vue 2 完整版:约 23 KB
Vue 3 运行时(不含编译器):约 10 KB 出头
具体数字随版本变化,面试说量级就够了,不要背精确值
Tree-shaking 的前提是 ESM :CommonJS 是运行时 require,静态分析不出来,摇不掉。Vue3 只发 ESM 版本。
副作用标记 :库要在 package.json 里声明 sideEffects: false,否则打包工具不敢删。
模板编译可以放到构建期 :.vue 文件在构建时编译成 render 函数,运行时就不需要 compiler 了。这也是为什么"运行时版"比"完整版"小很多。
面试这样答
Vue2 是以完整构造函数的形式提供的,不管项目用不用,Transition、KeepAlive、模板编译器都在包里。Vue3 用 ESM 重写并做了彻底模块化,所有 API 和内置组件都是独立导出的,配合 sideEffects 标记,打包工具能把没用到的部分摇掉。另外 .vue 文件在构建时就编译成 render 函数,运行时可以只引入 runtime 版本,把编译器排除掉,所以基座体积从 20 多 KB 降到 10 KB 左右。
14
shallowRef 和 markRaw:主动放弃深度响应
when deep reactivity is a cost
深度响应是有代价的。有些数据你不需要 它内部变化被追踪——
比如一个图表实例、一份只读的大列表。这时候主动降级,反而更快。
适合用的场景
第三方实例:ECharts、地图、编辑器 —— 被代理会出各种怪问题
一次性加载、之后只读的大列表
只关心整体替换、不关心内部变化的配置对象
✕ 用了会踩坑
以为它和 ref 一样用 → 改了内部数据界面不动
把 markRaw 过的对象放进 v-model
忘了 markRaw 是不可逆 的,之后再也代理不了
第三方类实例被代理会出问题 :Proxy 会拦截所有属性访问,某些库内部用 instanceof 或私有字段,被包一层之后行为就变了。
markRaw 之后再 reactive ,返回的还是原对象,不会有响应性。
大列表的常见优化 :shallowRef 存列表,整批替换时触发一次更新,而不是逐项追踪几万个属性。
面试这样答
Vue3 的响应式是可以按层降级的。shallowRef 只在 .value 被整体替换时触发更新,不追踪内部;shallowReactive 只让第一层属性响应式;markRaw 则标记一个对象永远不被代理,且不可逆。使用场景主要是两类:一是第三方类实例,比如图表、地图,被 Proxy 包裹后内部逻辑可能失效;二是性能优化,比如一份几万条只读数据用 shallowRef 存,避免为每个字段建立依赖追踪。
15
script setup 多做了什么
compile-time sugar
它是编译时的语法糖 ,不是新的运行时特性。
编译器把顶层声明直接塞进 setup() 的 return,
所以少写了样板,但运行时没有额外开销。
编译器大致做了什么
把 count、double、add 自动加入 setup 的返回值
把 defineProps / defineEmits 编译成真正的 props / emits 选项
把模板里用到的导入(如子组件)自动注册
运行时没有任何魔法,产物就是一个普通的 setup 函数
组件名怎么来 :默认取文件名。要改就用 defineOptions({ name: 'Xxx' }),或者用两个 script 块。
defineProps 不能解构 (Vue 3.5 之前):解构会丢响应性。要解构用 toRefs 或者 Vue 3.5+ 的响应式 props 解构。
顶层 await 可用 :因为编译后 setup 会带 async,配合 <Suspense> 用。
defineExpose :默认组件是"关闭"的,父组件通过 ref 拿不到内部东西,要显式暴露。
面试这样答
script setup 是编译期语法糖。编译器会把顶层声明的变量、函数、导入自动加入 setup 的返回,把 defineProps、defineEmits 这些宏编译成组件选项,把模板里用到的组件自动注册,所以模板可以直接使用不需要 return。它没有运行时开销,产物就是一个普通组件。需要注意两点:defineProps 解构会丢响应性,以及组件默认是关闭的,父组件要用 ref 访问内部方法得通过 defineExpose 显式暴露。
16
速答清单
last 30 minutes before the interview
考前扫一遍。卡住的回去看对应那节。
Q Vue3 响应式原理一句话概括?
用 Proxy 代理对象,在 get 里通过 track 收集依赖,在 set / deleteProperty 里通过 trigger 派发更新。依赖关系存在 WeakMap → Map → Set 的三层结构里:target → key → 副作用集合。
Q 为什么 Vue3 不支持 IE?
因为 Proxy 在语言层面无法 polyfill。Vue2 用的 Object.defineProperty 是可以降级的,Proxy 不行——它拦截的是语言内部操作,没有等价的替代实现。
Q 依赖收集的三层结构是什么?
WeakMap (target → depsMap)、Map (key → dep)、Set (effect 集合)。用 WeakMap 是为了 target 被回收时依赖表能自动释放,避免内存泄漏。
Q Vue3 比 Vue2 快在哪三个层面?
① 响应式:Proxy 惰性代理,初始化不再递归全量 defineProperty;② 编译:静态提升、PatchFlag、Block Tree,运行时 diff 只遍历动态节点;③ 体积:ESM + Tree-shaking,用不到的不进包。
Q ref 和 reactive 最本质的区别?
ref 用 get/set 访问器 包装一个 { value } 盒子,所以能装原始值;reactive 用 Proxy 包整个对象,所以只能装对象。响应式原理不同,导致解构行为、整体替换能力都不一样。
Q 为什么解构 reactive 会丢响应性,解构 ref 不会?
reactive 的响应性在 Proxy 的 get 拦截里,解构等于取一次值赋给普通变量,之后跟原对象无关。ref 本身就是对象,解构出来还是那个 ref,读 .value 仍然走访问器,所以链路没断。要解构 reactive 用 toRefs 。
Q Block Tree 解决的是什么问题?
PatchFlag 只标记了"哪里会变",但没解决"怎么快速找到这些节点"。Block Tree 在编译期把动态节点拍平收进 dynamicChildren ,diff 只遍历这个数组,跳过所有静态结构。v-if、v-for 会开新 Block 以保证结构稳定。
Q 最长递增子序列在 diff 里干嘛用?
用来找出"相对顺序已经正确、不需要移动"的那批节点。把它们固定住,只移动其余的,就能让真实 DOM 的 insertBefore 次数最少。时间复杂度 O(n log n),用贪心加二分实现。
Q Vue3 的生命周期改了哪些?
beforeCreate 和 created 被 setup 取代;beforeDestroy → onBeforeUnmount ,destroyed → onUnmounted ;新增 onRenderTracked / onRenderTriggered 用于调试。其余只是加了 on 前缀。
Q watch 和 watchEffect 怎么选?
需要旧值、需要精确控制依赖、需要 immediate/deep/flush 时用 watch;依赖多、懒得列举、想立即执行时用 watchEffect。watchEffect 的依赖是自动收集的,但也因此容易追踪到不需要的数据,复杂逻辑里反而要小心。
Q 为什么第三方实例要用 markRaw?
Proxy 会拦截所有属性访问,某些库内部依赖 instanceof 、私有字段或 this 的严格相等,被包一层代理后行为会变,可能报错或性能骤降。markRaw 标记后这个对象永不被代理,且不可逆。