第 18 章

跨平台渲染器:自定义渲染器的设计哲学与实现

第18章:跨平台渲染器——自定义渲染器的设计哲学与实现

@vue/runtime-core 这个包里没有一行代码调用 document.createElement——它甚至连 document 这个全局变量都不引用。整个组件系统、响应式绑定、生命周期、指令处理,都在完全不知道"DOM 是什么"的情况下运行。

本章核心问题:Vue 是如何把"渲染"这件事彻底抽象出来,让同一套组件逻辑能驱动 DOM、Canvas、小程序、服务端 HTML 字符串输出等完全不同的目标平台?

读完本章你将理解

Level 1 · 你需要知道的(1-3年经验)

渲染器的分层架构

Vue 3 把渲染相关的代码分成了三层:

层次结构:

┌─────────────────────────────────────────────────────────────┐
│  @vue/runtime-dom   (DOM 平台层)                           │
│  ├── patchDOMProp / patchAttr / patchClass / patchStyle     │
│  ├── patchEvent(addEventListener / removeEventListener)    │
│  └── 实现 RendererOptions,调用 document.*                  │
│                                                             │
│  调用 createRenderer(domOptions)                             │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  @vue/runtime-core  (平台无关的核心层)                      │
│  ├── 组件挂载/更新/卸载逻辑                                  │
│  ├── diff 算法                                              │
│  ├── 指令处理                                               │
│  ├── 生命周期调用                                            │
│  └── 通过 RendererOptions 接口调用平台方法                   │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  @vue/reactivity    (响应式系统,完全独立)                   │
│  └── ref / reactive / computed / watch                      │
└─────────────────────────────────────────────────────────────┘

@vue/runtime-dom 是 Vue 浏览器端的入口。如果你要做 Canvas 渲染,你不用 @vue/runtime-dom,而是直接用 @vue/runtime-core 提供的 createRenderer,传入你自己的 Canvas 操作方法。

RendererOptions 接口:渲染器的"驱动合同"

// packages/runtime-core/src/renderer.ts
export interface RendererOptions<HostNode = RendererNode, HostElement = RendererElement> {
  // 核心 DOM 操作
  createElement(type: string, isSVG?: boolean, isCustomizedBuiltIn?: string): HostElement
  createText(text: string): HostNode
  createComment(text: string): HostNode
  setText(node: HostNode, text: string): void
  setElementText(el: HostElement, text: string): void

  // DOM 树操作
  insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
  remove(el: HostNode): void
  parentNode(node: HostNode): HostElement | null
  nextSibling(node: HostNode): HostNode | null

  // 属性/事件处理
  patchProp(
    el: HostElement,
    key: string,
    prevValue: any,
    nextValue: any,
    isSVG?: boolean,
    prevChildren?: VNode[],
    parentComponent?: ComponentInternalInstance | null,
    unmountChildren?: UnmountChildrenFn
  ): void

  // 可选:用于 SSR 的特殊操作
  querySelector?: (selector: string) => HostElement | null
  setScopeId?(el: HostElement, id: string): void
  cloneNode?(node: HostNode): HostNode
  insertStaticContent?(
    content: string,
    parent: HostElement,
    anchor: HostNode | null,
    isSVG: boolean
  ): [HostNode, HostNode]
}

只要你实现了这 10-12 个方法,@vue/runtime-core 的全部功能(组件、Suspense、KeepAlive、指令……)都可以在你的平台上运行。

createRenderer:渲染器工厂

import { createRenderer } from '@vue/runtime-core'

// 创建一个"什么都不做"的测试渲染器
const { render, createApp } = createRenderer({
  createElement: (type) => ({ type, children: [], props: {} }),
  createText: (text) => ({ type: '#text', text }),
  createComment: (text) => ({ type: '#comment', text }),
  setText: (node, text) => { node.text = text },
  setElementText: (el, text) => { el.text = text },
  insert: (el, parent, anchor) => {
    const index = anchor ? parent.children.indexOf(anchor) : parent.children.length
    parent.children.splice(index, 0, el)
  },
  remove: (el) => {
    const parent = el.parentNode
    if (parent) {
      const index = parent.children.indexOf(el)
      parent.children.splice(index, 1)
    }
  },
  patchProp: (el, key, prevValue, nextValue) => {
    el.props[key] = nextValue
  },
  parentNode: (node) => node.parentNode || null,
  nextSibling: (node) => {
    const parent = node.parentNode
    if (!parent) return null
    const index = parent.children.indexOf(node)
    return parent.children[index + 1] || null
  },
})

@vue/server-renderer 的使用

// 服务端(Node.js)
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'

const app = createSSRApp({
  data() { return { msg: 'Hello SSR' } },
  template: '<div>{{ msg }}</div>'
})

// renderToString 不操作 DOM,而是产生 HTML 字符串
const html = await renderToString(app)
console.log(html)  // '<div>Hello SSR</div>'

SSR 渲染器的核心差异:它不需要实现 insert/remove/parentNode 等 DOM 操作,因为它只是拼接字符串。


Level 2 · 它是怎么运行的(3-5年经验)

createRenderer 的内部结构

// packages/runtime-core/src/renderer.ts(简化)
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

function baseCreateRenderer(options, createHydrationFns?) {
  // 把 RendererOptions 解构,后续所有操作都通过这些局部变量调用
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId,
    insertStaticContent: hostInsertStaticContent,
  } = options

  // 所有 patch / mount / unmount 函数都在这个闭包里定义
  // 它们通过上面的 host* 变量调用平台方法
  const patch = (n1, n2, container, anchor, ...) => {
    // ... 使用 hostCreateElement 等,不直接用 document
  }

  const mountElement = (vnode, container, anchor, ...) => {
    const el = (vnode.el = hostCreateElement(
      vnode.type,
      isSVG,
      vnode.props && vnode.props.is
    ))
    // ...
    hostInsert(el, container, anchor)
  }

  // 最终返回 render 函数和 createApp 工厂
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate),
  }
}

关键洞察:所有的渲染逻辑都在 baseCreateRenderer 这个函数的闭包内。hostCreateElement 等变量通过闭包被所有内部函数共享。换一套 options,就换了一套"底层驱动",上层逻辑完全不变。

DOM 渲染器的 patchProp 实现

DOM 平台的 patchProp 是最复杂的单个实现,它需要处理多种不同类型的属性:

// packages/runtime-dom/src/patchProp.ts(简化)
export const patchProp: DOMRendererOptions['patchProp'] = (
  el, key, prevValue, nextValue, isSVG, ...
) => {
  if (key === 'class') {
    patchClass(el, nextValue, isSVG)
  } else if (key === 'style') {
    patchStyle(el, prevValue, nextValue)
  } else if (isOn(key)) {
    // 以 on 开头的属性(onClickt → click 事件)
    if (!isModelListener(key)) {
      patchEvent(el, key, prevValue, nextValue, ...)
    }
  } else if (
    key[0] === '.'
      ? ((key = key.slice(1)), true)
      : !isSVG && nativeOnlyTags.has(el.tagName) && domAttrConfig[key]
        ? (el[key] = nextValue) && false
        : false
  ) {
    // DOM property(.value, .checked 等)
    patchDOMProp(el, key, nextValue, ...)
  } else {
    // HTML attribute(aria-*, data-*, 自定义属性)
    patchAttr(el, key, nextValue, isSVG)
  }
}

实战:构建一个最小化 Canvas 渲染器

// 节点类型定义
class CanvasNode {
  constructor(type, props = {}) {
    this.type = type          // 'rect' | 'circle' | 'text' | '#text' | '#fragment'
    this.props = props
    this.children = []
    this.parent = null
    this._x = 0              // 计算后的绝对位置
    this._y = 0
  }
}

// Canvas 渲染器选项
import { createRenderer } from '@vue/runtime-core'

const { createApp } = createRenderer({
  // 创建节点
  createElement(type) {
    return new CanvasNode(type)
  },
  createText(text) {
    return new CanvasNode('#text', { text })
  },
  createComment(text) {
    return new CanvasNode('#comment', { text })
  },

  // 文本操作
  setText(node, text) {
    node.props.text = text
    scheduleRedraw()
  },
  setElementText(el, text) {
    el.props.text = text
    scheduleRedraw()
  },

  // 树操作
  insert(el, parent, anchor) {
    el.parent = parent
    if (anchor) {
      const index = parent.children.indexOf(anchor)
      parent.children.splice(index, 0, el)
    } else {
      parent.children.push(el)
    }
    scheduleRedraw()
  },
  remove(el) {
    if (el.parent) {
      const index = el.parent.children.indexOf(el)
      el.parent.children.splice(index, 1)
      el.parent = null
    }
    scheduleRedraw()
  },
  parentNode(node) {
    return node.parent
  },
  nextSibling(node) {
    if (!node.parent) return null
    const index = node.parent.children.indexOf(node)
    return node.parent.children[index + 1] || null
  },

  // 属性更新
  patchProp(el, key, prevValue, nextValue) {
    el.props[key] = nextValue
    scheduleRedraw()
  },
})

// 实际 Canvas 渲染函数(将节点树转换为 Canvas 绘制调用)
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
let redrawScheduled = false

function scheduleRedraw() {
  if (!redrawScheduled) {
    redrawScheduled = true
    requestAnimationFrame(redraw)
  }
}

function redraw() {
  redrawScheduled = false
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  renderNode(rootNode, 0, 0)
}

function renderNode(node, x, y) {
  if (!node) return

  const { type, props, children } = node
  const nodeX = x + (props.x || 0)
  const nodeY = y + (props.y || 0)

  switch (type) {
    case 'rect':
      ctx.fillStyle = props.fill || '#000'
      ctx.fillRect(nodeX, nodeY, props.width || 100, props.height || 100)
      break
    case 'circle':
      ctx.fillStyle = props.fill || '#000'
      ctx.beginPath()
      ctx.arc(nodeX, nodeY, props.radius || 50, 0, Math.PI * 2)
      ctx.fill()
      break
    case 'text':
      ctx.fillStyle = props.color || '#000'
      ctx.font = props.font || '16px sans-serif'
      ctx.fillText(props.text || '', nodeX, nodeY)
      break
  }

  children.forEach(child => renderNode(child, nodeX, nodeY))
}

// 使用示例
const app = createApp({
  setup() {
    const color = ref('red')
    const x = ref(50)

    setTimeout(() => {
      color.value = 'blue'  // 响应式更新 → Canvas 重绘
      x.value = 100
    }, 1000)

    return () => h('rect', { fill: color.value, x: x.value, y: 50, width: 80, height: 80 })
  }
})

// 创建根节点
const rootNode = new CanvasNode('#fragment')
app.mount(rootNode)

渲染路径对比图

DOM 渲染器:

VNode tree
    │
    ▼ patch()
DOM 操作调用序列
├── document.createElement('div')
├── el.setAttribute('class', 'box')
├── el.addEventListener('click', handler)
└── document.body.appendChild(el)

真实 DOM 树
    │
    ▼ 浏览器渲染引擎
像素画面

────────────────────────────────────────────────────────

服务端渲染器(@vue/server-renderer):

VNode tree
    │
    ▼ renderToString()
字符串拼接序列
├── '<div'
├── ' class="box"'
├── '>'
└── '</div>'

HTML 字符串
    │
    ▼ HTTP 响应
客户端接收并水合(hydrate)

────────────────────────────────────────────────────────

Canvas 渲染器(自定义):

VNode tree
    │
    ▼ patch()(使用自定义 RendererOptions)
CanvasNode 树操作
├── new CanvasNode('rect')
├── node.props.fill = 'red'
└── parent.children.push(node)

CanvasNode 树
    │
    ▼ redraw()(requestAnimationFrame)
Canvas 2D 绘制调用
├── ctx.fillRect(...)
└── ctx.arc(...)

Canvas 像素

uni-app 和小程序端的渲染原理

uni-app(Vue 3 版本)使用自定义渲染器将 Vue 组件渲染到各平台的原生节点:

// uni-app 的渲染器选项(概念版,非实际代码)
const uniRenderer = createRenderer({
  createElement(type) {
    // 在小程序端,"创建节点"意味着创建一个描述对象
    // 最终通过 setData() 传递给小程序框架
    return { type, props: {}, children: [], nodeId: generateId() }
  },

  insert(el, parent, anchor) {
    // 修改虚拟 DOM 树
    // 然后通过 diff 计算出 setData 的最小更新
    parent.children.splice(anchor ? ... : parent.children.length, 0, el)
    batchSetData({ [el.nodeId]: el })  // 通知小程序框架
  },

  patchProp(el, key, prevValue, nextValue) {
    el.props[key] = nextValue
    // 只 setData 变化的属性,不是整棵树
    setData({ [`${el.nodeId}.${key}`]: nextValue })
  },

  // ...
})

关键原理:小程序的渲染层和逻辑层是分离的(两个独立的 JS 运行环境)。所有的节点操作都通过 setData 消息传递给渲染层。自定义渲染器把 Vue 的 VNode 操作翻译成 setData 调用。


Level 3 · 设计文档与源码(资深开发者)

@vue/server-renderer 的工作原理

服务端渲染器与客户端渲染器的根本区别:

// packages/server-renderer/src/renderToString.ts(简化)
export async function renderToString(
  input: App | VNode,
  context: SSRContext = {}
): Promise<string> {
  // 创建一个服务端应用实例(与客户端不同)
  const vnode = createVNode(input._component, input._props)
  vnode.appContext = input._context

  // 初始化 Suspense 计数
  const ctx: SSRContext = context
  const buffer: SSRBuffer = []

  await renderComponentVNode(vnode, null, ctx, buffer)

  return unrollBuffer(await Promise.all(buffer))
}

// 服务端组件渲染:直接递归生成字符串,不创建真实 DOM
async function renderComponentVNode(vnode, parentComponent, ctx, buffer) {
  const instance = createComponentInstance(vnode, parentComponent, null)
  await setupServerComponent(instance)  // 执行 setup,等待 async setup

  // 执行 render 函数,得到 subTree VNode
  const subTree = renderComponentRoot(instance)

  // 递归渲染 subTree 为字符串
  await renderVNode(subTree, instance, ctx, buffer)
}

async function renderVNode(vnode, parentComponent, ctx, buffer) {
  const { type, shapeFlag } = vnode

  if (type === Text) {
    buffer.push(escapeHtml(vnode.children))  // 文本节点:转义 + 追加
  } else if (type === Comment) {
    buffer.push(`<!--${vnode.children}-->`)
  } else if (type === Fragment) {
    await renderVNodeChildren(vnode.children, parentComponent, ctx, buffer)
  } else if (shapeFlag & ShapeFlags.ELEMENT) {
    // 原生元素:生成 HTML 标签
    await renderElementVNode(vnode, parentComponent, ctx, buffer)
  } else if (shapeFlag & ShapeFlags.COMPONENT) {
    // 组件:递归渲染
    await renderComponentVNode(vnode, parentComponent, ctx, buffer)
  }
}

async function renderElementVNode(vnode, parentComponent, ctx, buffer) {
  const { type: tag, props, children, shapeFlag } = vnode

  // 开始标签
  buffer.push(`<${tag}`)

  // 属性
  if (props) {
    for (const key in props) {
      buffer.push(` ${key}="${escapeHtmlAttr(props[key])}"`)
    }
  }

  // 自闭合标签处理
  if (isSelfClosingTag(tag)) {
    buffer.push(' />')
    return
  }

  buffer.push('>')

  // 子节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    buffer.push(escapeHtml(children))
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    await renderVNodeChildren(children, parentComponent, ctx, buffer)
  }

  // 结束标签
  buffer.push(`</${tag}>`)
}

SSR 渲染器不需要 RendererOptions,因为它不通过 createRenderer 创建——它直接操作 VNode 树,递归生成字符串。

水合(Hydration):SSR 到 CSR 的过渡

// packages/runtime-dom/src/index.ts
// DOM 渲染器通过 createHydrationRenderer 支持水合
export const { render, createApp } = ensureRenderer()

// 水合渲染器 = DOM 渲染器 + 额外的水合逻辑
export const { createApp: createSSRApp } = createHydrationRenderer({
  ...domRendererOptions,
  // 额外方法:用于在已有 DOM 上"认领"节点
  cloneNode: (el) => el.cloneNode(true),
  insertStaticContent: (content, parent, anchor) => {
    // SSR 产生的静态 HTML 内容直接插入,不需要逐节点创建
    const temp = document.createElement('template')
    temp.innerHTML = content
    const first = temp.content.firstChild as Element
    const last = temp.content.lastChild as Element
    parent.insertBefore(temp.content, anchor)
    return [first, last]
  }
})

// 水合过程:
// 1. 客户端接收 SSR HTML
// 2. createSSRApp().mount('#app') → 执行水合(不创建新 DOM,而是"认领"已有 DOM)
// 3. patch(existingDOMNode, newVNode) → 复用 DOM,只绑定事件监听器
// 4. 水合完成后,应用变为可交互状态

@vue/test-utils 与自定义渲染器的关系

// @vue/test-utils 底层使用 jsdom(一个 Node.js 版的 DOM 实现)
// 它不是自定义渲染器,而是在 jsdom 环境中运行真实的 DOM 渲染器

// 这意味着:
// 1. 测试速度:jsdom 比真实浏览器慢,比自定义渲染器慢
// 2. 准确性:jsdom 的 DOM API 覆盖率约 90%,部分 CSS/布局相关不可用
// 3. 使用场景:测试组件行为(不是视觉效果)

// 如果你在构建自定义渲染器,可以为它创建专属的测试渲染器:
const { render, createApp } = createRenderer(myCanvasRendererOptions)

function mountCanvas(component, props?) {
  const root = new CanvasNode('#fragment')
  const app = createApp(component, props)
  app.mount(root)
  return { root, app }
}

// 然后直接断言 CanvasNode 树,而不需要 jsdom
test('renders a rect', () => {
  const { root } = mountCanvas({ setup: () => () => h('rect', { fill: 'red' }) })
  expect(root.children[0].type).toBe('rect')
  expect(root.children[0].props.fill).toBe('red')
})

Vue 渲染器 vs React Reconciler:抽象层设计对比

Vue 3 的跨平台抽象:

@vue/runtime-core 定义 RendererOptions 接口(~12 个方法)
平台实现者实现这个接口
createRenderer(options) 将接口注入渲染器闭包

特点:
- 接口简单(12个方法)
- 同步调用:insert、remove 是同步的
- 不支持时间切片(没有 Concurrent Mode 等价物)
- diff 是同步的,长列表 diff 会阻塞主线程

────────────────────────────────────────────────────────

React 的跨平台抽象(Reconciler):

react-reconciler 定义 HostConfig 接口(~35 个方法)
平台实现者实现 HostConfig
创建:ReactReconciler(config)

特点:
- 接口复杂(35个方法,包括 prepareUpdate、commitMount 等阶段性方法)
- 异步调度:通过 Scheduler 支持可中断的渲染(Concurrent Mode)
- 更多控制点:渲染过程分 render 阶段(可中断)和 commit 阶段(同步)
- 代价:复杂度高,学习曲线陡峭

Vue 3 的选择是在简单性和灵活性之间取平衡:
12个方法足以覆盖 95% 的跨平台场景,
同时保持了渲染流程的可预测性和调试友好性。

Level 4 · 边界与陷阱(全体适用)

陷阱1:自定义渲染器中忘记处理 patchProp 的 null 值

// 当属性被移除时,nextValue 是 null
patchProp(el, 'fill', 'red', null)

// 错误写法:直接赋值
patchProp(el, key, prevValue, nextValue) {
  el.props[key] = nextValue  // el.props.fill = null → 下次渲染出现异常
}

// 正确写法:处理 null 值(删除属性)
patchProp(el, key, prevValue, nextValue) {
  if (nextValue === null || nextValue === undefined) {
    delete el.props[key]
  } else {
    el.props[key] = nextValue
  }
  scheduleRedraw()
}

陷阱2:scheduleRedraw 没有防重入导致性能问题

// 错误:每次 insert/remove/patchProp 都立即 redraw
patchProp(el, key, prevValue, nextValue) {
  el.props[key] = nextValue
  redraw()  // 如果一次更新涉及 50 个 prop,就会 redraw 50 次!
}

// 正确:使用 requestAnimationFrame 批量合并
let rafId = null
function scheduleRedraw() {
  if (rafId !== null) return  // 已经有等待中的 redraw,不重复调度
  rafId = requestAnimationFrame(() => {
    rafId = null
    redraw()
  })
}

陷阱3:SSR 中的 context 对象泄漏

// SSR 的 renderToString 接受一个 context 对象
// 用户可以在组件中通过 useSSRContext() 访问它
// 问题:在并发请求场景下,同一个 context 对象被多个请求共享

// 错误:使用全局 context
const globalContext = { state: {} }
app.use(store)
// 多个并发请求的 renderToString 都在修改同一个 state!

// 正确:每个请求创建新的 context
app.get('/page', async (req, res) => {
  const context = { req }  // 每个请求独立的 context
  const html = await renderToString(createApp(App), context)
  res.send(html)
})

陷阱4:自定义渲染器忘记实现 parentNode 导致 Teleport 失效

// Teleport 内部需要 parentNode 来确定锚点的父节点
// 如果没有正确实现,Teleport 会静默失败

// 错误:返回 undefined
parentNode(node) {
  return node.parent  // 如果 parent 从未被赋值,返回 undefined
}

// 正确:始终返回 null(而不是 undefined)
parentNode(node) {
  return node.parent || null  // null 是 RendererOptions 规定的"无父节点"返回值
}

陷阱5:在自定义渲染器中处理事件时忘记清理旧监听器

// 错误:每次 patchProp 都追加新监听器
patchProp(el, key, prevValue, nextValue) {
  if (key.startsWith('on')) {
    const eventName = key.slice(2).toLowerCase()
    el.canvas.addEventListener(eventName, nextValue)  // 没有移除旧的!
    // 每次更新都会追加一个新监听器,最终有数十个重复的监听器
  }
}

// 正确:先移除旧的,再添加新的
patchProp(el, key, prevValue, nextValue) {
  if (key.startsWith('on')) {
    const eventName = key.slice(2).toLowerCase()
    if (prevValue) {
      el.canvas.removeEventListener(eventName, prevValue)
    }
    if (nextValue) {
      el.canvas.addEventListener(eventName, nextValue)
    }
  }
}

陷阱6:SSR 中的数据获取时序

// 问题:组件在 onMounted 中获取数据,但 SSR 不执行 onMounted
const MyComp = defineComponent({
  setup() {
    const data = ref(null)
    onMounted(async () => {
      data.value = await fetch('/api/data').then(r => r.json())
    })
    // SSR 时 data.value 始终是 null
    return { data }
  }
})

// 正确:使用 async setup 或 onServerPrefetch
const MyComp = defineComponent({
  async setup() {
    // async setup 在 SSR 和客户端都会执行
    const data = await fetch('/api/data').then(r => r.json())
    return { data }
  }
  // 或:
  // setup() {
  //   const data = ref(null)
  //   onServerPrefetch(async () => {
  //     data.value = await fetch('/api/data').then(r => r.json())
  //   })
  //   return { data }
  // }
})

本章小结

  1. 分层设计是跨平台的基础@vue/runtime-core 通过 RendererOptions 接口(12 个方法)将所有平台相关操作抽象成可替换的驱动,本身不引用任何浏览器 API;createRenderer(options) 通过闭包把这 12 个方法注入到渲染器的所有内部函数中。

  2. 自定义渲染器的核心工作是树操作的映射insert/remove/parentNode/nextSibling 这四个方法定义了你的"节点"如何组成树形结构;createElement/createText 定义节点的创建方式;patchProp 定义属性如何被应用。实现这 10 个方法,就能运行完整的 Vue 组件系统。

  3. SSR 渲染器不通过 createRenderer 创建@vue/server-renderer 直接操作 VNode 树,递归生成 HTML 字符串;它不需要 insert/remove 等 DOM 树操作,因为它的"DOM"就是一个字符串缓冲区。

  4. 水合是 SSR 后的必要步骤:水合渲染器在已有 DOM 上"认领"节点(复用 DOM 元素),只绑定事件监听器,跳过节点创建;insertStaticContentcloneNode 是水合专用的两个额外 RendererOptions 方法。

  5. 小程序端渲染器的本质是 setData 翻译器:小程序的渲染层和逻辑层分离,自定义渲染器把 Vue 的 VNode 操作(insert/remove/patchProp)翻译成 setData 调用,通过框架的消息传递机制更新渲染层——这是 uni-app 等跨端框架的核心技术。

本章评分
4.7  / 5  (12 评分)

💬 留言讨论