大白话指导 vue3 渲染流程及手摸手教自定义渲染器

397 阅读7分钟

大家好,我是梅利奥猪猪!你们的反内卷盟主(已经过了几百年没更新博客了)!这次我给大家分享的主题就是 vue3 的渲染流程(宏观上的理解),用最简单的大白话让大家理解!以及手摸手保姆级别的和 XDM 一起玩自定义渲染器,手写自定义渲染器的快乐你们会了就懂了!希望能帮到 XDM!那话不多说,我们直接开始!

准备工作

搭建项目并 run 起来

这个就不多说了,XDM 快速用 vite 搭建 vue3 的项目yarn create vite,然后选择vuetypescript,最后安装依赖yarn并启动项目yarn dev就 ok 了

改造 App.vue

其实就是写个最简单的组件,代码如下

<template>
  <div id="my-app" class="test-app">我是App</div>
</template>

<script setup lang="ts"></script>

<style scoped></style>

验证结果

写完后页面理所当然就是这个样子,完成这一步的 XD,那你们准备工作就完成啦!

1-准备工作.jpg

渲染流程

先说结论,整个流程是这样的 template -> render -> vnode -> element -> mount

template -> render

先问 XDM 个事,大家有没有打印过我们的组件?它编译出来到底是什么样子的,我们写的 template 实际上最终就会变成 render 函数,此话怎讲,我们先来看下打印

// main.ts
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";

console.log({ App });

createApp(App).mount("#app");

2-看模版编译后的内容.jpg

还有个Vue 3 Template Explorer我们也可以去玩下,我们可以复制我们的模板<div id="my-app" class="test-app">我是App</div>,结果如下

3-模版编译render.jpg

到这里,我们已经大概清楚,我们写的模版最后到底会变成什么样!不就变成了 render 函数嘛!

render -> vnode

那有了 render 函数,又如何生成 vnode 呢!既然 render 是个函数,那我们先尝试执行并打印下看看

import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";

console.log({ App });
console.log("render执行后的结果", App.render?.());

createApp(App).mount("#app");

4-render执行的结果.jpg

到这里我们宏观的可以理解,模版编译出的 render 函数,一执行就能获取到我们的虚拟 dom(就是个 js 对象),它其实就能描述我们真实 dom,接下去我们就简单的写下 vnode 怎么变成 element

vnode -> element

我们写个简易版的,方便说明怎么生成 element 就可以。比如在这个案例中,我们的虚拟 dom 就是长这样的

const vnode = {
  type: "div",
  props: { id: "my-app", class: "test-app" },
  children: "我是App",
};

直接开撸完整的代码

// 新开个页面 render.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>render</title>
  </head>

  <body>
    <script>
      const vnode = {
        type: "div",
        props: { id: "my-app", class: "test-app" },
        children: "我是App",
      };
      const element = document.createElement(vnode.type);
      for (const key in vnode.props) {
        element.setAttribute(key, vnode.props[key]);
      }
      element.textContent = vnode.children;
      console.log(element);
    </script>
  </body>
</html>

5-虚拟dom到真实dom.jpg

那到这一步,我们已经把虚拟 dom 转换成真实 dom 了,是不是很简单!那接下去当然是把它挂载到我们页面啦

element -> mount

直接一行代码就能搞定了document.body.appendChild(element);

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>render</title>
  </head>

  <body>
    <script>
      const vnode = {
        type: "div",
        props: { id: "my-app", class: "test-app" },
        children: "我是App",
      };
      const element = document.createElement(vnode.type);
      for (const key in vnode.props) {
        element.setAttribute(key, vnode.props[key]);
      }
      element.textContent = vnode.children;
      console.log(element);
      +++ document.body.appendChild(element);
    </script>
  </body>
</html>

6-挂在到页面.jpg

很棒,我们的真实 dom 已经渲染出来了!

渲染流程简单总结

整个流程是: template(写模板) -> render(编译出 render 函数) -> vnode(render 函数执行生成虚拟 dom) -> element(转换为真实 dom 节点) -> mount(挂载到页面上)

自定义渲染器 - createRenderer()

xdm,不知道宏观的渲染流程你们了解了没有,学会了没有!然而渲染流程是我们的热身运动,我们重头戏主要还是玩自定义渲染器,先来看下vue 官方文档-自定义渲染器

7-createRenderer说明.jpg

8-自定义渲染器wakuwaku.jpg

简单来说,我们如果以后在非 dom 环境中也想玩vue,就可以自己写个渲染器,只要用到其他环境(比如 canvas 啥的等等其他环境)的 api 去渲染就可以了,因为我们刚接触这个新的 api,我们这边还是先用这个 api 手写个 dom 环境的渲染器!

准备工作

因为要自己写渲染器,我们手动导入我们需要的 api-createRenderer,并且把原先的 craeteApp 那行代码干掉

import { createApp, createRenderer } from "vue";
import "./style.css";
import App from "./App.vue";

// console.log({ App });
// console.log("render执行后的结果", App.render?.());

// createApp(App).mount("#app");

/**
 * 接下去要用createRenderer来实现dom的渲染器
 */

把对应代码注释掉后,打开浏览器看到啥都没有就是正常的,因为我们没有用现成的 dom 渲染器,而是等等要自己手写个!

9-自定义渲染器准备工作.jpg

打印大法开启之简易玩耍代码

我们先来看下 createRenderer 函数执行后的返回值是什么样子的

import { createApp, createRenderer, RendererOptions } from "vue";
import "./style.css";
import App from "./App.vue";

// console.log({ App });
// console.log("render执行后的结果", App.render?.());

// createApp(App).mount("#app");

/**
 * 接下去要用createRenderer来实现dom的渲染器
 */
const renderer = createRenderer({} as RendererOptions);
console.log("createRenderer", renderer);

10-打印createRenderer函数返回值.jpg

接着执行 createApp 看下结果

const renderer = createRenderer({} as RendererOptions);
// console.log("createRenderer", renderer);
const app = renderer.createApp(App);
console.log("createRenderer执行后createApp在执行的结果", app);

11-createApp结果打印.jpg

一鼓作气在执行 mount,我们要自己实现 dom 的,所以注意啦,这里传入的参数是我们页面中获取的真实 domo,不能传入选择器

const renderer = createRenderer({} as RendererOptions);
// console.log("createRenderer", renderer);
const app = renderer.createApp(App);
// console.log("createRenderer执行后createApp在执行的结果", app);
app.mount(document.querySelector("#app") as RendererElement);

此时此刻果然有报错了,毕竟我们自定义渲染器啥都没写,怎么可能可以正常 work

12-报错信息.jpg

修复报错并打印函数参数

因看到报错信息,我们就可以看下文档中有没有提及 options 的字段有哪些,于是找到了 options 类型

function createRenderer<HostNode, HostElement>(
  options: RendererOptions<HostNode, HostElement>
): Renderer<HostElement>;

interface Renderer<HostElement> {
  render: RootRenderFunction<HostElement>;
  createApp: CreateAppFunction<HostElement>;
}

interface RendererOptions<HostNode, HostElement> {
  patchProp(
    el: HostElement,
    key: string,
    prevValue: any,
    nextValue: any,
    // the rest is unused for most custom renderers
    isSVG?: boolean,
    prevChildren?: VNode<HostNode, HostElement>[],
    parentComponent?: ComponentInternalInstance | null,
    parentSuspense?: SuspenseBoundary | null,
    unmountChildren?: UnmountChildrenFn
  ): void;
  insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void;
  remove(el: HostNode): void;
  createElement(
    type: string,
    isSVG?: boolean,
    isCustomizedBuiltIn?: string,
    vnodeProps?: (VNodeProps & { [key: string]: any }) | null
  ): HostElement;
  createText(text: string): HostNode;
  createComment(text: string): HostNode;
  setText(node: HostNode, text: string): void;
  setElementText(node: HostElement, text: string): void;
  parentNode(node: HostNode): HostElement | null;
  nextSibling(node: HostNode): HostNode | null;

  // optional, DOM-specific
  querySelector?(selector: string): HostElement | null;
  setScopeId?(el: HostElement, id: string): void;
  cloneNode?(node: HostNode): HostNode;
  insertStaticContent?(
    content: string,
    parent: HostElement,
    anchor: HostNode | null,
    isSVG: boolean
  ): [HostNode, HostNode];
}

我们可以根据报错,先配置createElement,因为实现个简易版,我们就只打印必填参数

const renderer = createRenderer({
  +++ createElement(type, isSVG?, isCustomizedBuiltIn?, vnodeProps?) {
    +++ console.log(type);
  +++ },
} as RendererOptions);
// console.log("createRenderer", renderer);
const app = renderer.createApp(App);
// console.log("createRenderer执行后createApp在执行的结果", app);
app.mount(document.querySelector("#app") as RendererElement);

13-报错信息2.jpg

顺便完善下 createElement 的逻辑

  createElement(type, isSVG?, isCustomizedBuiltIn?, vnodeProps?) {
    console.log("createElement", { type });
    const element = document.createElement(type);
    return element as unknown;
  },

然后我们以同样的方式处理,直到消灭所有报错,完整代码如下

import {
  createApp,
  createRenderer,
  RendererElement,
  RendererOptions,
} from "vue";
import "./style.css";
import App from "./App.vue";

// console.log({ App });
// console.log("render执行后的结果", App.render?.());

// createApp(App).mount("#app");

/**
 * 接下去要用createRenderer来实现dom的渲染器
 */
const renderer = createRenderer({
  createElement(type, isSVG?, isCustomizedBuiltIn?, vnodeProps?) {
    console.log("createElement", { type });
    const element = document.createElement(type);
    return element as unknown;
  },
  setElementText(node, text) {
    console.log("setElementText", { node, text });
  },
  patchProp(
    el,
    key,
    prevValue,
    nextValue,
    isSVG?,
    prevChildren?,
    parentComponent?,
    parentSuspense?,
    unmountChildren?
  ) {
    console.log("patchProp", { el, key, prevValue, nextValue });
  },
  insert(el, parent, anchor?) {
    console.log("insert", { el, parent });
  },
} as RendererOptions);
// console.log("createRenderer", renderer);
const app = renderer.createApp(App);
// console.log("createRenderer执行后createApp在执行的结果", app);
app.mount(document.querySelector("#app") as RendererElement);

最终打印结果如下

14-最终打印结果.jpg

实现每个方法

import {
  createApp,
  createRenderer,
  RendererElement,
  RendererOptions,
} from "vue";
import "./style.css";
import App from "./App.vue";

// console.log({ App });
// console.log("render执行后的结果", App.render?.());

// createApp(App).mount("#app");

/**
 * 接下去要用createRenderer来实现dom的渲染器
 */
const renderer = createRenderer({
  // 创建元素
  createElement(type, isSVG?, isCustomizedBuiltIn?, vnodeProps?) {
    console.log("createElement", { type });
    const element = document.createElement(type);
    return element as unknown;
  },
  // 元素节点的文本内容
  setElementText(node, text) {
    console.log("setElementText", { node, text });
    node.textContent = text;
  },
  // 处理props,该案例中就是id和class属性
  patchProp(
    el,
    key,
    prevValue,
    nextValue,
    isSVG?,
    prevChildren?,
    parentComponent?,
    parentSuspense?,
    unmountChildren?
  ) {
    console.log("patchProp", { el, key, prevValue, nextValue });
    el.setAttribute(key, nextValue);
  },
  // 最后把dom节点加入到页面中
  insert(el, parent, anchor?) {
    console.log("insert", { el, parent });
    parent.append(el);
  },
} as RendererOptions);
// console.log("createRenderer", renderer);
const app = renderer.createApp(App);
// console.log("createRenderer执行后createApp在执行的结果", app);
app.mount(document.querySelector("#app") as RendererElement);

15-最终效果.jpg

自定义渲染器总结

最终,我们根据文档的说明,以及了解各个函数是干嘛用的,成功定制了我们自己的 dom 渲染器,并把结果渲染到页面上!总结就是多写多试多练!

文末水文总结

通过今天的学习笔记,我们捋清了整个 vue3 都渲染流程(其实 vue2 也差不多),并且知道了如何自定义渲染器!XDM 加油,一起卷起来!如果有机会,下次在带 XDM 写其他的自定义渲染器,那这篇水文就到这了,谢谢大家观赏!

参考链接