⚡ 30 秒速记
- 核心判断:虚拟节点是对界面结构的描述,渲染器通过挂载与更新把声明式描述映射为宿主环境操作
- 原理主线:围绕 「key 和 ref」、「parentVNode 以及它的作用」、「contextVNode」 建立输入、状态变化与输出之间的因果关系
- 文章范围:解析了Vue有状态组件的设计原理,介绍了插槽(slots)在VNode结构中的实现方式,以及如何通过组件实例访问和管理插槽内容,帮助开发者深入理解Vue组件的底层设计与渲染机制。
- 边界与代价:
key、节点类型和稳定序列决定复用边界;虚拟 DOM 解决可预测更新,不保证永远比手写 DOM 快 - 工程落地:性能分析要看实际提交次数、节点移动和组件边界,不能只用“减少 DOM 操作”解释
有状态组件的设计核心,是让组件实例保存自身状态和渲染结果,并通过关联的 VNode 接收外部信息。 挂载时先创建实例,再注入 slots,执行 render 得到组件内部的 VNode,最后把它挂载为真实 DOM。普通插槽和作用域插槽本质上都可以看成返回 VNode 的函数,区别只是后者接收子组件传入的数据。ref、事件和父子关系依赖不同的 VNode 引用,而函数式组件没有实例,不能照搬这套实例链。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。
假设我们有如下模板:
<MyComponent>
<div></div>
</MyComponent>
由这段模板可知,我们为 MyComponent 组件提供了一个空的 div 标签作为默认插槽内容,从DOM结构上看 <MyComponent> 标签有一个 div 标签作为子节点,通常我们可以将其编译为如下 VNode:
const compVNode = {
flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
tag: MyComponent,
children: {
flags: VNodeFlags.ELEMENT,
tag: 'div'
}
}
这其实没什么问题,但是我们更倾向于新建一个 slots 属性来存储这些子节点,这在语义上更加贴切,所以我们希望将模板编译为如下 VNode: