Vue 运行时包之间的依赖关系

2025-10-21 15:04:39

官方描述: 👀

但平时使用 Vue 时,我们一般不会直接调用 nodeOps.insertpatchProp,而是这样渲染一个 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 包里拿到的 renderhcreateApp,本质上是这些运行时能力组合后的结果。

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 渲染到指定容器里。

浏览器环境下,大致流程是:

  1. 根据 vnode.type 创建真实元素
  2. 根据 vnode.props 设置属性
  3. 根据 vnode.children 设置文本或继续挂载子节点
  4. 把真实元素插入到 container

对应到前面传入的 renderOptions,就是:

const el = createElement(vnode.type)

if (vnode.children) {
  setElementText(el, vnode.children)
}

insert(el, container)

这里的 createElementsetElementTextinsert 都来自 runtime-dom

但调用它们的流程,是 runtime-core 控制的。

这样拆分的原因

如果把所有逻辑都写在 runtime-dom 里,也能完成浏览器渲染, 但这样核心渲染流程就会和 DOM 强绑定,后续想复用到其他平台会很困难。

拆开之后对应的职责就清楚了:

  • runtime-core:处理 vnode、组件、patch、diff 等通用逻辑
  • runtime-dom:提供浏览器平台的节点和属性操作
  • vue:对外导出浏览器环境下常用的运行时 API

下一步

第一步要处理的,就是 vnode 如何被挂载成真实节点。