23-实现customRenderer

166 阅读4分钟

createRenderer

vue3有一个高阶api createRenderer,它可以自定义我们的渲染器

image.png

这章节模拟实现createRenderer,就目前代码,只支持dom类型渲染,进一步去修改整个render的逻辑

实现

修改render逻辑

  1. 暴露整个render.js,即createRenderer api
  2. 提取了element.ts 和 component.js 中的processElement、processComponent、processFragment、processTextNode等4个初始化函数
  3. 修改mountElement内部创建实例、设置属性、添加到根元素方法变为自定义传入,使其支持多种渲染器
  4. render暴露一个createApp的方法,供用户创建实例使用
/*
 * @Author: Lin zefan
 * @Date: 2022-03-21 22:04:58
 * @LastEditTime: 2022-04-01 22:01:09
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\runtime-core\render.ts
 *
 */

import { ShapeFlags } from "../shared/ShapeFlags";
import { createComponentInstance, setupComponent } from "./component";
import { createAppAPI } from "./createdApp";
import {
  Fragment,
  getChildrenShapeFlags,
  getShapeFlags,
  TextNode,
} from "./vnode";

export function createRenderer(options) {
  // 改名字是为了 debug 方便
  const {
    createElement: hostCreateElement,
    insert: hostInsert,
    patchProp: hostPatchProp,
    selector: hostSelector,
  } = options;

  function render(vnode, container) {
    // 根组件没有父级,所以是null
    patch(vnode, container, null);
  }

  function patch(vnode, container, parentComponent) {
    if (!vnode) return;
    const { type } = vnode;

    switch (type) {
      case Fragment:
        processFragment(vnode, container, parentComponent);
        break;
      case TextNode:
        processTextNode(vnode, container);
        break;

      default:
        const shapeFlags = getShapeFlags(type);

        if (shapeFlags === ShapeFlags.COMPONENT) {
          // 是一个Component
          processComponent(vnode, container, parentComponent);
        } else if (shapeFlags === ShapeFlags.ELEMENT) {
          // 是一个element
          processElement(vnode, container, parentComponent);
        }

        break;
    }
  }

  // 创建一个Fragment节点
  function processFragment(vnode: any, container: any, parentComponent) {
    mountChildren(vnode.children, container, parentComponent);
  }

  // 创建一个TextNode节点
  function processTextNode(vnode: any, container: any) {
    const textNode = document.createTextNode(vnode.children);
    container.append(textNode);
  }

  // ---------------------Element----------------------
  function processElement(vnode, container, parentComponent) {
    mountElement(vnode, container, parentComponent);
  }
  // ---------------------Element创建流程----------------------
  function mountElement(vnode, container, parentComponent) {
    const { type, props, children } = vnode;

    // 创建根元素、将元素挂载到实例
    const el = (vnode.$el = hostCreateElement(type));

    // 设置行内属性
    for (const key in props) {
      hostPatchProp(el, key, props);
    }

    // 设置children
    const shapeFlags = getChildrenShapeFlags(children);
    if (shapeFlags === ShapeFlags.TEXT_CHILDREN) {
      el.textContent = children;
    } else if (shapeFlags === ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(children, el, parentComponent);
    }

    hostInsert(el, container);
  }

  function mountChildren(children, container, parentComponent) {
    children.forEach((h) => {
      patch(h, container, parentComponent);
    });
  }

  // ---------------------Component---------------------------
  function processComponent(vnode, container, parentComponent) {
    // TODO,这里会比较vnode,然后做创建、更新操作,这里先处理创建

    // 创建组件
    mountComponent(vnode, container, parentComponent);

    // TODO,更新组件
    //   updateComponent(vnode, container);
  }

  // -----------------Component创建流程-------------------
  function mountComponent(vnode, container, parentComponent) {
    // 初始化Component实例
    const instance = createComponentInstance(vnode, parentComponent);
    // 初始化setup函数return的数据
    setupComponent(instance, container);

    // setupRenderEffect
    setupRenderEffect(instance, container);
  }

  function setupRenderEffect(instance, container) {
    const { proxy, vnode } = instance;
    // 通过render函数,获取render返回虚拟节点,并绑定render的this
    const subTree = instance.render.call(proxy);
    /**
     * 1. 调用组件render后把结果再次给到patch
     * 2. 再把对应的dom节点append到container
     * 3. 把当前实例传过去,让子组件可以通过parent获取父组件实例
     */
    patch(subTree, container, instance);
    /** 挂载当前的dom元素到$el
     * 1. 当遍历完所有Component组件后,会调用processElement
     * 2. 在processElement中,会创建dom元素,把创建的dom元素挂载到传入的vnode里面
     * 3. 当前的dom元素也就是processElement中创建的dom元素
     */
    vnode.el = subTree.$el;
  }

  // 暴露
  return {
    /** 将createApp方法暴露出去
     * 参数一为 render渲染函数,调用patch
     * 参数二为 是一个函数,返回一个节点,是可选的
     */
    createApp: createAppAPI(render, hostSelector),
  };
}

这一步做完,不要忘记删掉多余的文件和函数

创建runtime-dom

runtime-dom 文件夹处理的都是dom的玩意; 之前dom的操作逻辑,抽离到这边来

/*
 * @Author: Lin zefan
 * @Date: 2022-04-01 16:53:01
 * @LastEditTime: 2022-04-01 22:14:01
 * @LastEditors: Lin zefan
 * @Description: dom渲染
 * @FilePath: \mini-vue3\src\runtime-dom\index.ts
 *
 */

import { createRenderer } from "../runtime-core/render";
import { isDom } from "../shared/index";

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

const isEvents = (key: string = "") => {
  const reg = /^on[A-Z]/;
  if (reg.test(key)) {
    // onClick -> click
    return key.slice(2).toLocaleLowerCase();
  }
  return "";
};

export function patchProp(el, key, props) {
  /** 注册事件
   * 1. 判断是否on开头并包含一个大写字母开头
   * 2. 是的话,截取on后面的内容
   * 3. 注册元素事件
   */

  const val = props[key];
  if (isEvents(key)) {
    el.addEventListener(isEvents(key), val);
  } else {
    el.setAttribute(key, val);
  }
}

export function insert(el, parent) {
  parent.appendChild(el);
}

export function selector(container) {
  return isDom(container);
}

const renderer: any = createRenderer({
  createElement,
  patchProp,
  insert,
  selector,
});

/**
 * 暴露 createApp,这个方法就是创建vue实例的方法
 * @param args 当前的根节点,一般是App.js
 */
export const createApp = (...args) => {
  return renderer.createApp(...args);
};

// runtime-core是底层逻辑,放到这边暴露出去
export * from "../runtime-core/index";

创建createAppAPI函数

createAppAPI其实就在createApp的基础上包了一层,把render函数传了过去,因为render不再暴露了

/*
 * @Author: Lin zefan
 * @Date: 2022-03-21 21:49:41
 * @LastEditTime: 2022-04-01 22:02:20
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\runtime-core\createdApp.ts
 *
 */

import { isDom } from "../shared/index";
import { createdVNode } from "./vnode";

/**
 * 创建一个Vue实例
 * @param renderer render函数,内部调用了patch
 * @param selector selector函数,内部返回一个节点
 * @returns 
 */
export function createAppAPI(renderer, selector) {
  return function createApp(rootComponent) {
    return {
      // 暴露一个mount方法
      mount(rootContainer) {
        /**
         * 1. 将根组件(rootComponent)转换为vnode
         * 2. 再通过render函数将vnode渲染到mount接收的容器(rootContainer)中
         */
        const vnode = createdVNode(rootComponent);
        renderer(
          vnode,
          selector ? selector(rootContainer) : isDom(rootContainer)
        );
      },
    };
  };
}

修改打包入口引用

rollup打包的入口文件改为 runtime-dom,因为现在createApp方法放在 runtime-dom/index里边

/*
 * @Author: Lin zefan
 * @Date: 2022-03-22 15:40:00
 * @LastEditTime: 2022-04-01 18:19:05
 * @LastEditors: Lin zefan
 * @Description: 打包入口文件
 * @FilePath: \mini-vue3\src\index.ts
 *
 */

// 暴露runtime-core模块
export * from "./runtime-dom/index";

暴露createRenderer api

/*
 * @Author: Lin zefan
 * @Date: 2022-03-22 16:22:33
 * @LastEditTime: 2022-04-01 21:34:44
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\runtime-core\index.ts
 *
 */

export * from "./createdApp";
export { h } from "./h";
export { renderSlots } from "./helpers/renderSlots";
export { createTextVNode } from "./vnode";
export { getCurrentInstance } from "./component";
export { provide, inject } from "./apiInject";
export { createRenderer } from "./render";

贴一下新增的方法

/*
 * @Author: Lin zefan
 * @Date: 2022-03-15 19:28:09
 * @LastEditTime: 2022-04-01 18:27:13
 * @LastEditors: Lin zefan
 * @Description: 公用hook
 * @FilePath: \mini-vue3\src\shared\index.ts
 *
 */

export const isDom = (rootContainer) => {
  if (typeof rootContainer === "string") {
    return document.querySelector(rootContainer);
  }
  return rootContainer;
};