手写 mini-vue3 - 实现 custom-renderer(八)

实现自定义渲染函数 createRenderer,这是为了能兼容 canvas 等的 API 的使用。

官方给的说法是:

创建一个自定义渲染器。通过提供平台特定的节点创建以及更改 API,你可以在非 DOM 环境中也享受到 Vue 核心运行时的特性。

回顾上几节笔记,关于 DOM 渲染的功能都放到了 renderer.ts 这个模块中。

DOM 渲染到页面,经过的步骤有:

  1. 创建新标签
  2. 为标签注册事件、添加属性
  3. 孩子节点若为文本,直接设置标签内容为该文本,否则遍历 children 递归渲染 DOM
  4. 把新创建的标签添加到页面

以上除了递归 Array 类型的 children vnode 外,其余的都是操作 DOM。

我们在 DOM 环境下有一套渲染标签的 API,而在非 DOM 环境下也有不同的 API,那为了使得可以兼容不同平台的渲染,可以把原本写死的 DOM API 逻辑抽离出来,内部抛出对应的 API 让用户去自定义实现页面的渲染。

createRenderer 的基本实现思路:

  1. 把原本写在 renderer.ts 的方法,用一个函数包裹着,该函数为 createRenderer

  2. options 作为参数,用户自定义渲染的 API 作为 options 的一员,传入给 createRenderer 实现

  3. 内部抽离自定义渲染的逻辑,改为使用 options 的 API 抽象实现

  4. 重构 createApp.ts, 原本的 createApp 修改为闭包,即作为新的函数 createAppAPI 的返回值函数。把 render 函数作为 createAppAPI 参数。renderer.ts 模块中,createRenderer 抛出 createApp 成员。

    ts 复制代码
    // runtime-core/createApp.ts
    import { createVNode } from './vnode';
    export function createAppAPI(render) {
      return function createApp(rootComponent) {
        return {
          mount(rootContainer) {
            const vnode = createVNode(rootComponent);
            render(vnode, rootContainer);
          }
        }
      }
    }
    ts 复制代码
    // renderer.ts
    import { createAppAPI } from './createApp.ts';
    export function createRenderer(options) {
        
        function render() {
            ...
        }
        
        return {
            createApp: createAppAPI(render)
        }
    }

options API:

  • createElement - 创建新标签
  • patchProp - 添加新标签
  • insert - 移除标签
  • setElementText - 设置文本
ts 复制代码
// runtime-core/renderer.ts
import { createAppAPI } from './createApp';

export function createRender(options) {
  const { 
    createElement: hostCreateElement, // 创建新标签 API
    patchProp: hostPatchProp, // 处理属性 API
    insert: hostInsert,  // 添加新标签 API
    remove: hostRemove, // 移除标签 API
    setElementText: hostSetElementText, // 设置文本 API
  } = options;
  
  function render(vnode, container) {
    ...
  }

  function patch(vnode, container, parentComponent) {
    ...
  }
  
  function processElement(vnode, container, parentComponent) {
    mountElement(vnode, container, parentComponent);
  }

  // 重构,把原本的 DOM 操作使用 API 来替代
  function mountElement(vnode, container, parentComponent) {
    // 1. 创建标签
    const el = (vnode.el = hostCreateElement(vnode.type));
  
    // 2. 处理 标签内容
    const { children, shapeFlag } = vnode;
    // children 是字符串
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // el.textContent = children;
      hostSetElementText(ell, children);
      // children 是数组
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      ...
    }
  
    const { props } = vnode;
  
    // 3. 处理标签的 props 属性
    // 循环 props
    for (const key in props) {
      const val = props[key];
      // 处理注册事件和 props
      hostPatchProp(el, key, null, val);
    }
  
    // 4. 把 el 添加到 container
    hostInsert(el, container);
  }
  
  function mountChildren(children, container, parentComponent) {
    ...
  }
  
  function processComponent(vnode, container, parentComponent) {
    ...
  }
  
  function mountComponent(initialVNode, container, parentComponent) {
    ...
  }
  
  function processFragment(vnode container, parentComponent) {
    ...
  }
  
  function mountFragment(vnode, container, parentComponent) {
    ...
  }
  
  function processText(vnode, container) {
    ...
  }
  
  function setupRenderEffect(instance, initialVNode, container) {
      ...
  }

  return {
    createApp: createAppAPI(render),
  }
}
ts 复制代码
// runtime-core/index.ts
export { createRenderer } from './renderer';

vue 内部还是需要实现 DOM 环境的渲染的,所以新建 runtime-dom/index.ts,把刚才的具体实现移到这个模块来。

这时我们就可以引入 createRenderer 来实现一下了。

ts 复制代码
// runtime-dom/index.ts

import { createRenderer } from '../runtime-core';

function createElement(type) {
  return document.createElement(type);
}

function patchProp(el, key, prevVal, nextVal) {
  const isOn = (key: string) => /^on[A-Z]/.test(key);
  if (isOn(key)) {
    const event = key.slice(2).toLowerCase();
    el.addEventListener(event, nextVal);
  } else {
    if(nextVal === undefined || nextVal === null) {
      el.removeAttribute(key);
    } else {
      el.setAttribute(key, nextVal);
    }
  }
}

function insert(child, parent) {
  parent.append(el);
}

function remove(child) {
  const parent = child.parentNode;
  if(parent) {
    parent.removeChild(child);
  }
}

function setElementText(el, text) {
  el.textContent = text;
}

// 使用刚才写好的 createRender API
const renderer: any = createRenderer({
  createElement,
  patchProp,
  insert,
  remove,
  setElementText,
});

// 在这里抛出 createApp 给外部使用
export function createApp(...args) {
  return renderer.createApp(...args)
}

export * from '../runtime-core';

为什么在最后写 export * from '../runtime-core'; 呢?

这是遵循了 vue3 的模块依赖关系有序性。

因为 runtime-dom 依赖于 runtime-core,所以最终在末尾添加导出 runtime-core 的声明。

这样,我们在 src/index.ts 这个总入口,导出:

ts 复制代码
export * from './runtime-dom';

这样,在打包后,便向用户抛出了所有在 runtime-core/indexruntime-dom/index 中导出的成员。

相关推荐
吴声子夜歌10 分钟前
Vue3——新语法
前端·javascript·vue.js
jiayong2313 分钟前
第 36 课:任务详情抽屉快捷改状态
开发语言·前端·javascript·vue.js·学习
琢磨先生TT38 分钟前
为什么很多后台系统功能不少,看起来却还是很廉价?
前端·vue.js·设计
一 乐2 小时前
交通感知与车路协同系统|基于springboot + vue交通感知与车路协同系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·交通感知与车路协同系统
踩着两条虫3 小时前
强强联合!VTJ.PRO 正式接入 DeepSeek V4,AI 编码能力再跃升
前端·vue.js·ai编程
Beginner x_u4 小时前
前端八股整理(Vue 02)|组件通信、生命周期、v-if 与 v-show
前端·javascript·vue.js
万物得其道者成4 小时前
Vue3 使用 Notification 浏览器通知,解决页面关闭后旧通知点击无法跳转问题
前端·vue.js·edge浏览器
一條狗4 小时前
学习日报 20260423|[特殊字符] 深度解析:Vue 3 SPA 部署到 Spring Boot 的 404/500 错误排查与完美解决方案-2
vue.js·spring boot·学习
LIO6 小时前
Vue 3 实战——搜索框检索高亮的优雅实现
前端·vue.js
_thought6 小时前
踩坑记录:Vue Devtools(Vue2版)在火狐浏览器上,未在控制台显示
前端·javascript·vue.js