手写 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 小时前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
程序员爱技术5 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
cs_dn_Jie10 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic11 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿11 小时前
webWorker基本用法
前端·javascript·vue.js
customer0812 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
getaxiosluo13 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v13 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
栈老师不回家14 小时前
Vue 计算属性和监听器
前端·javascript·vue.js