大家好,我是梅利奥猪猪!你们的反内卷盟主(已经过了几百年没更新博客了)!这次我给大家分享的主题就是 vue3 的渲染流程(宏观上的理解),用最简单的大白话让大家理解!以及手摸手保姆级别的和 XDM 一起玩自定义渲染器,手写自定义渲染器的快乐你们会了就懂了!希望能帮到 XDM!那话不多说,我们直接开始!
准备工作
搭建项目并 run 起来
这个就不多说了,XDM 快速用 vite 搭建 vue3 的项目yarn create vite,然后选择vue 和 typescript,最后安装依赖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,那你们准备工作就完成啦!
渲染流程
先说结论,整个流程是这样的 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");
还有个Vue 3 Template Explorer我们也可以去玩下,我们可以复制我们的模板<div id="my-app" class="test-app">我是App</div>,结果如下
到这里,我们已经大概清楚,我们写的模版最后到底会变成什么样!不就变成了 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");
到这里我们宏观的可以理解,模版编译出的 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>
那到这一步,我们已经把虚拟 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>
很棒,我们的真实 dom 已经渲染出来了!
渲染流程简单总结
整个流程是: template(写模板) -> render(编译出 render 函数) -> vnode(render 函数执行生成虚拟 dom) -> element(转换为真实 dom 节点) -> mount(挂载到页面上)
自定义渲染器 - createRenderer()
xdm,不知道宏观的渲染流程你们了解了没有,学会了没有!然而渲染流程是我们的热身运动,我们重头戏主要还是玩自定义渲染器,先来看下vue 官方文档-自定义渲染器
简单来说,我们如果以后在非 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 渲染器,而是等等要自己手写个!
打印大法开启之简易玩耍代码
我们先来看下 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);
接着执行 createApp 看下结果
const renderer = createRenderer({} as RendererOptions);
// console.log("createRenderer", renderer);
const app = renderer.createApp(App);
console.log("createRenderer执行后createApp在执行的结果", app);
一鼓作气在执行 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
修复报错并打印函数参数
因看到报错信息,我们就可以看下文档中有没有提及 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);
顺便完善下 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);
最终打印结果如下
实现每个方法
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);
自定义渲染器总结
最终,我们根据文档的说明,以及了解各个函数是干嘛用的,成功定制了我们自己的 dom 渲染器,并把结果渲染到页面上!总结就是多写多试多练!
文末水文总结
通过今天的学习笔记,我们捋清了整个 vue3 都渲染流程(其实 vue2 也差不多),并且知道了如何自定义渲染器!XDM 加油,一起卷起来!如果有机会,下次在带 XDM 写其他的自定义渲染器,那这篇水文就到这了,谢谢大家观赏!