# 三、运行时核心

本章内容图解

image

可以以这张图为向导, 阅读本章节

GitHub: https://github.com/Roman-29/mini-vue

# 初始化主流程

观察图片, 如果我们要实现功能, 可以将主流程分为 component 和 element 两个方向处理

# 测试代码

App.js

import { createVnode as h } from "../../lib/mini-vue.esm.js";

export const App = {
  render() {
    return h(
      // type
      "div",
      // props
      {
        id: "root",
        class: ["red", "hard"],
      },
      // children
      [h("p", { class: "red" }, "hi"), h("p", { class: "blue" }, "mini-vue")]
    );
  },

  setup() {
    return {};
  },
};

main.js

import { createApp } from "../../lib/mini-vue.esm.js";
import { App } from "./App.js";

const rootContainer = document.querySelector("#app");
createApp(App).mount(rootContainer);

index.html

<html lang="en">
  <head>
    <title>Document</title>
    <style>
      .red {
        color: red;
      }

      .blue {
        color: blue;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script src="main.js" type="module"></script>
  </body>
</html>

# component 主流程代码

创建 createApp.ts 作为整个运行时的起始

import { render } from "./renderer";
import { createVnode } from "./vnode";

export function createApp(rootComponent) {
  return {
    mount(rootContainer) {
      // 将组件转化为虚拟节点
      // component -> vnode
      const vnode = createVnode(rootComponent);

      render(vnode, rootContainer);
    },
  };
}

创建的 app 实例应该有个 mount 方法, 用于挂载节点. 对组件进行渲染时需要根据用途分类新建代码文件处理:

vnode.ts 用来转换虚拟节点

export function createVnode(type, props?, children?) {
  const vnode = {
    type,
    props,
    children,
    el,
  };

  return vnode;
}

renderer.ts 用来渲染虚拟节点

import { createComponentInstance, setupComponent } from "./component";

export function render(vnode, container) {
  patch(vnode, container);
}

function patch(vnode, container) {
  // 处理组件
  processComponent(vnode, container);
}

function processComponent(vnode, container) {
  mountComponent(vnode, container);
}

function mountComponent(vnode, container) {
  const instance = createComponentInstance(vnode);

  setupComponent(instance);
  setupRenderEffect(instance, container);
}

function setupRenderEffect(instance, container) {
  const subTree = instance.render();

  // 拿到组件render函数渲染的虚拟节点继续递归patch
  patch(subTree, container);

  initialVNode.el = subTree.el;
}

component.ts 用来处理组件内容

import { proxyRefs } from "../reactivity";

export function createComponentInstance(vnode) {
  const component = {
    vnode,
    type: vnode.type, // 类型
    setupState: {}, // setup的return值
    el: null, // 组件对应的DOM节点
    props: {},
    slots: {},
  };

  return component;
}

export function setupComponent(instance) {
  // TODO
  // initProps
  // initSlots

  setupStatefulComponent(instance);
}

function setupStatefulComponent(instance) {
  const Component = instance.type;

  const { setup } = Component;

  if (setup) {
    const setupResult = setup();

    handleSetupResult(instance, setupResult);
  }
}

function handleSetupResult(instance, setupResult) {
  // function or object
  // TODO function(render函数)

  if (typeof setupResult === "object") {
    // 这里需要将setup里的ref对象全部代理出来, 从setupResult获取的ref都不再需要.value获取值
    instance.setupState = proxyRefs(setupResult);
  }

  finishComponentSetup(instance);
}

function finishComponentSetup(instance) {
  const Component = instance.type;

  if (Component.render) {
    instance.render = Component.render;
  }
}

# element 主流程代码

renderer.ts 之前只处理了 component 类型的虚拟节点, 现在补充处理 element 节点

function patch(vnode, container) {
  // 需要判断vnode是组件还是element
  if (typeof vnode.type === "string") {
    // type是
    processElement(vnode, container);
  } else if (isObject(vnode.type)) {
    // 处理组件
    processComponent(vnode, container);
  }
}

function processElement(vnode, container) {
  mountElement(vnode, container);
}

function mountElement(vnode, container) {
  const el = (vnode.el = document.createElement(vnode.type));

  const { props, children } = vnode;

  for (const key in props) {
    const val = props[key];
    el.setAttribute(key, val);
  }

  if (typeof children === "string") {
    el.textContent = children;
  } else if (Array.isArray(children)) {
    mountChildren(vnode, el);
  }
  container.append(el);
}

function mountChildren(vnode, container) {
  vnode.children.forEach((v) => {
    patch(v, container);
  });
}

# 实现组价代理对象

# 简介

上面我们实现的组件渲染并没有将变量用到组件渲染中, 现在我们修改测试代码, 将 setup 中返回的变量放到 render 函数中

App.js

export const App = {
  render() {
    return h(
      "div",
      {
        id: "root",
        class: ["red", "hard"],
      },
      this.msg
    );
  },

  setup() {
    return {
      msg: "power by mini-vue",
    };
  },
};

# 代码

要实现在 render 函数中使用 this 调用到 setup 的值, 就需要用到代理对象

在 component.ts 中创建 proxy

import { PublicInstanceProxyHandles } from "./componentPublicInstance";

function setupStatefulComponent(instance) {
  const Component = instance.type;

  // 执行setup之前先给组件创建代理对象
  instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandles);

  const { setup } = Component;

  if (setup) {
    const setupResult = setup();

    handleSetupResult(instance, setupResult);
  }
}

componentPublicInstance.ts

// 代理出组件实例的关键信息
const publicPropertiesMap = {
  $el: (i) => i.vnode.el,
  $slots: (i) => i.slots,
  $props: (i) => i.props,
};

export const PublicInstanceProxyHandles = {
  get: function({ _: instance }, key) {
    // setup属性
    const { setupState } = instance;
    if (hasOwn(setupState, key)) {
      return setupState[key];
    }

    // 组件属性
    const pulicGetter = publicPropertiesMap[key];
    if (pulicGetter) {
      return pulicGetter(instance);
    }
  },
};

const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key);

修改 renderer.ts

function setupRenderEffect(instance, initialVNode, container) {
  const { proxy } = instance;

  // render函数this指向proxy
  const subTree = instance.render.call(proxy);

  // vnode-> patch
  // vnode-> element-> mountElement
  patch(subTree, container);

  initialVNode.el = subTree.el;
}

# shapeFlags

# 简介

shapeFlags 是描述虚拟节点的类型的一种标识, 用来更好的区分虚拟节点, 进行不同的逻辑处理.

# 代码

新建 ShapeFlags.ts 通过位运算判断类型

export const enum ShapeFlags {
  ELEMENT = 1, // 0001
  STATEFUL_COMPONENT = 1 << 1, // 0010
  TEXT_CHILDREN = 1 << 2, // 0100
  ARRAY_CHILDREN = 1 << 3, // 1000
}

修改 vnode.ts 将 ShapeFlags 挂载到虚拟节点上

import { ShapeFlags } from "../share/ShapeFlags";
export function createVnode(type, props?, children?) {
  const vnode = {
    type,
    props,
    children,
    shapeFlag: getShapeFlag(type),
    el: null,
  };

  if (typeof children === "string") {
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
  } else if (Array.isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  }

  return vnode;
}

最后修改 renderer.ts

function patch(vnode, container) {
  // 需要判断vnode是组件还是element
  const { shapeFlag } = vnode;
  if (shapeFlag & ShapeFlags.ELEMENT) {
    processElement(vnode, container);
  } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    // 处理组件
    processComponent(vnode, container);
  }
}

function mountElement(vnode, container) {
  const el = (vnode.el = document.createElement(vnode.type));

  const { props, children, shapeFlag } = vnode;

  for (const key in props) {
    const val = props[key];
    el.setAttribute(key, val);
  }

  // 判断子节点类型
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    el.textContent = children;
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(vnode, el);
  }
  container.append(el);
}

# 事件注册功能

# 简介

在 render 函数中的 props 参数增加一个事件名, 用来注册事件, 测试代码如下

export const App = {
  render() {
    return h(
      "div",
      {
        id: "root",
        class: ["red", "hard"],
        onClick() {
          console.log("click");
        },
        onMousedown() {
          console.log("mousedown");
        },
      },
      this.msg
    );
  },

  setup() {
    return {
      msg: "power by mini-vue",
    };
  },
};

# 代码

修改 renderer.ts , 在挂载 element 的时候将事件注册到 el 中

function mountElement(vnode, container) {
  const el = (vnode.el = document.createElement(vnode.type));

  const { props, children, shapeFlag } = vnode;

  for (const key in props) {
    const val = props[key];
    // 判断是否为事件
    const isOn = (key: string) => /^on[A-Z]/.test(key);
    if (isOn(key)) {
      const event = key.slice(2).toLowerCase();
      el.addEventListener(event, val);
    } else {
      el.setAttribute(key, val);
    }
  }

  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    el.textContent = children;
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(vnode, el);
  }
  container.append(el);
}

# 组件的 props 功能

# 简介

在父子组件通信的时候, 我们会用到 props 将父组件的数据传递给子组件, 测试代码如下:

App.js

export const App = {
  render() {
    return h(
      "div",
      {
        id: "root",
      },
      [h(Foo, { count: 1 })]
    );
  },

  setup() {
    return {
      msg: "power by mini-vue",
    };
  },
};

const Foo = {
  name: "foo",
  setup(props) {
    console.log(props);
  },
  render() {
    // this.count可以获取到值
    return h("div", {}, "foo: " + this.count);
  },
};

# 代码

修改 component.ts , 在 setup 的时候初始化 props

export function setupComponent(instance) {
  // 初始化props, 将虚拟节点的props挂载到组件中
  instance.props = instance.vnode.props || {};

  // TODO
  // initSlots

  setupStatefulComponent(instance);
}

function setupStatefulComponent(instance) {
  const Component = instance.type;

  // 执行setup之前先给组件创建代理对象
  instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandles);

  const { setup } = Component;

  if (setup) {
    //  对props进行shallowReadonly封装, 禁止子组件修改props
    const setupResult = setup(shallowReadonly(instance.props));

    handleSetupResult(instance, setupResult);
  }
}

子组件如何通过 this 访问 props 呢? 也是通过代理, 在 componentPublicInstance 增加获取 props 的逻辑

export const PublicInstanceProxyHandles = {
  get: function({ _: instance }, key) {
    const { setupState, props } = instance;
    if (key in setupState) {
      return setupState[key];
    }

    if (hasOwn(setupState, key)) {
      return setupState[key];
    } else if (hasOwn(props, key)) {
      // 如果key在props里有值, 就返回props的值
      return props[key];
    }

    const pulicGetter = publicPropertiesMap[key];
    if (pulicGetter) {
      return pulicGetter(instance);
    }
  },
};

# 组件 emit 功能

# 简介

在父子组件通信的时候, 我们会用到 emit 在子组件触发自定义事件, 测试代码如下:

App.js

export const App = {
  name: "App",
  render() {
    // emit
    return h("div", {}, [
      h("div", {}, "App"),
      h(Foo, {
        onAdd(a, b) {
          console.log("onAdd", a, b);
        },
      }),
    ]);
  },

  setup() {
    return {};
  },
};

const Foo = {
  name: "foo",
  render() {
    const btn = h(
      "button",
      {
        onClick: this.emitAdd,
      },
      "emitAdd"
    );

    const foo = h("p", {}, "foo");
    return h("div", {}, [foo, btn]);
  },

  setup(props, { emit }) {
    const emitAdd = () => {
      emit("add", 1, 2);
    };

    return {
      emitAdd,
    };
  },
};

# 代码

修改 component.ts 将 emit 存放到组件实例中, 并且放到 setup 参数中

import { emit } from "./componentEmit";

export function createComponentInstance(vnode) {
  const component = {
    vnode,
    type: vnode.type,
    setupState: {},
    el: null,
    props: {},
    slots: {},
    emit: () => {},
  };

  // 锁定第一个参数是组件instance
  component.emit = emit.bind(null, component) as any;

  return component;
}

function setupStatefulComponent(instance) {
  const Component = instance.type;

  // 执行setup之前先给组件创建代理对象
  instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandles);

  const { setup } = Component;

  if (setup) {
    //  对props进行shallowReadonly封装, 禁止子组件修改props
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit,
    });

    handleSetupResult(instance, setupResult);
  }
}

新增 componentEmit.ts 专门处理 emit

export function emit(instance, event, ...args) {
  // 父组件自定义事件也是存放在 props 中
  const { props } = instance;

  const handlerName = toHandlerKey(camelize(event));

  const handler = props[handlerName];
  handler && handler(...args);
}

// 增加on开头
const toHandlerKey = (str) => {
  return str ? "on" + capitalize(str) : "";
};

// add -> Add
const capitalize = (str: string) => {
  return str.charAt(0).toUpperCase() + str.slice(1);
};

// add-foo -> addFoo
const camelize = (str: string) => {
  return str.replace(/-(\w)/g, (_, c: string) => {
    return c ? c.toUpperCase() : "";
  });
};

这样在调取 emit 的时候, 会将事件名进行重写, 并匹配执行 props 中对应的事件

# Fragment 和 Text 节点

# 简介

Fragment 和 Text 节点是新的虚拟节点类型

Fragment 类型不会创建父节点, 只渲染子节点

Text 类型是文本节点

# 代码

在 vnode.ts 新增这两个节点

export const Fragment = Symbol("Fragment");
export const Text = Symbol("Text");

在 renderer.ts 中, 当 patch 节点的时候判断类型

import { Fragment, Text } from "./vnode";

function patch(vnode, container) {
  // 需要判断vnode是组件还是element
  const { type, shapeFlag } = vnode;

  switch (type) {
    // Fragment只渲染children
    case Fragment:
      processFragment(vnode, container);
      break;
    case Text:
      processText(vnode, container);
      break;
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(vnode, container);
      } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
        // 处理组件
        processComponent(vnode, container);
      }
      break;
  }
}

function processFragment(vnode, container) {
  // 直接渲染子节点
  mountChildren(vnode, container);
}

function processText(vnode, container) {
  const { children } = vnode;
  const textNode = (vnode.el = document.createTextNode(children));
  container.append(textNode);
}

# 组件 slots 功能

# 简介

测试代码如下:

App.js

import { renderSlots } from "../../lib/mini-vue.esm.js";

export const App = {
  name: "App",
  render() {
    const app = h("div", {}, "App");

    const foo = h(
      Foo,
      {},
      {
        // 作用域插槽, age在子组件中
        header: ({ age }) => h("p", {}, "header" + age),
        footer: () => h("p", {}, "footer"),
      }
    );

    return h("div", {}, [app, foo]);
  },

  setup() {
    return {};
  },
};

const Foo = {
  setup() {
    return {};
  },
  render() {
    const foo = h("p", {}, "foo");

    // 通过renderSlots把slots转换为vnode参与渲染
    const age = 18;
    return h("div", {}, [
      renderSlots(this.$slots, "header", { age }),
      foo,
      renderSlots(this.$slots, "footer"),
    ]);
  },
};

# 代码

首先在创建组件虚拟节点的时候, 预处理插槽, 修改 vnode.ts 和 ShapeFlags.ts

export function createVnode(type, props?, children?) {
  const vnode = {
    type,
    props,
    children,
    shapeFlag: getShapeFlag(type),
    el: null,
  };

  if (typeof children === "string") {
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
  } else if (Array.isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  }

  // 判断虚拟节点是否为组件
  if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    // 判断是否有 children 作为 slots
    if (typeof children === "object") {
      vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN;
    }
  }

  return vnode;
}
export const enum ShapeFlags {
  ELEMENT = 1, // 0001
  STATEFUL_COMPONENT = 1 << 1, // 0010
  TEXT_CHILDREN = 1 << 2, // 0100
  ARRAY_CHILDREN = 1 << 3, // 1000
  SLOT_CHILDREN = 1 << 4,
}

修改 component.ts 在 setup 的时候初始化 slots

export function setupComponent(instance) {
  initProps(instance, instance.vnode.props);

  // 组件虚拟节点的children就是组件的插槽
  initSlots(instance, instance.vnode.children);

  setupStatefulComponent(instance);
}

function initSlots(instance, children) {
  const { vnode } = instance;

  if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {
    normalizeObjectSlots(children, instance.slots);
  }
}

// 具名插槽
function normalizeObjectSlots(children, slots) {
  for (const key in children) {
    const value = children[key];

    // slot是一个函数, props是作用域插槽的参数
    // 返回一个虚拟节点数组
    slots[key] = (props) => normalizeSlotValue(value(props));
  }
}

// slots需要是数组, 才能在后续被createVnode转换为虚拟节点
function normalizeSlotValue(value) {
  return Array.isArray(value) ? value : [value];
}

新增 renderSlots.ts 用来渲染 slots

import { createVnode, Fragment } from "../vnode";

export function renderSlots(slots, name, props) {
  const slot = slots[name];

  if (slot) {
    if (typeof slot === "function") {
      // props是作用域插槽的参数
      return createVnode(Fragment, {}, slot(props));
    }
  }
}

# getCurrentInstance 功能

# 简介

创建一个 API, 用来获取当前正在执行 setup 的组件实例对象

# 代码

修改 component.ts

function setupStatefulComponent(instance) {
  const Component = instance.type;

  instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandles);

  const { setup } = Component;

  if (setup) {
    // 设置当前组件实例
    setCurrentInstance(instance);
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit,
    });

    setCurrentInstance(null);

    handleSetupResult(instance, setupResult);
  }
}

let currentInstance = null;

export function getCurrentInstance() {
  return currentInstance;
}

export function setCurrentInstance(instance) {
  currentInstance = instance;
}

# 组件 provides 和 inject 功能

# 简介

跨层级的组件存取数据

测试代码:

import { h, provide, inject } from "../../lib/mini-vue.esm.js";

export default {
  name: "App",
  setup() {},
  render() {
    return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]);
  },
};

const Provider = {
  name: "Provider",
  setup() {
    provide("foo", "fooVal");
    provide("bar", "barVal");
  },
  render() {
    return h("div", {}, [h("p", {}, "Provider"), h(ProviderTwo)]);
  },
};

const ProviderTwo = {
  name: "ProviderTwo",
  setup() {
    provide("foo", "fooTwo");
    const foo = inject("foo");

    return {
      foo,
    };
  },
  render() {
    return h("div", {}, [
      h("p", {}, `ProviderTwo foo:${this.foo}`),
      h(Consumer),
    ]);
  },
};

const Consumer = {
  name: "Consumer",
  setup() {
    const foo = inject("foo");
    const bar = inject("bar");
    const baz = inject("baz", () => "bazDefault");

    return {
      foo,
      bar,
      baz,
    };
  },

  render() {
    return h("div", {}, `Consumer: - ${this.foo} - ${this.bar} - ${this.baz}`);
  },
};

# 代码

修改 component.ts , 在创建组件实例的时候需要传入组件的父节点, 这样就能将 provides 层层传递下去

export function createComponentInstance(vnode, parent) {
  const component = {
    vnode,
    type: vnode.type,
    setupState: {},
    el: null,
    props: {},
    slots: {},
    provides: parent ? parent.provides : {},
    parent,
    emit: () => {},
  };

  // 锁定第一个参数是组件instance
  component.emit = emit.bind(null, component) as any;

  return component;
}

新增 apiInject.ts , 处理 provide 和 inject

import { getCurrentInstance } from "./component";

export function provide(key: string, value: any) {
  const currentInstance: any = getCurrentInstance();

  if (currentInstance) {
    // 获取当前组件的provides
    let { provides } = currentInstance;
    // 获取当前组件的父组件的provides
    const parentProvides = currentInstance.parent.provides;

    // 如果相同说明首次使用provide api, 进行初始化
    if (provides === parentProvides) {
      // 初始化当前组件的provides, 使用原型链保持对父组件的provides的关联
      provides = currentInstance.provides = Object.create(parentProvides);
    }

    provides[key] = value;
  }
}

export function inject(key, defaultValue) {
  const currentInstance: any = getCurrentInstance();

  if (currentInstance) {
    const parentProvides = currentInstance.parent.provides;

    if (key in parentProvides) {
      return parentProvides[key];
    } else {
      if (typeof defaultValue === "function") {
        return defaultValue();
      }
      return defaultValue;
    }
  }
}

provide 使用原型链的方式将属性一层一层传下去, 这样既能保障之间保持对父组件的 provides 的关联, 又不会出现多层之间属性被替换的问题

# 自定义渲染器

# 简介

在我们之前处理 element 的逻辑中, 都是默认渲染 DOM, 但是如果可以将渲染节点中的创建, 赋值, 挂载抽离出来, 便可以自定义一个渲染器, 用来渲染到其他平台.

# 代码

修改 renderer.ts , 不直接提供 render 函数, 而是将创建,赋值,插入等逻辑抽离成参数, 方便实现自定义渲染, 再向外提供.

import { createAppAPI } from "./createApp";

export function createRenderer(options) {
  const {
    createElement: hostCreateElement,
    patchProp: hostPatchProp,
    insert: hostInsert,
  } = options;

  function render(vnode, container) {
    patch(vnode, container, null);
  }

  ...

  function mountElement(vnode, container, parentComponent) {
    const el = (vnode.el = hostCreateElement(vnode.type));

    const { props } = vnode;

    for (const key in props) {
      const val = props[key];

      hostPatchProp(el, key, val);
    }

    const { children, shapeFlag } = vnode;

    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      el.textContent = children;
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(vnode, el, parentComponent);
    }

    hostInsert(el, container);
  }

  ...

  return {
    createApp: createAppAPI(render),
  };
}

原来的 render 函数不再向外提供, 那么在 createApp.ts 就需要将 render 做为参数引入

export function createAppAPI(render) {
  return function createApp(rootComponent) {
    return {
      mount(rootContainer) {
        // 将组建转化为虚拟节点
        // component -> vnode

        const vnode = createVnode(rootComponent);
        render(vnode, rootContainer);
      },
    };
  };
}

最后新增 runtime-dom/index.ts 将 DOM API 的代码放到这个地方, 作为默认的渲染器

import { createRenderer } from "../runtime-core";

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

function patchProp(el, key, val) {
  const isOn = (key: string) => /^on[A-Z]/.test(key);
  if (isOn(key)) {
    const event = key.slice(2).toLowerCase();
    el.addEventListener(event, val);
  } else {
    el.setAttribute(key, val);
  }
}

function insert(el, container) {
  container.append(el);
}

// 使用 createRenderer API 创建render
const renderer: any = createRenderer({
  createElement,
  patchProp,
  insert,
});

export function createApp(...args) {
  return renderer.createApp(...args);
}

export * from "../runtime-core";

# 总结

经过以上的改造后, 当我们想往常使用 createApp 的时候, 会先创建一个默认的 renderer , renderer 包含一个 createApp 方法会把当前的 render 传入并使用

如果我们需要实现自己的渲染器, 参考代码如下:

import { createRenderer } from "../../lib/mini-vue.esm.js";
import { App } from "./App.js";

const game = new PIXI.Application({
  width: 500,
  height: 500,
});

// 创建画布
document.body.append(game.view);

// 自定义渲染器
const renderer = createRenderer({
  createElement(type) {
    if (type === "rect") {
      const rect = new PIXI.Graphics();
      rect.beginFill(0xff0000);
      rect.drawRect(0, 0, 100, 100);
      rect.endFill();

      return rect;
    }
  },
  patchProp(el, key, val) {
    el[key] = val;
  },
  insert(el, parent) {
    parent.addChild(el);
  },
});

// 使用自定义渲染器创建App
renderer.createApp(App).mount(game.stage);
Last Updated: 3/29/2023, 2:39:52 PM