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

相关推荐
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
一路向前的月光8 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   8 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Jiaberrr9 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
程序员大金12 小时前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
道爷我悟了13 小时前
Vue入门-指令学习-v-html
vue.js·学习·html
无咎.lsy13 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
工业互联网专业14 小时前
毕业设计选题:基于ssm+vue+uniapp的校园水电费管理小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
计算机学姐14 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis
twins352015 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js