首页 » 神马SEO » 静态化勾选全局seo_vue3编译优化之静态提升

静态化勾选全局seo_vue3编译优化之静态提升

访客 2024-10-27 0

扫一扫用手机浏览

文章目录 [+]

<template> <div> <h1>title</h1> <p>{{ msg }}</p> <button @click="handleChange">change msg</button> </div></template><script setup lang="ts">import { ref } from "vue";const msg = ref("hello");function handleChange() { msg.value = "world";}</script>

这个demo代码很大略,个中的h1标签便是我们说的静态节点,p标签便是动态节点。
点击button按钮会将相应式msg变量的值更新,然后会实行render函数将msg变量的最新值"world"渲染到p标签中。

我们先来看看未开启静态提升之前天生的render函数是什么样的:

静态化勾选全局seo_vue3编译优化之静态提升 静态化勾选全局seo_vue3编译优化之静态提升 神马SEO

由于在vite项目中启动的vue都是开启了静态提升,以是我们须要在 Vue 3 Template Explorer网站中看看未开启静态提升的render函数的样子(网站URL为: template-explorer.vuejs.org/ ),如下图将hoistStatic这个选项取消勾选即可:

静态化勾选全局seo_vue3编译优化之静态提升 静态化勾选全局seo_vue3编译优化之静态提升 神马SEO
(图片来自网络侵删)

未开启静态提升天生的render函数如下:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("template", null, [ _createElementVNode("div", null, [ _createElementVNode("h1", null, "title"), _createElementVNode("p", null, _toDisplayString(_ctx.msg), 1 / TEXT /), _createElementVNode("button", { onClick: _ctx.handleChange }, "change msg", 8 / PROPS /, ["onClick"]) ]) ]))}

每次相应式变量更新后都会实行render函数,每次实行render函数都会实行createElementVNode方法天生h1标签的虚拟DOM。
但是我们这个h1标签明明便是一个静态节点,根本就不须要每次实行render函数都去天生一次h1标签的虚拟DOM。

vue3对此做出的优化便是将“实行createElementVNode方法天生h1标签虚拟DOM的代码”提取到render函数表面去,这样就只有初始化的时候才会去天生一次h1标签的虚拟DOM,也便是我们这篇文章中要讲的“静态提升”。
开启静态提升后天生的render函数如下:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"const _hoisted_1 = /#__PURE__/_createElementVNode("h1", null, "title", -1 / HOISTED /)export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("template", null, [ _createElementVNode("div", null, [ _hoisted_1, _createElementVNode("p", null, _toDisplayString(_ctx.msg), 1 / TEXT /), _createElementVNode("button", { onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleChange && _ctx.handleChange(...args))) }, "change msg") ]) ]))}

从上面可以看到天生h1标签虚拟DOM的createElementVNode函数被提取到render函数表面去实行了,只有初始化时才会实行一次将天生的虚拟DOM赋值给_hoisted_1变量。
在render函数中直策应用_hoisted_1变量即可,无需每次实行render函数都去天生h1标签的虚拟DOM,这便是我们这篇文章中要讲的“静态提升”。

我们接下来还是一样的套路通过debug的办法来带你搞清楚vue是如何实现静态提升的,注:本文利用的vue版本为3.4.19

欧阳平时写文章参考的多本vue源码电子书、解锁我更多vue事理文章

如何实现静态提升

实现静态提升紧张分为两个阶段:

transform阶段遍历AST抽象语法树,将静态节点找出来进行标记和处理,然后将这些静态节点塞到根节点的hoists数组中。
generate阶段遍历上一步在根节点存的hoists数组,在render函数外去天生存储静态节点虚拟DOM的_hoisted_x变量。
然后在render函数中利用这些_hoisted_x变量表示这些静态节点。
transform阶段

在我们这个场景中transform函数简化后的代码如下:

function transform(root, options) { // ...省略 if (options.hoistStatic) { hoistStatic(root, context); } root.hoists = context.hoists;}

从上面可以看到实现静态提升是实行了hoistStatic函数,我们给hoistStatic函数打个断点。
让代码走进去看看hoistStatic函数是什么样的,在我们这个场景中简化后的代码如下:

function hoistStatic(root, context) { walk(root, context, true);}

从上面可以看到这里依然不是详细实现的地方,接着将断点走进walk函数。
在我们这个场景中简化后的代码如下:

function walk(node, context, doNotHoistNode = false) { const { children } = node; for (let i = 0; i < children.length; i++) { const child = children[i]; if ( child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.ELEMENT ) { const constantType = doNotHoistNode ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context); if (constantType > ConstantTypes.NOT_CONSTANT) { if (constantType >= ConstantTypes.CAN_HOIST) { child.codegenNode.patchFlag = PatchFlags.HOISTED + ` / HOISTED /`; child.codegenNode = context.hoist(child.codegenNode); continue; } } } if (child.type === NodeTypes.ELEMENT) { walk(child, context); } }}

我们先在debug终端上面看看传入的第一个参数node是什么样的,如下图:

从上面可以看到此时的node为AST抽象语法树的根节点,树的构造和template中的代码刚好对上。
外层是div标签,div标签下面有h1、p、button三个标签。

我们接着来看walk函数,简化后的walk函数只剩下一个for循环遍历node.children。
在for循环里面紧张有两块if语句:

第一块if语句的浸染是实现静态提升第二块if语句的浸染是递归遍历整颗树。

我们来看第一块if语句中的条件,如下:

if ( child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.ELEMENT)

在将这块if语句之前,我们先来理解一下这里的两个列举。
NodeTypes和ElementTypes

NodeTypes列举

NodeTypes表示AST抽象语法树中的所有node节点类型,列举值如下:

enum NodeTypes { ROOT, // 根节点 ELEMENT, // 元素节点,比如:div元素节点、Child组件节点 TEXT, // 文本节点 COMMENT, // 注释节点 SIMPLE_EXPRESSION, // 大略表达式节点,比如v-if="msg !== 'hello'"中的msg!== 'hello' INTERPOLATION, // 双大括号节点,比如{{msg}} ATTRIBUTE, // 属性节点,比如 title="我是title" DIRECTIVE, // 指令节点,比如 v-if="" // ...省略}

看到这里有的小伙伴可能有疑问了,为什么AST抽象语法树中有这么多种节点类型呢?

我们来看一个例子你就明白了,如下:

<div v-if="msg !== 'hello'" title="我是title">msg为 {{ msg }}</div>

上面这段代码转换成AST抽象语法树后会天生很多node节点:

div对应的是ELEMENT元素节点v-if对应的是DIRECTIVE指令节点v-if中的msg !== 'hello'对应的是SIMPLE_EXPRESSION大略表达式节点title对应的是ATTRIBUTE属性节点msg为对应的是ELEMENT元素节点{{ msg }}对应的是INTERPOLATION双大括号节点ElementTypes列举

div元素节点、Child组件节点都是NodeTypes.ELEMENT元素节点,那么如何区分是不是组件节点呢?就须要利用ElementTypes列举来区分了,如下:

enum ElementTypes { ELEMENT, // html元素 COMPONENT, // 组件 SLOT, // 插槽 TEMPLATE, // 内置template元素}

现在来看第一块if条件,你该当很随意马虎看得懂了:

if ( child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.ELEMENT)

如果当前节点是html元素节点,那么就知足if条件。

当前的node节点是最外层的div节点,当然知足这个if条件。

接着将断点走进if条件内,第一行代码如下:

const constantType = doNotHoistNode ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context);

在搞清楚这行代码之前先来理解一下ConstantTypes列举

ConstantTypes列举

我们来看看ConstantTypes列举,如下:

enum ConstantTypes { NOT_CONSTANT = 0, // 不是常量 CAN_SKIP_PATCH, // 跳过patch函数 CAN_HOIST, // 可以静态提升 CAN_STRINGIFY, // 可以预字符串化}

ConstantTypes列举的浸染便是用来标记静态节点的4种等级状态,高档级的状态拥有低等级状态的所有能力。
比如: NOT_CONSTANT:表示当前节点不是静态节点。
比如下面这个p标签利用了msg相应式变量:

<p>{{ msg }}</p>const msg = ref("hello");

CAN_SKIP_PATCH:表示当前节点在重新实行render函数时可以跳过patch函数。
比如下面这个p标签虽然利用了变量name,但是name是一个常量值。
以是这个p标签实在是一个静态节点,但是由于利用了name变量,以是不能提升到render函数表面去。

<p>{{ name }}</p>const name = "name";

CAN_HOIST:表示当前静态节点可以被静态提升,当然每次实行render函数时也无需实行patch函数。
demo如下:

<h1>title</h1>

CAN_STRINGIFY:表示当前静态节点可以被预字符串化,下一篇文章会专门讲预字符串化。
从debug终端中可以看到此时doNotHoistNode变量的值为true,以是constantType变量的值为ConstantTypes.NOT_CONSTANT。
getConstantType函数的浸染是根据当前节点以及其子节点拿到静态节点的constantType。

我们接着来看后面的代码,如下:

if (constantType > ConstantTypes.NOT_CONSTANT) { if (constantType >= ConstantTypes.CAN_HOIST) { child.codegenNode.patchFlag = PatchFlags.HOISTED + ` / HOISTED /`; child.codegenNode = context.hoist(child.codegenNode); continue; }}

前面我们已经讲过了,当前div节点的constantType的值为ConstantTypes.NOT_CONSTANT,以是这个if语句条件不通过。

我们接着看walk函数中的末了一块代码,如下:

if (child.type === NodeTypes.ELEMENT) { walk(child, context);}

前面我们已经讲过了,当前child节点是div标签,以是当然知足这个if条件。
将子节点div作为参数,递归调用walk函数。

我们再次将断点走进walk函数,和上一次实行walk函数不同的是,上一次walk函数的参数为root根节点,这一次参数是div节点。

同样的在walk函数内先利用for循环遍历div节点的子节点,我们先来看第一个子节点h1标签,也便是须要静态提升的节点。
很明显h1标签是知足第一个if条件语句的:

if ( child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.ELEMENT)

在debug终端中来看看h1标签的constantType的值,如下:

从上图中可以看到h1标签的constantType值为3,也便是ConstantTypes.CAN_STRINGIFY。
表明h1标签是第一流级的预字符串,当然也能静态提升。

h1标签的constantType当然就能知足下面这个if条件:

if (constantType > ConstantTypes.NOT_CONSTANT) { if (constantType >= ConstantTypes.CAN_HOIST) { child.codegenNode.patchFlag = PatchFlags.HOISTED + ` / HOISTED /`; child.codegenNode = context.hoist(child.codegenNode); continue; }}

值得一提的是上面代码中的codegenNode属性便是用于天生对应node节点的render函数。

然后以codegenNode属性作为参数实行context.hoist函数,将其返回值赋值给节点的codegenNode属性。
如下:

child.codegenNode = context.hoist(child.codegenNode);

上面这行代码的浸染实在便是将原来天生render函数的codegenNode属性更换成用于静态提升的codegenNode属性。

context.hoist方法

将断点走进context.hoist方法,简化后的代码如下:

function hoist(exp) { context.hoists.push(exp); const identifier = createSimpleExpression( `_hoisted_${context.hoists.length}`, false, exp.loc, ConstantTypes.CAN_HOIST ); identifier.hoisted = exp; return identifier;}

我们先在debug终端看看传入的codegenNode属性。
如下图:

从上图中可以看到此时的codegenNode属性对应的便是h1标签,codegenNode.children对应的便是h1标签的title文本节点。
codegenNode属性的浸染便是用于天生h1标签的render函数。

在hoist函数中首先实行 context.hoists.push(exp)将h1标签的codegenNode属性push到context.hoists数组中。
context.hoists是一个数组,数组中存的是AST抽象语法树中所有须要被静态提升的所有node节点的codegenNode属性。

接着便是实行createSimpleExpression函数天生一个新的codegenNode属性,我们来看传入的第一个参数:

`_hoisted_${context.hoists.length}`

由于这里处理的是第一个须要静态提升的静态节点,以是第一个参数的值_hoisted_1。
如果处理的是第二个须要静态提升的静态节点,其值为_hoisted_2,依次类推。

接着将断点走进createSimpleExpression函数中,代码如下:

function createSimpleExpression( content, isStatic = false, loc = locStub, constType = ConstantTypes.NOT_CONSTANT) { return { type: NodeTypes.SIMPLE_EXPRESSION, loc, content, isStatic, constType: isStatic ? ConstantTypes.CAN_STRINGIFY : constType, };}

这个函数的浸染很大略,根据传入的内容天生一个大略表达式节点。
我们这里传入的内容便是_hoisted_1。

表达式节点我们前面讲过了,比如:v-if="msg !== 'hello'"中的msg!== 'hello'便是一个大略的表达式。

同理上面的_hoisted_1表示的是利用了一个变量名为_hoisted_1的表达式。

我们在debug终端上面看看hoist函数返回值,也便是h1标签新的codegenNode属性。
如下图:

此时的codegenNode属性已经变成了一个大略表达式节点,表达式的内容为:_hoisted_1。
后续实行generate天生render函数时,在render函数中h1标签就变成了表达式:_hoisted_1。

末了再实行transform函数中的root.hoists = context.hoists,将context高下文中存的hoists属性数组赋值给根节点的hoists属性数组,后面在generate天生render函数时会用。

至此transform阶段已经完成了,紧张做了两件事:

将h1静态节点找出来,将该节点天生render函数的codegenNode属性push到根节点的hoists属性数组中,后面generate天生render函数时会用。
将上一步h1静态节点的codegenNode属性更换为一个大略表达式,表达式为:_hoisted_1。
generate阶段

在generate阶段紧张分为两部分:

将原来render函数内调用createElementVNode天生h1标签虚拟DOM的代码,提到render函数表面去实行,赋值给全局变量_hoisted_1。
在render函数内直策应用_hoisted_1变量即可。

如下图:

天生render函数表面的_hoisted_1变量

经由transform阶段的处理,根节点的hoists属性数组中存了所有须要静态提升的静态节点。
我们先来看如何处理这些静态节点,天生h1标签对应的_hoisted_1变量的。
代码如下:

genHoists(ast.hoists, context);

将根节点的hoists属性数组传入给genHoists函数,将断点走进genHoists函数,在我们这个场景中简化后的代码如下:

function genHoists(hoists, context) { const { push, newline } = context; newline(); for (let i = 0; i < hoists.length; i++) { const exp = hoists[i]; if (exp) { push(`const _hoisted_${i + 1} = ${``}`); genNode(exp, context); newline(); } }}

generate部分的代码会在后面文章中逐行剖析,这篇文章就不细看到每个函数了。
大略阐明一下genHoists函数中利用到的那些方法的浸染。

context.code属性:此时的render函数字符串,可以在debug终端看一下实行每个函数后render函数字符串是什么样的。
newline方法:向当前的render函数字符串中插入换行符。
push方法:向当前的render函数字符串中插入字符串code。
genNode函数:在transform阶段给会每个node节点天生codegenNode属性,在genNode函数中会利用codegenNode属性天生对应node节点的render函数代码。

在刚刚进入genHoists函数,我们在debug终端利用context.code看看此时的render函数字符串是什么样的,如下图:

从上图中可以看到此时的render函数code字符串只有一行import vue的代码。

然后实行newline方法向render函数code字符串中插入一个换行符。

接着遍历在transform阶段网络的须要静态提升的节点凑集,也便是hoists数组。
在debug终端来看看这个hoists数组,如下图:

从上图中可以看到在hoists数组中只有一个h1标签须要静态提升。

在for循环中会先实行一句push方法,如下:

push(`const _hoisted_${i + 1} = ${``}`)

这行代码的意思是插入一个名为_hoisted_1的const变量,此时该变量的值还是空字符串。
在debug终端利用context.code看看实行push方法后的render函数字符串是什么样的,如下图:

从上图中可以看到_hoisted_1全局变量的定义已经天生了,值还没天生。

接着便是实行genNode(exp, context)函数天生_hoisted_1全局变量的值,同理在debug终端看看实行genNode函数后的render函数字符串是什么样的,如下图:

从上面可以看到render函数表面已经定义了一个_hoisted_1变量,变量的值为调用createElementVNode天生h1标签虚拟DOM。

天生render函数中return的内容

在generate中同样也是调用genNode函数天生render函数中return的内容,代码如下:

genNode(ast.codegenNode, context);

这里传入的参数ast.codegenNode是根节点的codegenNode属性,在genNode函数中会从根节点开始递归遍历整颗AST抽象语法树,为每个节点天生自己的createElementVNode函数,实行createElementVNode函数会天生这些节点的虚拟DOM。

我们先来看看传入的第一个参数ast.codegenNode,也便是根节点的codegenNode属性。
如下图:

从上图中可以看到静态节点h1标签已经变成了一个名为_hoisted_1的变量,而利用了msg变量的动态节点依然还是p标签。

我们再来看看实行这个genNode函数之前render函数字符串是什么样的,如下图:

从上图中可以看到此时的render函数字符串还没天生return中的内容。

实行genNode函数后,来看看此时的render函数字符串是什么样的,如下图:

从上图中可以看到,在天生的render函数中h1标签静态节点已经变成了_hoisted_1变量,_hoisted_1变量中存的是静态节点h1的虚拟DOM,以是每次页面更新重新实行render函数时就不会每次都去天生一遍静态节点h1的虚拟DOM。

总结

全体静态提升的流程图如下:

全体流程紧张分为两个阶段:

在transform阶段中:将h1静态节点找出来,将静态节点的codegenNode属性push到根节点的hoists属性数组中。
将h1静态节点的codegenNode属性更换为一个大略表达式节点,表达式为:_hoisted_1。
在generate阶段中:在render函数表面天生一个名为_hoisted_1的全局变量,这个变量中存的是h1标签的虚拟DOM。
在render函数内直策应用_hoisted_1变量就可以表示这个h1标签。
标签:

相关文章

IT专业发展趋势及未来职业规划

随着信息技术的飞速发展,IT行业已经成为当今世界最具活力和竞争力的产业之一。在这个充满变革的时代,选择IT专业无疑是一个明智的决定...

神马SEO 2024-12-27 阅读0 评论0

IT38帽子,时尚与科技的完美融合

在科技飞速发展的今天,时尚与科技的融合已成为一种趋势。IT38帽子作为一款兼具时尚与科技的产品,备受消费者喜爱。本文将从IT38帽...

神马SEO 2024-12-27 阅读0 评论0

IFIT漫威,跨界融合,共创无限可能

在当今这个科技飞速发展的时代,跨界融合已成为推动产业发展的重要动力。IFIT漫威,作为我国一家专注于健身领域的科技公司,与漫威漫画...

神马SEO 2024-12-27 阅读0 评论0

IT,理科的延伸与拓展

随着科技的飞速发展,信息技术(Information Technology,简称IT)逐渐成为我们生活中不可或缺的一部分。IT究竟...

神马SEO 2024-12-27 阅读0 评论0

CSS父子级,构建网页布局的艺术与方法

在网页设计中,CSS(层叠样式表)是不可或缺的工具之一。其中,父子级关系是CSS布局中的核心概念,它决定了元素在页面中的位置和层级...

神马SEO 2024-12-27 阅读0 评论0