👉 项目 Github 地址:github.com/XC0703/VueS…
(希望各位看官给本菜鸡的项目点个 star,不胜感激。)
16、模版的处理(三)
16-1 codegen 实现
16-1-1 前言
上文实现了 transform,现在就进入整个 compile 最后的代码生成 codegen 环节了,个人感觉和前面的 transform 比起来,codegen 真的非常简单,源码位置在这里:
我的实现和源码实现区别蛮大的,因为源码中考虑了各种 helper、cache、hoist 之类的,而这些我都没实现。经过 transform 之后,AST 上的 codegenNode 节点上挂载的 type 属性就是这个节点对应的代码结构,源码就是根据这个来进行代码生成的,具体可以看看源码,这部分还是比较明了的。
前面 transform 模块基本完全按照源码的结构来写,生成的 codegenNode 结构是和源码基本一致的,但是正因如此,在 codegen 环节不得不处理的非常非常非常生硬,希望各位见谅,理解个意思就行了。
由于不需要考虑各种复杂的结构,我这里就简单划分为元素、属性、文本、组合表达式,分别进行代码生成即可。而生成节点的函数也就很自然的想到了之前在 runtime 模块暴露出的 h 函数,源码中使用的是 createVNode,不过这两者区别不大,都能创建 VNode,下面这个是 h 函数接收的参数:
// weak-vue\packages\runtime-core\src\h.ts
// h函数的作用==>生成vnode(createVNode原理可以回去前面的内容看),核心之一==>处理参数
export function h(type, propsOrChildren, children) {}
16-1-2 基本逻辑的实现
codegen 的生成核心是借助一个 generate 函数,在这里面我们需要获取上下文,然后生成代码的初步结构,内容由 ``genNode 递归生成,最后当然也得返回生成的代码。
我们先去查看我们最后需要挂载在组件实例上的 render 函数是怎么样的结构,以前面的测试用例为例:
// 模版
// <div class="a" v-bind:b="c">parse {{ element }}</div>
// 它对应的render函数应该是下面这个结构:
function render() {
return h("div", { class: "a", b: "c" }, ["parse ", `${element}`]);
}
因此我们根据这个要生成的结果可以轻易知道如何生成 render 字符串的大体结构:
// weak-vue\packages\compiler-core\src\codegen.ts
// codegen 代码生成
export const generate = (ast) => {
// 获取上下文(包含生成代码所需的状态和工具函数)
const context = createCodegenContext();
// push用于添加代码到上下文中,indent和deindent用于增加或减少代码的缩进级别。
const { push, indent, deindent } = context;
indent();
push("with (ctx) {"); // with语句用于确保ctx中的属性和方法可以在代码块内部直接访问,用于后面的new Function生成代码(因此此时生成的是字符串,里面的h函数、渲染的值以及函数等都需要传入)
indent();
push("return function render(){return ");
if (ast.codegenNode) {
genNode(ast.codegenNode, context); // 递归生成代码
} else {
push("null");
}
deindent();
push("}}");
return {
ast,
code: context.code,
};
};
接下来是实现上面 用到的两个函数:createCodegenContext 与 genNode 。
// weak-vue\packages\compiler-core\src\codegen.ts
// 获取上下文
const createCodegenContext = () => {
const context = {
// state
code: "", // 目标代码
indentLevel: 0, // 缩进等级
// method
push(code) {
context.code += code;
},
indent() {
newline(++context.indentLevel);
},
deindent(witoutNewLine = false) {
if (witoutNewLine) {
--context.indentLevel;
} else {
newline(--context.indentLevel);
}
},
newline() {
newline(context.indentLevel);
},
};
function newline(n) {
context.push("\n" + " ".repeat(n));
}
return context;
};
genNode 里面简单的用 switch-case 进行一个流程控制调用不同的方法即可:
// weak-vue\packages\compiler-core\src\codegen.ts
// 生成代码
const genNode = (node, context) => {
// 如果是字符串就直接 push
if (typeof node === "string") {
context.push(node);
return;
}
switch (node.type) {
case NodeTypes.ELEMENT:
genElement(node, context);
break;
case NodeTypes.TEXT:
case NodeTypes.INTERPOLATION:
genTextData(node, context);
break;
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context);
break;
}
};
此时,codegen 的基本逻辑便实现了,下面的工作是实现各个节点类型对应具体 codegen 逻辑。
16-2-3 核心功能的实现
我们这步依次实现上面的 genElement 、genTextData、genCompoundExpression 三个方法。
16-2-3-1 genElement
开头说到,创建 VNode 使用 h 函数,也就是说我们需要解析出 tag、props、children 作为参数传入,这里把生成属性和子节点的逻辑抽离了出去,genElement 如下:
// weak-vue\packages\compiler-core\src\codegen.ts
// 生成元素节点
const genElement = (node, context) => {
const { push, deindent } = context;
const { tag, children, props } = node;
// tag
push(`h(${tag}, `);
// props
if (props) {
genProps(props.properties, context);
} else {
push("null, ");
}
// children
if (children) {
genChildren(children, context);
} else {
push("null");
}
deindent();
push(")");
};
其中,genProps 要做的就是获取节点中的属性数据,并拼接成一个对象的样子 push 进目标代码,这里看一下在上面 genElement 中调用 genProps 传入的 props.properties 是个什么东西:
// <p class="a" @click="fn()">hello {{ World }}</p>
[
{
type: "JS_PROPERTY",
key: {
type: "SIMPLE_EXPRESSION",
content: "class",
isStatic: true,
},
value: {
type: "SIMPLE_EXPRESSION",
content: {
type: "TEXT",
content: "a",
},
isStatic: true,
},
},
{
type: "JS_PROPERTY",
key: {
type: "SIMPLE_EXPRESSION",
content: "onClick",
isStatic: true,
isHandlerKey: true,
},
value: {
type: "SIMPLE_EXPRESSION",
content: "fn()",
isStatic: false,
},
},
];
那么我们就只需要按照这个结构来进行操作就可以了,如下:
// weak-vue\packages\compiler-core\src\codegen.ts
// 获取节点中的属性数据
const genProps = (props, context) => {
const { push } = context;
if (!props.length) {
push("{}");
return;
}
push("{ ");
for (let i = 0; i < props.length; i++) {
// 遍历每个 prop 对象,获取其中的 key 节点和 value 节点
const prop = props[i];
const key = prop ? prop.key : "";
const value = prop ? prop.value : prop;
if (key) {
// key
genPropKey(key, context);
// value
genPropValue(value, context);
} else {
// 如果 key 不存在就说明是一个 v-bind
const { content, isStatic } = value;
const contentStr = JSON.stringify(content);
push(`${contentStr}: ${isStatic ? contentStr : content}`);
}
if (i < props.length - 1) {
push(", ");
}
}
push(" }, ");
};
// 生成键
const genPropKey = (node, context) => {
const { push } = context;
const { isStatic, content } = node;
push(isStatic ? JSON.stringify(content) : content);
push(": ");
};
// 生成值
const genPropValue = (node, context) => {
const { push } = context;
const { isStatic, content } = node;
push(isStatic ? JSON.stringify(content.content) : JSON.stringify(content));
};
调用后的结果:
{ "class": "a", "onClick": "fn()" },
子节点是一个数组,只需要参考上面 genProps 的结构写就可以了,但是,由于我的 transformText 偷大懒没有生成 codegenNode,不得不单独进行处理,此外组合表达式 COMPOUND_EXPRESSION 也单独进行处理,其余正常递归 genNode 即可:
// weak-vue\packages\compiler-core\src\codegen.ts
// 生成子节点
const genChildren = (children, context) => {
const { push, indent } = context;
push("[");
indent();
// 单独处理 COMPOUND_EXPRESSION
if (children.type === NodeTypes.COMPOUND_EXPRESSION) {
genCompoundExpression(children, context);
}
// 单独处理 TEXT
else if (isObject(children) && children.type === NodeTypes.TEXT) {
genNode(children, context);
}
// 其余节点直接递归
else {
for (let i = 0; i < children.length; i++) {
const child = children[i];
genNode(child.codegenNode || child.children, context);
push(", ");
}
}
push("]");
};
16-2-3-2 genTextData
插值表达式和文本节点都会由这个函数处理,因为他们两者在代码生成的结果上来说,唯一的区别就是子节点是否是字符串。
// weak-vue\packages\compiler-core\src\codegen.ts
// 生成文本节点和插值表达式节点
const genTextData = (node, context) => {
const { push } = context;
const { type, content } = node;
// 如果是文本节点直接拿出 content
// 如果是插值表达式需要拿出 content.content
const textContent =
type === NodeTypes.TEXT
? JSON.stringify(content)
: NodeTypes.INTERPOLATION
? content.content
: "";
if (type === NodeTypes.TEXT) {
push(textContent);
}
if (type === NodeTypes.INTERPOLATION) {
push("`${");
push(`${textContent}`);
push("}`");
}
};
16-2-3-3 genCompoundExpression
组合表达式其实本质上就是一个节点,几个子节点可能是文本节点或者插值表达式节点,直接递归即可。
// weak-vue\packages\compiler-core\src\codegen.ts
// 生成复合表达式
const genCompoundExpression = (node, context) => {
const { push } = context;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (typeof child === "string") {
push(child);
} else {
genNode(child, context);
}
if (i !== node.children.length - 1) {
push(", ");
}
}
};
16-2-4 总结
16-2-4-1 Q&A 环节
Q:h 函数和 createVNode 的关系?
A:h 函数其实底层调用的就是 createVNode,属于是父子关系,而 h 函数中进行了一些容错处理之类的,比如你用 h 函数可以不传 props 直接传入 children,而这调 `createVNode 会报错。
Q:你这里的实现和源码的实现主要区别在哪?
A:处处都是区别,源码中的实现是完全以 codegenNode 的 type 属性作为指导来生成对应的结构,而节点的内容不是主要关注点,也就是说,我这里的实现是从功能为出发点,而源码是以结构为出发点,这就造成了一个很明显的区别,源码中根本没有什么 genChildren、genProps、genPropKey,源码中用的是 genObjectExpression、genArrayExpression、genNodeListAsArray 之类的,这样以结构为出发点抽离函数,就可以很大程度复用函数,操作起来也更为灵活,我写的这个确实是笨瓜代码。
Q:那你写的这个有什么用吗?
A:跑通了我自己写的测试,了解了源码运行的大体流程。
16-2-4-2 测试
我们上面实现了 generate 方法,此时在我们前面实现的 baseCompile 方法里面返回 generate 方法的调用结果即可。
// weak-vue\packages\compiler-core\src\compile.ts
// 完整编译过程:template -> ast -> codegen -> render
export const baseCompile = (template, options: any = {}) => {
// 第一步:将模板字符串转换成AST
const ast = isString(template) ? baseParse(template) : template;
// 第二步:AST加工
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
transform(
ast,
extend({}, options, {
nodeTransforms: [...nodeTransforms, ...(options.nodeTransforms || [])],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
),
})
);
// 第三步:将AST转换成渲染函数,最终得到一个render渲染函数
return generate(ast);
};
此时给我们新增的 codegen 方法添加对应的 ``log 调试,重新去 npm run build 跑我们上面的测试用例。
<div class="a" v-bind:b="c">parse {{ element }}</div>
我们重点查看下面几个打印效果:
最后生成的 generate(ast).code,即我们需要的 render 函数,如下:
// generate(ast).code:
'\n with (ctx) {\n return function render(){return h("div", { "class": "a", "b": "c" }, [\n "parse ", `${element}`]\n )\n }}';
// 格式化后:
with (ctx) {
return function render() {
return h("div", { class: "a", b: "c" }, ["parse ", `${element}`]);
};
}
基本符合预期,生成正确。
16-2 compile 实现
16-2-1 模块整合
其实关于 compile 模块的整合,我们基本已经完成了,即 weak-vue\packages\compiler-core\src\compile.ts 文件里面的 baseCompile 函数,因为我们秉持着测试驱动开发的原则,基本上是实现一个小功能就会进行相应的测试。
我们这里讲的整合指的是当前的 compile 模块和前面已经实现的 runtime 模块进行整合。
先找到我们前面实现的的 weak-vue\packages\runtime-core\src\component.ts 文件里面的 finishComponentSetup ,这个方法我们用于处理 render(把 render 挂载到实例上去):
// weak-vue\packages\runtime-core\src\component.ts
// 处理render(把render挂载到实例上去)
function finishComponentSetup(instance) {
// 判断组件中有没有render方法,没有则
const Component = instance.type; // createVNode时传入给type的是rootComponent,本质是一个对象,组件的所有属性都在这里,比如setup方法,比如render方法
if (!instance.render) {
// 这里的render指的是上面instance实例的render属性,在handlerSetupResult函数中会赋值(赋值的情况:组件有setup且返回函数),如果没有setup则此时会为false,则需要赋组件的render方法
if (!Component.render && Component.template) {
// TODO:模版编译
}
instance.render = Component.render;
}
// console.log(instance.render);
}
前面我们渲染的实现是手动给组件实例挂载一个 render 函数,其中 render 函数返回的就是前面实现的 render 函数。然后 instance.render = Component.render 这行代码才能生效,使得渲染流程不被阻塞。
<div id="app">111111111111</div>
<script src="../runtime-dom/dist/runtime-dom.global.js"></script>
<script>
let { createApp, h } = VueRuntimeDom;
let App = {
render() {
return h(
"div",
{ style: { color: "red" } },
h("div", {}, ["张三", h("p", {}, "李四")])
);
},
};
createApp(App, { name: "张三", age: 10 }).mount("#app");
</script>
而现在没有手动挂载 render 的环节了,应该要进到 if (!Component.render && Component.template)这里面的 TODO 逻辑处理。而此时我们的组件实例 Component.template 也是为空的,所以首先要把 template 字符串挂载到组件实例上。而 Component = instance.type; ,同时从创建组件实例的方法 createComponentInstance 的定义中可以清晰地看到 instance.type = vnode.type; 。所以此时要使得组件实例上挂载了 template 模版字符串,需要依次做两件事:
// weak-vue\packages\runtime-dom\src\index.ts
export const createApp = (rootComponent, rootProps) => {
// ...
app.mount = function (container) {
// 挂载组件之前要清空原来的内容,同时把模版字符串处理后(将标签前后的换行空格去除,压缩成一行,防止对AST生成造成影响)挂载到container上
container = nodeOps.querySelector(container);
// 第一件事:将模版字符串挂载到container上(把标签前后的空格换行去除,防止对codegen环节造成影响),因为后续会清空container.innerHTML
container.template = container.innerHTML
.replace(/\n\s*/g, "")
.replace(/\s+</g, "<")
.replace(/>\s+/g, ">");
container.innerHTML = "";
// 渲染新的内容(挂载dom)
mount(container);
};
return app;
};
// weak-vue\packages\runtime-core\src\apiCreateApp.ts
export function apiCreateApp(render) {
return function createApp(rootComponent, rootProps) {
let app = {
// ...
mount(container) {
let vnode = createVNode(rootComponent, rootProps);
// 第二件事:挂载模版到vnode上(container.innerHTML被清空之前,已先把模版字符串挂载到container上)
vnode.type.template = container.template;
// ...
},
};
return app;
};
}
此时去 finishComponentSetup 里面留下的 TODO 传入我们上面 generate 方法返回的渲染函数即可:
// weak-vue\packages\runtime-core\src\component.ts
if (!Component.render && Component.template) {
// 模版编译
let { template } = Component;
if (template[0] === "#") {
const el = document.querySelector(template);
template = el ? el.innerHTML : "";
}
const { code } = baseCompile(template);
// console.log("这是编译后的代码", code);
const fn = new Function("ctx", code);
const ctx = extend(
{ h: h },
instance.attrs,
instance.props,
instance.setupState
);
const render = fn(ctx); // 将字符串里面的h函数、渲染的值以及函数都变成需要的值,而不是字符串
Component.render = render;
}
注意,这里涉及了 new Function 的用法,可以看下面的代码进行了解:
// const obj = {}
// const h = () =>{
// console.log("Hello World!")
// }
// const code = "function render(h){return h()}"
// // 将code转化为函数
// const fn = new Function("return " + code)
// console.log("fn", fn)
// const render = fn()
// console.log("render", render)
// render(h) // 输出:Hello World!
// obj.render = render
// obj.render(h) // 输出:Hello World!
// const obj = {}
// const h = () =>{
// console.log("Hello World!")
// }
// const ctx = {h: h}
// const code = "function render(){with (ctx) {return h()}}"
// // 将code转化为函数
// const fn = new Function("return " + code)
// console.log("fn", fn)
// const render = fn()
// console.log("render", render)
// obj.render = render
// obj.render.call(ctx) // 输出:Hello World!
const obj = {};
const h = () => {
console.log("Hello World!");
};
const ctx = { h: h };
const code = "with (ctx) {return function render(){return h()}}";
// 将code转化为函数
const fn = new Function("ctx", code);
console.log("fn", fn);
const render = fn(ctx);
console.log("render", render);
obj.render = render;
render(); // 输出:Hello World!
obj.render(); // 输出:Hello World!
16-2-2 测试
新建一个测试用例,重新去 npm run build 打包:
<!-- weak-vue\packages\examples\10.compiler.html -->
<!-- 用模版来渲染 -->
<script src="../runtime-dom/dist/runtime-dom.global.js"></script>
<div id="template">
<div>
<p class="myText">Hello, World!This is my weak-vue!</p>
<div>counter: {{ counter.value }}</div>
<button @click="fn()">click</button>
</div>
</div>
<script>
let { createApp, ref } = VueRuntimeDom;
const counter = ref(0);
// 生成随机颜色的函数
const getRandomColor = () => {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
const fn = () => {
const myText = document.getElementsByClassName("myText")[0];
myText.style.color = getRandomColor();
counter.value++;
};
let App = {
setup() {
return {
counter,
};
},
};
createApp(App, {}).mount("#template");
</script>
上面的模板直接在组件中书写 h 函数来渲染则是下面的结构:
<!-- weak-vue\packages\examples\10.compiler.html -->
<div id="template"></div>
<script src="../runtime-dom/dist/runtime-dom.global.js"></script>
<script>
let { createApp, h, ref } = VueRuntimeDom;
const counter = ref(0);
// 生成随机颜色的函数
const getRandomColor = () => {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
const fn = () => {
const myText = document.getElementsByClassName("myText")[0];
myText.style.color = getRandomColor();
counter.value++;
};
let App = {
render() {
return h("div", null, [
h("p", { class: "myText" }, ["Hello, World!This is my weak-vue!"]),
h("div", null, ["counter: ", `${counter.value}`]),
h("button", { onclick: "fn()" }, ["click"]),
]);
},
};
createApp(App, {}).mount("#template");
</script>
无论是哪种渲染方式,最终的效果都是符合预期的:
注意:
笔者在测试该用例时,并不是一次性Bug Free的,而是发现了前面留下的一个又一个坑,然后依次进行填补,最终才通过测试。因此本章会涉及一些其他模块的代码改动,但是在文章中没有详细地指出来,具体可以看我这章对应的提交记录涉及的代码更改。(高情商:测试驱动开发;低情商:面向测试用例开发。 总之,如果各位没有通过该测试用例或者控制台报错,可以具体地对比我本章提交的代码与目前代码的不同之处。(调试代码、发现问题产生的原因也是程序员要学会的必备技能之一,发现 Bug 的过程中可以让我们梳理前面所写的逻辑,对原理进一步了解。)
自此,这个简易版的 Vue3 我们就基本完成了,这章的代码提交请看提交记录:16、模版的处理(三)。
17、总结
17-1 学习本项目的好处
虽然本项目是 vue3 的最简化模型,但是”麻雀虽小,肝胆俱全“,功能基本完善,代码量也足够,跟着做完也会受益颇丰:
- 进一步学习了解 vue 的核心原理和逻辑:比如订阅发布响应式系统的实现,diff 算法的具体逻辑,组件渲染的过程,模版编译的流程等;
- 了解到工业级代码库优秀的设计:比如 monorepo 的代码组织架构 ,shapeFlag、patchFlag 的设计方式,策略模式、工厂模式等设计模式在源码中的运用;
- 掌握”测试驱动开发“的思想,提高自己的代码编写及调试能力;
- 了解 monorepo、rollup 的配置,拓宽技术栈;
- 提高自己文档撰写和总结的能力。
17-2 文档阅读的重点
- monrepo 环境的搭建:1、Vue3 环境的搭建.md
- 响应式的订阅发布实现:3、依赖收集.md、4、触发更新.md
- 渲染的流程:9、组件的渲染(一)
- diff 算法:12、组件的更新(二).md
- 模版编译的流程:14、模版的处理(一).md
17-3 一些面试题
17-3-1 vue 双向数据绑定的原理?
单向比较好理解,比如一个变量在元素中呈现,这是一个方向,视图的元素绑定了某个函数可以改变该变量,这是另一个方向,如果此时变量改变后元素呈现的值也相应变了,此时便是双向了。vue 的双向数据绑定指的是视图和响应式数据之间的双向,主要基于 Observer、Watcher、Compile 三者实现。
Observer 负责数据劫持,主要基于 Proxy 实现,具体为 vue 初始化时,那些响应式数据都会被处理成一个 proxy 对象,当整个对象或者对象某一个属性被使用时触发 getter 方法,然后去收集 effect 依赖(effect 本质是响应式数据被改变时要去触发的函数,即视图的更新相关,即上面提到的 Watcher),即一个对象会有很多个属性,每个属性都有自己的 effect 依赖列表并接受各自的依赖管理器管理便于获取正确的 effect,最终实现嵌套的 map 结构。在模板进行初始化编译时,会找到其中动态绑定的数据,从 data 中获取并初始化视图,同时元素上会绑定相应的响应式数据更新函数,这个过程发生在 Compile 中。
此时便实现了上面提到的双向数据绑定的效果,当元素上绑定的数据更新函数触发时,会触发响应式数据的 proxy 对象的 setter 方法,并通过该对象的依赖管理器找到正确的 effect 函数去触发,从而去实现视图的更新。
17-3-2 diff 算法的总结?
- 首先 vue 组件的渲染借助了虚拟 dom 的原理,组件更新时,因为响应式的存在,会去重新生成一个 vnode。
- 此时,进入新老两个 vnode 比较阶段,首先会对比是否是相同元素,如果不是相同元素,则之间卸载老的节点,重新走 mountElement 方法去挂载渲染节点。
- 如果是相同元素,则会去对比 props 参数, props 不同有三种情况(依据实际情况进行对属性进行处理即可):
- 属性在新旧中都有,但是值不一样
- 在旧的组件中有,在新的中没有
- 在新的组件中有,在旧的中没有
- props 对比介绍之后,便会去对比 children 子节点,这也是 diff 算法中最复杂最核心的内容。children 子节点本质上是一个可以嵌套的数组,对于嵌套子节点的处理,直接走递归即可。对于同一层级下的新旧对比,又分为简单情况和复杂情况的处理。
- 简单情况是利用了双端 diff 算法,本质是利用双指针的原理,处理新旧子节点都是数组且头部或者尾部节点可复用的情况,其中新的子节点数量的时候要插入,旧的子节点数量多的时候要删除。处理这种简单情况时,我们只考虑了顺序的情况,要么都要新增,要么都要删除。
- 但实际有些节点是可以复用的只是顺序变了,有些要新增,有些则要删除。对于这种复杂情况的处理,vue 引入了映射表,即以新的乱序节点为基准创建一个映射表;再遍历旧的乱序节点过程中去该映射表查找是否已存在,如果有,说明是可以复用的,如果没有,说明是该旧节点需要删除的。
- 此时再用一个数组 newIndexToOldIndexMap 来表示新的节点在旧的节点数组中的索引+1,默认为 0 表示该节点不存在旧的节点序列中。
- 之后倒序遍历该数组,如果值为 0 表示要新增该节点,如果不为 0 则要移动节点到正确的位置。移动时默认是一个个移动的,产生了一些没有必要的性能损耗,比如某个区段的子节点顺序都没有变,可以不用操作这部分节点。
- 因此 vue 中引入了最长递增子序列的原理,在移动某个节点前,判断该节点是否在最长递增子序列中,在则不用移动跳过即可,继续操作前一个节点。
- Vue 3 的算法效率更高,因为它使用了一些优化技巧,例如按需更新、静态标记等,会跳过静态子树的比较减少比较次数。
17-3-3 谈一下 vue3 的 diff 算法发生了哪些变更?
随着 Vue 3.0 的发布,其 diff 算法相较于 Vue 2.x 版本有了很多变化,这里简单概括一下其中的主要变更:
1、patchFlag
在 Vue 3.0 中,通过 patchFlag 标记 VNode 的类型和操作(比如属性或子元素),以便后续更新时可以跳过一些无需更新的节点,从而显著提升更新效率。
2、静态提升
Vue 3.0 支持将静态节点在编译时优化,从而避免在后续更新时对它们进行比较,进一步提升更新效率。
3、缓存事件处理函数
Vue 3.0 缓存事件处理函数,避免在每次更新时都重新创建相同的函数对象,从而减少性能损耗。
4、HoistStatic
Vue 3.0 中引入了 HoistStatic 优化,即将静态节点和静态子树抽离出来,以便更好地利用浏览器的 DOM 节点复用机制。
5、Block tree
Vue 3.0 使用基于 Block tree 的优化策略,即将组件树抽象为若干个 Block,在每个 Block 内部进行 VNode 的比较和更新,进一步减小了更新的消耗。
17-3-4 vue3 性能优化?
vue3 主要从以下三个方面对性能作出了优化:
- 体积优化 :Vue 3.0 采用了 Tree-shaking 技术,使得打包后的包更小,同时也使得 Vue 3.0 在移动端的表现更好。任何一个函数,如 ref、reavtived、computed 等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小。
- 编译优化:Vue3 在模板编译阶段阶段时,会进行静态节点标记优化,即调用调用 optimize 方法遍历 template 字符串转化而成的抽象语法树 AST,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能。
- 数据劫持优化:vue2 中采用 defineProperty 来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加 getter 和 setter,实现响应式 vue3 采用 proxy 重写了响应式系统,因为 proxy 可以对整个对象进行监听,所以不需要深度遍历,进而提高了性能。
17-3-5 vue 编译到渲染的原理?
-
模板编译阶段:将模板编译成渲染函数(
render function)。在这个阶段,Vue会将类似于{{msg}}这样的指令和语法解析成对应的JavaScript代码,生成一个渲染函数(render function)。模板编译又分三个阶段,解析parse、加工优化transform、生成generate,最终生成可执行函数render。- 解析阶段:调用
parse`` 方法对 template字符串进行解析(原理是基于正则表达式),将标签、指令、属性等转化为抽象语法树AST。 - 加工优化阶段:调用
transform方法遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。 - 生成阶段:调用
generate方法将最终的AST转化为render 函数字符串render字符串并将静态部分放到staticRenderFns中,最后通过new Function(render``) 生成render函数。
- 解析阶段:调用
-
渲染阶段:创建虚拟
DOM树,并根据渲染函数生成VNode节点树。在这个阶段,Vue 根据模板中的指令和数据生成一个虚拟DOM树,然后将虚拟DOM树转换成VNode节点树。 -
更新阶段:将新的
VNode节点树渲染到真实的DOM上。在这个阶段,Vue会通过比较新旧VNode树来确定哪些节点需要被更新、哪些节点需要被新增或删除,并且将更新后的VNode节点树渲染到真实的DOM上。