手写 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 中导出的成员。

相关推荐
LCG元3 小时前
Vue.js组件开发-使用vue-pdf显示PDF
vue.js
哥谭居民00014 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
烟波人长安吖~4 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
PleaSure乐事5 小时前
使用Vue的props进行组件传递校验时出现 Extraneous non-props attributes的解决方案
vue.js
土豆炒马铃薯。6 小时前
【Vue】前端使用node.js对数据库直接进行CRUD操作
前端·javascript·vue.js·node.js·html5
赵大仁7 小时前
深入解析 Vue 3 的核心原理
前端·javascript·vue.js·react.js·ecmascript
bidepanm7 小时前
Vue.use()和Vue.component()
前端·javascript·vue.js
Ashore_9 小时前
从简单封装到数据响应:Vue如何引领开发新模式❓❗️
前端·vue.js
顽疲9 小时前
从零用java实现 小红书 springboot vue uniapp (6)用户登录鉴权及发布笔记
java·vue.js·spring boot·uni-app
&活在当下&9 小时前
ref 和 reactive 的用法和区别
前端·javascript·vue.js