Runtime Dom
runtime-dom 是 Vue 3 源码中的一个核心包,它提供了与浏览器 DOM 相关的运行时功能。
除此以外,还包含了reactivity响应式系统、runtime-core 运行时核心(这个包含了组件、渲染器、虚拟DOM),我们的runtime-dom包含浏览器DOM的特定实现,比如DOM操作、事件和属性,complier-dom:浏览器模板编译器。
职责
通过runtime-dom我们可以创还能针对浏览器DOM的渲染器
// runtime-dom 创建针对浏览器 DOM 的渲染器
const renderer = createRenderer({
// 平台特定的节点操作
createElement(type) {
return document.createElement(type)
},
setElementText(node, text) {
node.textContent = text
},
patchProp(el, key, prevValue, nextValue) {
// 处理属性、事件、样式等
if (key === 'class') { ... }
else if (key === 'style') { ... }
else if (isOn(key)) { ... } // 事件 @click
else { ... }
},
// ... 其他 DOM 操作
})
它还负责导出浏览器专用的API:
// 从 runtime-dom 导出的核心 API
export {
createApp, // 创建 Vue 应用
render, // 渲染函数
h, // 创建 VNode
createElementVNode,
// ... 其他
}
可以看出runtime-dom的职责就是DOM实现,是平台相关内容,就是负责补充DOM操作细节的。runtime-core的职责是浏览器的核心逻辑,是平台无关的,它负责虚拟DOM、组件系统。
// runtime-core 中的渲染器基类
function createRenderer(nodeOps) {
// 通用渲染逻辑
function render(vnode, container) {
// 调用 nodeOps 的方法操作平台特定节点
nodeOps.insert(el, container)
}
}
// runtime-dom 注入 DOM 操作实现
const renderer = createRenderer({
insert: (el, parent) => parent.appendChild(el),
remove: el => el.remove(),
// ...
})
vnode
我们熟悉的VNode实现在runtime-core中的,虚拟DOM属于平台无关的核心概念:
// packages/runtime-core/src/vnode.ts
export interface VNode<HostNode = any, HostElement = any> {
type: any
props: any
key: string | number | symbol | null
ref: any
children: any
component: ComponentInternalInstance | null
el: HostNode | null
// ... 其他属性
}
export function createVNode(type, props, children): VNode {
// 创建 VNode 的逻辑
}
正因为如此,我们完全就可以在服务端使用VNode来生成HTML字符串,或者是小程序使用。
虚拟DOM就是为了在频繁的UI更新中,用js的计算成本来换取昂贵的DOM操作成本,因为真实的DOM操作涉及到了创建元素,修改属性、插入节点、删除等操作,开销都比较大,容易出发样式的重新计算、布局,DOM树重构,而且DOM对象本身也比较重,有大量属性和方法。
通过虚拟DOM我们就可以用js对象来描述真实DOM结构,这样玩法就比较大了,我们可以进行批量更新,将多次修改合成一次DOM操作,可以做到跨平台渲染,能够制作声明式UI描述UI长相,只更新变化部分,避免全量更新。
虚拟DOM操作是逃不开合并批量更新和diff算法自动处理的。diff和列表顺序变化是一起的,如果没有key,diff算法效率就会极低,甚至产生错误更新。
我们熟悉的v-for也是列表实现的,通过列表,可以数据驱动,自动更新,让diff算法自动处理,模板能够统一和保持一致。
flowchart LR
A[数据列表<br/>Array] -->|v-for| B[遍历]
B --> C[生成 VNode 列表]
C --> D[diff 算法]
D --> E[最小化 DOM 更新]
组件工厂
VNode 和组件工厂(Component Factory)是强耦合的,我们可以根据组件工厂来生产VNode。
// 组件定义
const MyComponent = {
setup() {
return () => h('div', 'Hello')
}
}
// 创建 VNode 时,type 是组件对象
const vnode = h(MyComponent)
// vnode = {
// type: MyComponent, // ← 组件定义
// props: {},
// children: null,
// component: null // 渲染时会创建组件实例
// }
h()是vue的虚拟DOM创建方法,全称叫做createVNode,h 是它的简写。
h() 接收类型、属性、子节点三个参数,返回一个描述 UI 结构的 JavaScript 对象(VNode)。h 是 hyperscript 的缩写,意为“描述 HTML 结构的 JavaScript 脚本”,这个概念最早源于 React 的 React.createElement,Vue 沿用了这个命名
// React 写法
React.createElement('div', { className: 'container' }, 'Hello')
// Vue 写法(更简洁)
h('div', { class: 'container' }, 'Hello')
// 创建单个元素
h('div', { class: 'container' }, 'Hello Vue')
// 返回的 VNode 对象(简化)
{
type: 'div',
props: { class: 'container' },
children: 'Hello Vue',
// ... 其他内部属性
}
h()和模板是有直接关系的,如果你在template模板里写了,最后编译阶段会产生渲染函数,其中dom结构就是依靠h()表示。
<!-- 模板写法 -->
<template>
<div class="card">
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
</template>
<!-- 编译后的渲染函数(简化) -->
<script>
export default {
setup() {
return () => h('div', { class: 'card' }, [
h('h1', this.title),
h('p', this.content)
])
}
}
</script>
h()就是用于生产VNode的工具,而patch()就是用于消费VNode的工具,用于首次渲染。当有数据变化的时候,就会用h 生成新的VNode。
patch也在runtime-core中,位置在src/renderer.ts中,他就是渲染器的核心哈数,完全不依赖浏览器。对于我们之前的感知,当有数据变化的时候就会让触发器完成effect的重新执行才对,为什么会有新的vnode产生呢,似乎有点不一样。
因为数据变化让effect重新执行渲染函数的时候就会重新调用h!
flowchart TD
A["修改数据<br/>count.value++"]
B["trigger 依赖更新"]
C["effect 重新执行"]
D["执行渲染函数 render()"]
E["调用 h() 生成新 VNode"]
F["调用 patch(oldVNode, newVNode)"]
G["diff 算法 + 批量 DOM 更新"]
H["真实 DOM 更新"]
A --> B --> C --> D --> E --> F --> G --> H