vue3编译优化之“静态提升”

vue3 · 浏览次数 : 10

小编点评

**codegenNode属性** 是一个用于生成h1标签的render函数。在hoist函数中,codegenNode属性会被 push 到 context.hoists数组中。context.hoists是一个数组,存储所有需要被静态提升的所有node节点的codegenNode属性。 **createSimpleExpression 函数** 用于生成一个新的codegenNode属性。函数接收以下参数: * **content:**要生成的表达式的内容。 * **isStatic:**一个布尔值,指示是否处理静态表达式。 * **loc:**一个字符串,指示表达式的位置。 * **constType:**一个常量类型,指示表达式类型。 如果 **isStatic** 为 true,函数会返回一个 SimpleExpression 对象,其中包含以下属性: * **type:**NodeTypes.SIMPLE_EXPRESSION。 * **loc:**表达式的位置。 * **content:**表达式的内容。 * **isStatic:**布尔值为 true。 * **constType:** constType 属性设置为 ConstantTypes.CAN_STRINGIFY。 否则,函数会返回一个 null 值。 **genHoists 函数** 用于处理所有需要静态提升的静态节点。函数接收参数: * **hoists:**要处理的静态节点集合。 * **context:**一个上下文对象。 函数会遍历 **hoists** 数组,并为每个 node 创建一个codegenNode属性。如果 node 是一个表达式,则递归调用 genNode 函数生成其子节点。如果 node 是一个静态节点,则将 node 的 code 存储在 context.code 中。 **generate 函数** 用于生成render函数。函数接收参数: * **_hoists:**一个包含所有需要静态提升的静态节点的数组。 * **context:**一个上下文对象。 函数会遍历 **_hoists** 数组,并为每个 node 创建一个codegenNode属性。如果 node 是一个表达式,则递归调用 genNode 函数生成其子节点。如果 node 是一个静态节点,则将 node 的 code 存储在 context.code 中。最后,函数返回 render 函数的返回值。

正文

前言

在上一篇 vue3早已具备抛弃虚拟DOM的能力了文章中讲了对于动态节点,vue做的优化是将这些动态节点收集起来,然后当响应式变量修改后进行靶向更新。那么vue对静态节点有没有做什么优化呢?答案是:当然有,对于静态节点会进行“静态提升”。这篇文章我们来看看vue是如何进行静态提升的。

什么是静态提升?

我们先来看一个demo,代码如下:

<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函数是什么样的:

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

未开启静态提升生成的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

如何实现静态提升

实现静态提升主要分为两个阶段:

  • 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是什么样的,如下图:
root-code

从上面可以看到此时的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语句之前,我们先来了解一下这里的两个枚举。NodeTypesElementTypes

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的值,如下:
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属性。如下图:
before-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属性。如下图:
after-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变量即可。

如下图:
generate

生成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函数字符串是什么样的,如下图:
before-genHoists

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

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

接着遍历在transform阶段收集的需要静态提升的节点集合,也就是hoists数组。在debug终端来看看这个hoists数组,如下图:
hoists

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

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

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

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

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

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

从上面可以看到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属性。如下图:
ast-codegenNode

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

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

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

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

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

总结

整个静态提升的流程图如下:
full-progress

整个流程主要分为两个阶段:

  • transform阶段中:

    • 将h1静态节点找出来,将静态节点的codegenNode属性push到根节点的hoists属性数组中。

    • 将h1静态节点的codegenNode属性替换为一个简单表达式节点,表达式为:_hoisted_1

  • generate阶段中:

    • 在render函数外面生成一个名为_hoisted_1的全局变量,这个变量中存的是h1标签的虚拟DOM。

    • 在render函数内直接使用_hoisted_1变量就可以表示这个h1标签。

关注(图1)公众号:【前端欧阳】,解锁我更多vue原理文章。
加我(图2)微信回复「666」,免费领取欧阳研究vue源码过程中收集的源码资料,欧阳写文章有时也会参考这些资料。同时让你的朋友圈多一位对vue有深入理解的人。
公众号微信

与vue3编译优化之“静态提升”相似的内容:

vue3编译优化之“静态提升”

前言 在上一篇 vue3早已具备抛弃虚拟DOM的能力了文章中讲了对于动态节点,vue做的优化是将这些动态节点收集起来,然后当响应式变量修改后进行靶向更新。那么vue对静态节点有没有做什么优化呢?答案是:当然有,对于静态节点会进行“静态提升”。这篇文章我们来看看vue是如何进行静态提升的。 什么是静态

基于uniapp+vue3自定义增强版table表格组件「兼容H5+小程序+App端」

vue3+uniapp多端自定义table组件|uniapp加强版综合表格组件 uv3-table:一款基于uniapp+vue3跨端自定义手机端增强版表格组件。支持固定表头/列、边框、斑马纹、单选/多选,自定义表头/表体插槽、左右固定列阴影高亮显示。支持编译兼容H5+小程序端+App端。 如下图:

uniapp+vue3聊天室|uni-app+vite4+uv-ui跨端仿微信app聊天语音/朋友圈

原创研发uniapp+vue3+pinia2跨三端仿微信app聊天模板Uniapp-Wechat。 uni-vue3-wchat基于uni-app+vue3+pinia2+uni-ui+uv-ui等技术跨端仿制微信App界面聊天项目,支持编译到H5+小程序端+App端。实现编辑框多行消息/emoj混

浅析Vite本地构建原理

前言 随着Vue3的逐渐普及以及Vite的逐渐成熟,我们有必要来了解一下关于vite的本地构建原理。 对于webpack打包的核心流程是通过分析JS文件中引用关系,通过递归得到整个项目的依赖关系,并且对于非JS类型的资源,通过调用对应的loader将其打包编译生成JS 代码,最后再启动开发服务器。

面试官:在原生input上面使用v-model和组件上面使用有什么区别?

前言 还是上一篇面试官:来说说vue3是怎么处理内置的v-for、v-model等指令? 文章的那个粉丝,面试官接着问了他另外一个v-model的问题。 面试官:vue3的v-model都用过吧,来讲讲。 粉丝:v-model其实就是一个语法糖,在编译时v-model会被编译成:modelValue

基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理

在工作流页面中,除了特定的业务表单信息外,往往也需要同时展示通用申请单的相关信息,因此在页面设计的时候需要使用一些组件化的概念来实现动态的内容展示处理,本篇随笔介绍Vue3+TypeScript+ElementPus的前端工作流模块中实现统一的表单编辑和表单详情查看处理。

Vue3快速上手

好久没上传了,闲来无事把囤积已久的笔记给上传上传 1. Vue3简介 2020年9月18日,Vue.js发布版3.0版本,代号:One Piece(n 经历了:4800+次提交、40+个RFC、600+次PR、300+贡献者 官方发版地址:Release v3.0.0 One Piece · vue

【干货】Vue3 组件通信方式详解

毫无疑问,组件通信是Vue中非常重要的技术之一,它的出现能够使我们非常方便的在不同组件之间进行数据的传递,以达到数据交互的效果。所以,学习组件通信技术是非常有必要的,本文将总结Vue中关于组件通信的八种方式,帮助大家在使用Vue的过程中更加得心应手! 如果文中有不对、疑惑的地方,欢迎在评论区留言指正...

vue3.4的更新,保证你看的明明白白

defineModel 同学已经转正 defineModel 在vue3.3中还是一个实验性功能, 但是经过一个学期的努力,该同学已经转正。 defineModel的简单介绍 defineModel() 返回的值是一个 ref。 它可以像其他 ref 一样被访问以及修改。 它能起到在父组件和当前变量

vue3实现模拟地图上,站点名称按需显示的功能

很久很久没有更新博客了,因为实在是太忙了,每天都有公司的事情忙不完....... 最近在做车辆模拟地图,在实现控制站点名称按需显示时,折腾了好一段时间,特此记录一下。最终界面如下图所示: 站点显示需求:首末站必须显示,从第一个站开始,如果站点名称能显示下,则显示,如果站点名称会重叠则隐藏,以此类推。