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

相关推荐
徐小夕1 小时前
100小时,我做了一款AI CAD建模软件,开源!
前端·vue.js·github
淸湫3 小时前
项目中使用了全局权限管理,请详细描述如何通过Vue Router的路由守卫来实现全局权限控制?
前端·vue.js
李剑一3 小时前
前端必看 | Vue 刷新页面,生命周期钩子直接 "罢工",原来问题在这?90% 开发者都栽过!
前端·vue.js
閞杺哋笨小孩3 小时前
域名驱动多租户入驻:后台配置 + 前端解析
前端·vue.js
用户125758524364 小时前
写了三年定时任务还在手改 Cron 表达式?这个 GoFrame 后台框架帮你全闭环了
vue.js
前端那点事5 小时前
Vue3自定义Hooks保姆级教程!从原理到企业级实战,告别混乱代码
前端·vue.js
前端那点事5 小时前
别再乱用Vue3响应式!ref、reactive、toRef、toRefs完整区别+企业级落地实战
前端·vue.js
閞杺哋笨小孩5 小时前
从脚手架到构建注入:Vue 多租户「入驻」工程实践
vue.js·vite
卤蛋fg67 小时前
VxeTable 实现表尾合计行并支持数据实时统计
vue.js
杨大厨wd7 小时前
Vue3 业务组件封装别只会传 props:如何设计一个真正好用的组件
vue.js