Vue 运行时包之间的依赖关系
官方描述: 👀
但平时使用 Vue 时,我们一般不会直接调用 nodeOps.insert 或 patchProp,而是这样渲染一个 vnode:
const vnode = h('div', 'hello world')
render(vnode, app)
从官方描述中能看到从 runtime-core 中引入了 createRenderer
import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({
patchProp,
insert,
remove,
createElement
// ...
})
// `render` 是底层 API
// `createApp` 返回一个应用实例
export { createApp, render }
// 重新导出 Vue 的核心 API
export * from '@vue/runtime-core'
包之间的依赖关系
Vue 运行时里几个包的大致关系:
/**
* reactivity 提供响应式能力
*
* runtime-core 依赖 reactivity
* runtime-core 提供 vnode、组件、通用渲染流程
*
* runtime-dom 依赖 runtime-core
* runtime-dom 提供浏览器 DOM 平台的渲染能力
*
* vue 包最终导出 runtime-dom
*/
也就是,真正的渲染逻辑在 runtime-core。
浏览器相关的 DOM 操作在 runtime-dom。
从 vue 包里拿到的 render、h、createApp,本质上是这些运行时能力组合后的结果。
runtime-dom 做了什么
runtime-dom 的核心工作,是从 runtime-core 里引入 createRenderer,然后把 DOM 平台的配置传进去。
简化后大概是这样:
import { createRenderer } from '@vue/runtime-core'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
const { render, createApp } = createRenderer({
patchProp,
...nodeOps
})
export { createApp, render }
export * from '@vue/runtime-core'
nodeOps 负责节点操作,patchProp 负责属性更新。
它们合在一起,就是浏览器平台的 renderOptions:
const renderOptions = {
patchProp,
insert,
createElement,
remove,
setElementText,
createText,
setText,
parentNode,
nextSibling,
querySelector
}
runtime-dom 不负责实现完整的 diff 流程,它只提供平台能力。
真正拿着这些能力去渲染 vnode 的,是 runtime-core 里的 createRenderer。
createRenderer
createRenderer 会接收平台配置,并返回对应平台可以使用的渲染 API。
先写一个最小结构:
// packages/runtime-core/src/renderer.ts
export function createRenderer(renderOptions) {
const render = (vnode, container) => {
// 后面会在这里处理挂载和更新
}
return {
render,
createApp
}
}
现在先不急着实现 render 内部逻辑,只要先明确一件事:
createRenderer 不关心自己运行在哪个平台。
它只通过 renderOptions 调用平台能力。比如需要创建元素时,不直接写 document.createElement,而是调用传进来的
createElement:
export function createRenderer(options) {
const {
createElement,
insert,
patchProp,
setElementText
} = options
const render = (vnode, container) => {
// 使用 createElement 创建节点
// 使用 patchProp 更新属性
// 使用 insert 插入容器
}
return {
render,
createApp
}
}
这样 runtime-core 就保持了平台无关。
浏览器平台传 DOM 操作进来,它就渲染 DOM。
如果别的平台传入自己的节点操作,它也可以复用同一套核心渲染流程。
render 和 vnode
再回到一开始的代码:
const vnode = h('div', 'hello world')
render(vnode, app)
h 创建出来的是 vnode。
vnode 不属于 runtime-dom,而是属于 runtime-core 的通用数据结构。
可以先把它理解成这样:
const vnode = {
type: 'div',
props: null,
children: 'hello world'
}
render 做的事情,就是把这个 vnode 渲染到指定容器里。
浏览器环境下,大致流程是:
- 根据
vnode.type创建真实元素 - 根据
vnode.props设置属性 - 根据
vnode.children设置文本或继续挂载子节点 - 把真实元素插入到
container
对应到前面传入的 renderOptions,就是:
const el = createElement(vnode.type)
if (vnode.children) {
setElementText(el, vnode.children)
}
insert(el, container)
这里的 createElement、setElementText、insert 都来自 runtime-dom。
但调用它们的流程,是 runtime-core 控制的。
这样拆分的原因
如果把所有逻辑都写在 runtime-dom 里,也能完成浏览器渲染, 但这样核心渲染流程就会和 DOM 强绑定,后续想复用到其他平台会很困难。
拆开之后对应的职责就清楚了:
runtime-core:处理 vnode、组件、patch、diff 等通用逻辑runtime-dom:提供浏览器平台的节点和属性操作vue:对外导出浏览器环境下常用的运行时 API
下一步
第一步要处理的,就是 vnode 如何被挂载成真实节点。