Vue createRenderer 自定义渲染器从入门到实战

Vue createRenderer 自定义渲染器从入门到实战

🔥 Vue 3它不仅能高效渲染浏览器 DOM,还能实现小程序、Native 等多端运行。而支撑这一切的核心,就是 createRenderer 函数。它允许我们自定义渲染逻辑,摆脱 Vue 内置 DOM 渲染的限制,打造适配任意平台的渲染器

一、自定义 DOM 渲染器

示例重点实现支持事件绑定的 patchProp 方法,还会加入虚拟节点更新案例,直观看到渲染器的更新流程。

完整可运行代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Vue 自定义渲染器入门示例</title>
  <!-- 引入 Vue 3 完整版,方便浏览器直接运行 -->
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <!-- 渲染挂载容器 -->
  <div id="app"></div>

  <script>
    // 从 Vue 中解构出 createRenderer 和 h 函数
    const { createRenderer, h } = Vue;

    // 1. 创建自定义渲染器:传入平台渲染配置对象
    const renderer = createRenderer({
      // 创建元素节点:根据标签名创建 DOM 元素
      createElement(tag) {
        console.log(`[渲染步骤] 创建元素节点:<${tag}>`);
        return document.createElement(tag);
      },

      // 更新元素属性:核心改造!支持普通属性 + 事件绑定(onXXX 格式)
      patchProp(el, key, prevValue, nextValue) {
        // 判断是否是事件属性(以 on 开头,且第二个字母大写,如 onClick、onInput)
        const isEvent = key.startsWith('on') && /^on[A-Z]/.test(key);
        
        if (isEvent) {
          // 提取事件名(去掉 on 前缀,转为小写,如 onClick -> click)
          const eventName = key.slice(2).toLowerCase();
          
          // 移除旧的事件监听(如果有旧值)
          if (prevValue) {
            el.removeEventListener(eventName, prevValue);
          }
          
          // 绑定新的事件监听(如果有新值)
          if (nextValue) {
            el.addEventListener(eventName, nextValue);
            console.log(`[渲染步骤] 绑定事件:${eventName},回调函数已挂载`);
          }
        } else {
          // 普通属性:直接用 setAttribute 处理
          if (nextValue === undefined || nextValue === null) {
            el.removeAttribute(key);
            console.log(`[渲染步骤] 移除普通属性:${key}`);
          } else {
            el.setAttribute(key, nextValue);
            console.log(`[渲染步骤] 更新普通属性:${key} = ${nextValue}`);
          }
        }
      },

      // 插入元素:将子元素插入到父元素的指定位置
      insert(el, parent, anchor) {
        console.log(`[渲染步骤] 插入元素:将 <${el.tagName.toLowerCase()}> 插入到 <${parent.tagName.toLowerCase()}>`);
        parent.insertBefore(el, anchor || null);
      },

      // 移除元素:从父节点中移除当前元素
      remove(el) {
        console.log(`[渲染步骤] 移除元素:<${el.tagName.toLowerCase()}>`);
        el.parentNode.removeChild(el);
      },

      // 创建文本节点:创建 DOM 文本节点
      createText(text) {
        console.log(`[渲染步骤] 创建文本节点:${text}`);
        return document.createTextNode(text);
      },

      // 更新文本节点:修改文本节点的内容
      setText(node, text) {
        console.log(`[渲染步骤] 更新文本节点:${node.nodeValue} → ${text}`);
        node.nodeValue = text;
      }
    });

    // 2. 获取挂载容器
    const app = document.getElementById('app');

    // 3. 初始虚拟节点(无事件)
    const vnode1 = h('div', { title: '初始节点' }, 'Hello initial vnode');

    // 4. 1秒后更新的虚拟节点(带 onClick 事件)
    const vnode2 = h(
      'div',
      {
        onClick() {
          console.log('更新了!点击事件触发成功~');
        },
        title: '更新后节点(带点击事件)' // 同时更新普通属性
      },
      'hello world'
    );

    // 5. 先渲染初始虚拟节点
    renderer.render(vnode1, app);

    // 6. 1秒后更新虚拟节点,触发 patchProp 处理事件和属性更新
    setTimeout(() => {
      console.log('==== 开始更新虚拟节点 ====');
      renderer.render(vnode2, app);
    }, 1000);
  </script>
</body>
</html>

运行效果

  1. 打开浏览器运行该 HTML 文件,页面先显示 Hello initial vnode,鼠标悬浮弹出「初始节点」提示;
  2. 1秒后,文本自动更新为 hello world,悬浮提示变为「更新后节点(带点击事件)」;
  3. 点击文本所在的 div,控制台打印 更新了!点击事件触发成功~
  4. 全程控制台会清晰打印渲染、更新、事件绑定的日志,直观看到自定义渲染器的完整执行流程。

二、核心拆解:这段代码到底在做什么?

我们逐部分拆解代码,理解 createRenderer 的核心组成和工作逻辑,重点解析新增的虚拟节点更新案例。

1. 核心引入:createRendererh 函数

javascript 复制代码
const { createRenderer, h } = Vue;

这两个函数是实现自定义渲染的关键,各自承担核心职责:

  • createRenderer :Vue 3 提供的渲染器工厂函数 ,接收一套「平台渲染接口」,返回一个具备完整渲染能力的自定义渲染器实例。这个实例拥有 createApprender 方法,和 Vue 默认的 DOM 渲染器功能一致,只是渲染逻辑由我们自定义。
  • h 函数 :全称 createVNode,核心作用是构建虚拟 DOM 节点(VNode)。它接收标签名/组件、属性对象、子节点/文本内容,返回一个标准的 VNode 对象,作为渲染器的输入数据。

2. 核心步骤:创建自定义渲染器(createRenderer

javascript 复制代码
const renderer = createRenderer({ /* 渲染配置对象 */ });

createRenderer 接收一个配置对象作为唯一参数,这个对象必须实现 6 个核心方法,它们是渲染器与「目标平台」的交互桥梁,负责将 VNode 转换为目标平台的真实节点(这里是浏览器 DOM)。

6 个核心渲染方法详解(DOM 平台)
方法名 核心作用 入参说明
createElement 创建元素节点 tag:标签名(如 'div'、'p'),返回创建好的 DOM 元素
patchProp 更新元素属性 el:真实 DOM 元素、key:属性名、prevValue:旧属性值、nextValue:新属性值
insert 插入元素 el:要插入的 DOM 元素、parent:父 DOM 元素、anchor:插入参考节点(null 则插入末尾)
remove 移除元素 el:要移除的 DOM 元素
createText 创建文本节点 text:文本内容,返回创建好的 DOM 文本节点
setText 更新文本节点 node:真实 DOM 文本节点、text:新的文本内容
关键亮点:patchProp 支持事件绑定

本次改造的核心是 patchProp 方法,它不仅能处理 title 这类普通属性,还能识别 onClick 这类事件属性,实现 DOM 事件的绑定与移除:

  • 先判断属性是否为 onXXX 格式的事件;
  • 提取原生事件名(onClickclick);
  • 遵循「先清后绑」原则,避免重复绑定导致多次触发。

3. 新增亮点:虚拟节点更新案例(核心解析)

自定义渲染器如何处理 VNode 更新,这也是 Vue 响应式更新的底层缩影:

javascript 复制代码
// 2. 获取挂载容器
const app = document.getElementById('app');

// 3. 初始虚拟节点(无事件)
const vnode1 = h('div', { title: '初始节点' }, 'Hello initial vnode');

// 4. 1秒后更新的虚拟节点(带 onClick 事件)
const vnode2 = h(
  'div',
  {
    onClick() {
      console.log('更新了!点击事件触发成功~');
    },
    title: '更新后节点(带点击事件)' // 同时更新普通属性
  },
  'hello world'
);

// 5. 先渲染初始虚拟节点
renderer.render(vnode1, app);

// 6. 1秒后更新虚拟节点,触发 patchProp 处理事件和属性更新
setTimeout(() => {
  console.log('==== 开始更新虚拟节点 ====');
  renderer.render(vnode2, app);
}, 1000);
这段代码的核心逻辑:
  1. 初始渲染 :调用 renderer.render(vnode1, app),渲染器将 vnode1 转换为真实 DOM,插入到挂载容器中,完成首次渲染;
  2. 延迟更新 :1 秒后调用 renderer.render(vnode2, app),渲染器会自动对比 vnode1vnode2 的差异(属性、文本内容);
  3. 差异更新
    • 对于 title 属性:触发 patchProp 方法,将旧值「初始节点」更新为新值「更新后节点(带点击事件)」;
    • 对于 onClick 事件:触发 patchProp 方法,绑定新的点击事件回调;
    • 对于文本内容:触发 setText 方法,将「Hello initial vnode」更新为「hello world」;
  4. 无全量重建:整个更新过程没有删除旧 DOM 再创建新 DOM,而是只更新有差异的部分,这也是 Vue 渲染高效的核心原因。

4. 挂载应用的两种方式

案例使用 renderer.render(vnode, container) 直接渲染 VNode,除此之外,也可以通过 renderer.createApp(component).mount(container) 挂载组件,两种方式均有效:

  • 直接渲染 VNode:更灵活,适合手动控制渲染流程(如本次的延迟更新案例);
  • 通过 createApp 挂载:更贴近日常 Vue 开发,适合组件化开发场景。

三、深入理解:自定义渲染器的工作流程

整个渲染与更新过程可以总结为 4 个核心步骤,形成一个完整的闭环:

  1. 生成 VNode :通过 h 函数创建标准 VNode,提供渲染的数据源;
  2. 首次渲染:渲染器调用 6 个核心方法,将 VNode 转换为真实节点,插入到挂载容器中;
  3. VNode 对比:更新时,渲染器对比新旧 VNode,找出属性、文本等差异;
  4. 差异更新 :针对差异部分,调用对应的 patchPropsetText 等方法,更新真实节点,无需全量重建。
相关推荐
ywf12151 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭1 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf7 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特7 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷8 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian8 小时前
前端node常用配置
前端
华洛9 小时前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq9 小时前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A10 小时前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常10 小时前
被EdgeToEdge适配折磨疯了,谁懂!
前端