面试官:来说说vue3是怎么处理内置的v-for、v-model等指令?

前言

最近有粉丝找到我,说被面试官给问懵了。

  • 粉丝:面试官上来就问“一个vue文件是如何渲染成浏览器上面的真实DOM?”,当时还挺窃喜这题真简单。就简单说了一下先是编译成render函数、然后根据render函数生成虚拟DOM,最后就是根据虚拟DOM生成真实DOM。按照正常套路面试官接着会问vue响应式原理和diff算法,结果面试官不讲武德问了我“那render函数又是怎么生成的呢?”。

  • 我:之前写过一篇 看不懂来打我,vue3如何将template编译成render函数 文章专门讲过这个吖。

  • 粉丝:我就是按照你文章回答的面试官,底层其实是调用的一个叫baseCompile的函数。在baseCompile函数中主要有三部分,执行baseParse函数将template模版转换成模版AST抽象语法树,接着执行transform函数处理掉vue内置的指令和语法糖就可以得到javascript AST抽象语法树,最后就是执行generate函数递归遍历javascript AST抽象语法树进行字符串拼接就可以生成render函数。当时在想这回算是稳了,结果跟着就翻车了。

  • 粉丝:面试官接着又让我讲“transform函数内具体是如何处理vue内置的v-for、v-model等指令?”,你的文章中没有具体讲过这个吖,我只有说不知道。面试官接着又问:generate函数是如何进行字符串拼接得到的render函数呢?,我还是回答的不知道。

  • 我:我的锅,接下来就先安排一篇文章来讲讲**transform函数内具体是如何处理vue内置的v-for、v-model等指令?**。

先来看个流程图

先来看一下我画的transform函数执行流程图,让你对整个流程有个大概的印象,后面的内容看着就不费劲了。如下图:
full-progress

从上面的流程图可以看到transform函数的执行过程主要分为下面这几步:

  • transform函数中调用createTransformContext函数生成上下文对象。在上下文对象中存储了当前正在转换的node节点的信息,后面的traverseNodetraverseChildrennodeTransforms数组中的转换函数、directiveTransforms对象中的转换函数都会依赖这个上下文对象。

  • 然后执行traverseNode函数,traverseNode函数是一个典型的洋葱模型。第一次执行traverseNode函数的时候会进入洋葱模型的第一层,先将nodeTransforms数组中的转换函数全部执行一遍,对第一层的node节点进行第一次转换,将转换函数返回的回调函数存到第一层的exitFns数组中。经过第一次转换后v-for等指令已经被初次处理了。

  • 然后执行traverseChildren函数,在traverseChildren函数中对当前node节点的子节点执行traverseNode函数。此时就会进入洋葱模型的第二层,和上一步一样会将nodeTransforms数组中的转换函数全部执行一遍,对第二层的node节点进行第一次转换,将转换函数返回的回调函数存到第二层的exitFns数组中。

  • 假如第二层的node节点已经没有了子节点,洋葱模型就会从“进入阶段”变成“出去阶段”。将第二层的exitFns数组中存的回调函数全部执行一遍,对node节点进行第二次转换,然后出去到第一层的洋葱模型。经过第二次转换后v-for等指令已经被完全处理了。

  • 同样将第一层中的exitFns数组中存的回调函数全部执行一遍,由于此时第二层的node节点已经全部处理完了,所以在exitFns数组中存的回调函数中就可以根据子节点的情况来处理父节点。

  • 执行nodeTransforms数组中的transformElement转换函数,会返回一个回调函数。在回调函数中会调用buildProps函数,在buildProps函数中只有当node节点中有对应的指令才会执行directiveTransforms对象中对应的转换函数。比如当前node节点有v-model指令,才会去执行transformModel转换函数。v-model等指令也就被处理了。

关注公众号:前端欧阳,解锁我更多vue干货文章。还可以加我微信,私信我想看哪些vue原理文章,我会根据大家的反馈进行创作。

举个例子

还是同样的套路,我们通过debug一个简单的demo来带你搞清楚transform函数内具体是如何处理vue内置的v-for、v-model等指令。demo代码如下:

<template>
  <div>
    <input v-for="item in msgList" :key="item.id" v-model="item.value" />
    <p>标题是:{{ title }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const msgList = ref([
  {
    id: 1,
    value: "",
  },
  {
    id: 2,
    value: "",
  },
  {
    id: 3,
    value: "",
  },
]);
const title = ref("hello word");
</script>

在上面的代码中,我们给input标签使用了v-for和v-model指令,还渲染了一个p标签。p标签中的内容由foo变量、bar字符串、baz变量拼接而来的。

我们在上一篇 看不懂来打我,vue3如何将template编译成render函数 文章中已经讲过了,将template模版编译成模版AST抽象语法树的过程中不会处理v-for、v-model等内置指令,而是将其当做普通的props属性处理。

比如我们这个demo,编译成模版AST抽象语法树后。input标签对应的node节点中就增加了三个props属性,name分别为for、bind、model,分别对应的是v-for、v-bind、v-model。真正处理这些vue内置指令是在transform函数中。

transform函数

本文中使用的vue版本为3.4.19transform函数在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。找到transform函数的代码,打上断点。

从上一篇文章我们知道了transform函数是在node端执行的,所以我们需要启动一个debug终端,才可以在node端打断点。这里以vscode举例,首先我们需要打开终端,然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。
debug-terminal

接着在debug终端中执行yarn dev(这里是以vite举例)。在浏览器中访问 http://localhost:5173/,此时断点就会走到transform函数中了。我们在debug终端中来看看调用transform函数时传入的root变量,如下图:
before-transform

从上图中我们可以看到transform函数接收的第一个参数root变量是一个模版AST抽象语法树,为什么说他是模版AST抽象语法树呢?因为这棵树的结构和template模块中的结构一模一样,root变量也就是模版AST抽象语法树是对template模块进行描述。

根节点的children下面只有一个div子节点,对应的就是最外层的div标签。div节点children下面有两个子节点,分别对应的是input标签和p标签。input标签中有三个props,分别对应input标签上面的v-for指令、key属性、v-model指令。从这里我们可以看出来此时vue内置的指令还没被处理,在执行parse函数生成模版AST抽象语法树阶段只是将其当做普通的属性处理后,再塞到props属性中。

p标签中的内容由两部分组成:<p>标题是:{{ title }}</p>。此时我们发现p标签的children也是有两个,分别是写死的文本和title变量。

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

function transform(root, options) {
  const context = createTransformContext(root, options);
  traverseNode(root, context);
}

从上面的代码中可以看到transform函数内主要有两部分,从名字我想你应该就能猜出他们的作用。传入模版AST抽象语法树options,调用createTransformContext函数生成context上下文对象。传入模版AST抽象语法树context上下文对象,调用traverseNode函数对树中的node节点进行转换。

createTransformContext函数

在讲createTransformContext函数之前我们先来了解一下什么是context(上下文)

什么是上下文

上下文其实就是在某个范围内的“全局变量”,在这个范围内的任意地方都可以拿到这个“全局变量”。举两个例子:

在vue中可以通过provied向整颗组件树提供数据,然后在树的任意节点可以通过inject拿到提供的数据。比如:

根组件App.vue,注入上下文。

const count = ref(0)
provide('count', count)

业务组件list.vue,读取上下文。

const count = inject('count')

在react中,我们可以使用React.createContext 函数创建一个上下文对象,然后注入到组件树中。

const ThemeContext = React.createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  // ...
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

在这颗组件树的任意层级中都能拿到上下文对象中提供的数据:

const theme = useContext(ThemeContext);

树中的节点一般可以通过children拿到子节点,但是父节点一般不容易通过子节点拿到。在转换的过程中我们有的时候需要拿到父节点进行一些操作,比如将当前节点替换为一个新的节点,又或者直接删掉当前节点。

所以在这里会维护一个context上下文对象,对象中会维护一些状态和方法。比如当前正在转换的节点是哪个,当前转换的节点的父节点是哪个,当前节点在父节点中是第几个子节点,还有replaceNoderemoveNode等方法。

上下文中的一些属性和方法

我们将断点走进createTransformContext函数中,简化后的代码如下:

function createTransformContext(
  root,
  {
    nodeTransforms = [],
    directiveTransforms = {},
    // ...省略
  }
) {
  const context = {
    // 所有的node节点都会将nodeTransforms数组中的所有的转换函数全部执行一遍
    nodeTransforms,
    // 只执行node节点的指令在directiveTransforms对象中对应的转换函数
    directiveTransforms,
    // 需要转换的AST抽象语法树
    root,
    // 转换过程中组件内注册的组件
    components: new Set(),
    // 转换过程中组件内注册的指令
    directives: new Set(),
    // 当前正在转换节点的父节点,默认转换的是根节点。根节点没有父节点,所以为null。
    parent: null,
    // 当前正在转换的节点,默认为根节点
    currentNode: root,
    // 当前转换节点在父节点中的index位置
    childIndex: 0,
    replaceNode(node) {
      // 将当前节点替换为新节点
    },
    removeNode(node) {
      // 删除当前节点
    },
    // ...省略
  };
  return context;
}

从上面的代码中可以看到createTransformContext中的代码其实很简单,第一个参数为需要转换的模版AST抽象语法树,第二个参数对传入的options进行解构,拿到options.nodeTransforms数组和options.directiveTransforms对象。

nodeTransforms数组中存了一堆转换函数,在树的递归遍历过程中会将nodeTransforms数组中的转换函数全部执行一遍。directiveTransforms对象中也存了一堆转换函数,和nodeTransforms数组的区别是,只会执行node节点的指令在directiveTransforms对象中对应的转换函数。比如node节点中只有v-model指令,那就只会执行directiveTransforms对象中的transformModel转换函数。这里将拿到的nodeTransforms数组和directiveTransforms对象都存到了context上下文中。

context上下文中存了一些状态属性:

  • root:需要转换的AST抽象语法树。

  • components:转换过程中组件内注册的组件。

  • directives:转换过程中组件内注册的指令。

  • parent:当前正在转换节点的父节点,默认转换的是根节点。根节点没有父节点,所以为null。

  • currentNode:当前正在转换的节点,默认为根节点。

  • childIndex:当前转换节点在父节点中的index位置。

context上下文中存了一些方法:

  • replaceNode:将当前节点替换为新节点。

  • removeNode:删除当前节点。

traverseNode函数

接着将断点走进traverseNode函数中,在我们这个场景中简化后的代码如下:

function traverseNode(node, context) {
  context.currentNode = node;
  const { nodeTransforms } = context;
  const exitFns = [];
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context);
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit);
      } else {
        exitFns.push(onExit);
      }
    }
    if (!context.currentNode) {
      return;
    } else {
      node = context.currentNode;
    }
  }

  traverseChildren(node, context);

  context.currentNode = node;
  let i = exitFns.length;
  while (i--) {
    exitFns[i]();
  }
}

从上面的代码中我们可以看到traverseNode函数接收两个参数,第一个参数为当前需要处理的node节点,第一次调用时传的就是树的根节点。第二个参数是上下文对象。

我们再来看traverseNode函数的内容,内容主要分为三部分。分别是:

  • nodeTransforms数组内的转换函数全部执行一遍,如果转换函数的执行结果是一个回调函数,那么就将回调函数push到exitFns数组中。

  • 调用traverseChildren函数处理子节点。

  • exitFns数组中存的回调函数依次从末尾取出来挨个执行。

traverseChildren函数

我们先来看看第二部分的traverseChildren函数,代码很简单,简化后的代码如下:

function traverseChildren(parent, context) {
  let i = 0;
  for (; i < parent.children.length; i++) {
    const child = parent.children[i];
    context.parent = parent;
    context.childIndex = i;
    traverseNode(child, context);
  }
}

traverseChildren函数中会去遍历当前节点的子节点,在遍历过程中会将context.parent更新为当前的节点,并且将context.childIndex也更新为当前子节点所在的位置。然后再调用traverseNode函数处理当前的子节点。

所以在traverseNode函数执行的过程中,context.parent总是指向当前节点的父节点,context.childIndex总是指向当前节点在父节点中的index位置。如下图:

traverseChildren

进入时执行的转换函数

我们现在回过头来看第一部分的代码,代码如下:

function traverseNode(node, context) {
  context.currentNode = node;
  const { nodeTransforms } = context;
  const exitFns = [];
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context);
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit);
      } else {
        exitFns.push(onExit);
      }
    }
    if (!context.currentNode) {
      return;
    } else {
      node = context.currentNode;
    }
  }
  // ...省略
}

首先会将context.currentNode更新为当前节点,然后从context上下文中拿到由转换函数组成的nodeTransforms数组。

在 看不懂来打我,vue3如何将template编译成render函数 文章中我们已经讲过了nodeTransforms数组中主要存了下面这些转换函数,代码如下:

const nodeTransforms = [
  transformOnce,
  transformIf,
  transformMemo,
  transformFor,
  transformFilter,
  trackVForSlotScopes,
  transformExpression
  transformSlotOutlet,
  transformElement,
  trackSlotScopes,
  transformText
]

很明显我们这里的v-for指令就会被nodeTransforms数组中的transformFor转换函数处理。

看到这里有的小伙伴就会问了,怎么没有在nodeTransforms数组中看到处理v-model指令的转换函数呢?处理v-model指令的转换函数是在directiveTransforms对象中。在directiveTransforms对象中主要存了下面这些转换函数:

const directiveTransforms = {
  bind: transformBind,
  cloak: compilerCore.noopDirectiveTransform,
  html: transformVHtml,
  text: transformVText,
  model: transformModel,
  on: transformOn,
  show: transformShow
}

nodeTransformsdirectiveTransforms的区别是,在递归遍历转换node节点时,每次都会将nodeTransforms数组中的所有转换函数都全部执行一遍。比如当前转换的node节点中没有使用v-if指令,但是在转换当前node节点时还是会执行nodeTransforms数组中的transformIf转换函数。

directiveTransforms是在递归遍历转换node节点时,只会执行node节点中存在的指令对应的转换函数。比如当前转换的node节点中有使用v-model指令,所以就会执行directiveTransforms对象中的transformModel转换函数。由于node节点中没有使用v-html指令,所以就不会执行directiveTransforms对象中的transformVHtml转换函数。

我们前面讲过了context上下文中存了很多属性和方法。包括当前节点的父节点是谁,当前节点在父节点中的index位置,替换当前节点的方法,删除当前节点的方法。这样在转换函数中就可以通过context上下文对当前节点进行各种操作了。

将转换函数的返回值赋值给onExit变量,如果onExit不为空,说明转换函数的返回值是一个回调函数或者由回调函数组成的数组。将这些回调函数push进exitFns数组中,在退出时会将这些回调函数倒序全部执行一遍。

执行完回调函数后会判断上下文中的currentNode是否为空,如果为空那么就return掉整个traverseNode函数,后面的traverseChildren等函数都不会执行了。如果context.currentNode不为空,那么就将本地的node变量更新成context上下文中的currentNode

为什么需要判断context上下文中的currentNode呢?原因是经过转换函数的处理后当前节点可能会被删除了,也有可能会被替换成一个新的节点,所以在每次执行完转换函数后都会更新本地的node变量,保证在下一个的转换函数执行时传入的是最新的node节点。

退出时执行的转换函数回调

我们接着来看traverseNode函数中最后一部分,代码如下:

function traverseNode(node, context) {
  // ...省略
  context.currentNode = node;
  let i = exitFns.length;
  while (i--) {
    exitFns[i]();
  }
}

由于这段代码是在执行完traverseChildren函数再执行的,前面已经讲过了在traverseChildren函数中会将当前节点的子节点全部都处理了,所以当代码执行到这里时所有的子节点都已经处理完了。所以在转换函数返回的回调函数中我们可以根据当前节点转换后的子节点情况来决定如何处理当前节点。

在处理子节点的时候我们会将context.currentNode更新为子节点,所以在处理完子节点后需要将context.currentNode更新为当前节点。这样在执行转换函数返回的回调函数时,context.currentNode始终就是指向的是当前的node节点。

请注意这里是倒序取出exitFns数组中存的回调函数,在进入时会按照顺序去执行nodeTransforms数组中的转换函数。在退出时会倒序去执行存下来的回调函数,比如在nodeTransforms数组中transformIf函数排在transformFor函数前面。transformIf用于处理v-if指令,transformFor用于处理v-for指令。在进入时transformIf函数会比transformFor函数先执行,所以在组件上面同时使用v-if和v-for指令,会是v-if指令先生效。在退出阶段时transformIf函数会比transformFor函数后执行,所以在transformIf回调函数中可以根据transformFor回调函数的执行结果来决定如何处理当前的node节点。

traverseNode函数其实就是典型的洋葱模型,依次从父组件到子组件挨着调用nodeTransforms数组中所有的转换函数,然后从子组件到父组件倒序执行nodeTransforms数组中所有的转换函数返回的回调函数。traverseNode函数内的设计很高明,如果你还没反应过来,别着急我接下来会讲他高明在哪里。

洋葱模型traverseNode函数

我们先来看看什么是洋葱模型,如下图:
onion

洋葱模型就是:从外面一层层的进去,再一层层的从里面出来。

第一次进入traverseNode函数的时候会进入洋葱模型的第1层,先依次将nodeTransforms数组中所有的转换函数全部执行一遍,对当前的node节点进行第一次转换。如果转换函数的返回值是回调函数或者回调函数组成的数组,那就将这些回调函数依次push到第1层定义的exitFns数组中。

然后再去处理当前节点的子节点,处理子节点的traverseChildren函数其实也是在调用traverseNode函数,此时已经进入了洋葱模型的第2层。同理在第2层也会将nodeTransforms数组中所有的转换函数全部执行一遍,对第2层的node节点进行第一次转换,并且将返回的回调函数依次push到第2层定义的exitFns数组中。

同样的如果第2层节点也有子节点,那么就会进入洋葱模型的第3层。在第3层也会将nodeTransforms数组中所有的转换函数全部执行一遍,对第3层的node节点进行第一次转换,并且将返回的回调函数依次push到第3层定义的exitFns数组中。

请注意此时的第3层已经没有子节点了,那么现在就要从一层层的进去,变成一层层的出去。首先会将第3层exitFns数组中存的回调函数依次从末尾开始全部执行一遍,会对第3层的node节点进行第二次转换,此时第3层中的node节点已经被全部转换完了。

由于第3层的node节点已经被全部转换完了,所以会出去到洋葱模型的第2层。同样将第2层exitFns数组中存的回调函数依次从末尾开始全部执行一遍,会对第2层的node节点进行第二次转换。值得一提的是由于第3层的node节点也就是第2层的children节点已经被完全转换了,所以在执行第2层转换函数返回的回调函数时就可以根据子节点的情况来处理父节点。

同理将第2层的node节点全部转换完了后,会出去到洋葱模型的第1层。将第1层exitFns数组中存的回调函数依次从末尾开始全部执行一遍,会对第1层的node节点进行第二次转换。

当出去阶段的第1层全部处理完后了,transform函数内处理内置的v-for等指令也就处理完了。执行完transform函数后,描述template解构的模版AST抽象语法树也被处理成了描述render函数结构的javascript AST抽象语法树。后续只需要执行generate函数,进行普通的字符串拼接就可以得到render函数。

继续debug

搞清楚了traverseNode函数,接着来debug看看demo中的v-for指令和v-model指令是如何被处理的。

  • v-for指令对应的是transformFor转换函数。

  • v-model指令对应的是transformModel转换函数。

transformFor转换函数

通过前面我们知道了用于处理v-for指令的transformFor转换函数是在nodeTransforms数组中,每次处理node节点都会执行。我们给transformFor转换函数打3个断点,分别是:

  • 进入transformFor转换函数之前。

  • 调用transformFor转换函数,第1次对node节点进行转换之后。

  • 调用transformFor转换函数返回的回调函数,第2次对node节点进行转换之后。

我们将代码走到第1个断点,看看执行transformFor转换函数之前input标签的node节点是什么样的,如下图:
transformFor1

从上图中可以看到input标签的node节点中还是有一个v-for的props属性,说明此时v-for指令还没被处理。

我们接着将代码走到第2个断点,看看调用transformFor转换函数第1次对node节点进行转换之后是什么样的,如下图:
transformFor2

从上图中可以看到原本的input的node节点已经被替换成了一个新的node节点,新的node节点的children才是原来的node节点。并且input节点props属性中的v-for指令也被消费了。新节点的source.content里存的是v-for="item in msgList"中的msgList变量。新节点的valueAlias.content里存的是v-for="item in msgList"中的item。请注意此时arguments数组中只有一个字段,存的是msgList变量。

我们接着将代码走到第3个断点,看看调用transformFor转换函数返回的回调函数,第2次对node节点进行转换之后是什么样的,如下图:
transformFor3

从上图可以看到arguments数组中多了一个字段,input标签现在是当前节点的子节点。按照我们前面讲的洋葱模型,input子节点现在已经被转换完成了。所以多的这个字段就是input标签经过transform函数转换后的node节点,将转换后的input子节点存到父节点上面,后面生成render函数时会用。

transformModel转换函数

通过前面我们知道了用于处理v-model指令的transformModel转换函数是在directiveTransforms对象中,只有当node节点中有对应的指令才会执行对应的转换函数。我们这里input上面有v-model指令,所以就会执行transformModel转换函数。

我们在前面的 看不懂来打我,vue3如何将template编译成render函数 文章中已经讲过了处理v-model指令是调用的@vue/compiler-dom包的transformModel函数,很容易就可以找到@vue/compiler-dom包的transformModel函数,然后打一个断点,让断点走进transformModel函数中,如下图:
transformModel

从上面的图中我们可以看到在@vue/compiler-dom包的transformModel函数中会调用@vue/compiler-core包的transformModel函数,拿到返回的baseResult对象后再一些其他操作后直接return baseResult

从左边的call stack调用栈中我们可以看到transformModel函数是由一个buildProps函数调用的,buildProps函数是由postTransformElement函数调用的。而postTransformElement函数则是transformElement转换函数返回的回调函数,transformElement转换函数是在nodeTransforms数组中。

所以directiveTransforms对象中的转换函数调用其实是由nodeTransforms数组中的transformElement转换函数调用的。如下图:
directiveTransforms

看名字你应该猜到了buildProps函数的作用是生成props属性的。点击Step Out将断点跳出transformModel函数,走进buildProps函数中,可以看到buildProps函数中调用transformModel函数的代码如下图:
buildProps

从上图中可以看到执行directiveTransforms对象中的转换函数不仅可以对节点进行转换,还会返回一个props数组。比如我们这里处理的是v-model指令,返回的props数组就是由v-model指令编译而来的props属性,这就是所谓的v-model语法糖。

看到这里有的小伙伴会疑惑了v-model指令不是会生成modelValueonUpdate:modelValue两个属性,为什么这里只有一个onUpdate:modelValue属性呢?

答案是只有给自定义组件上面使用v-model指令才会生成modelValueonUpdate:modelValue两个属性,对于这种原生input标签是不需要生成modelValue属性的,而且input标签本身是不接收名为modelValue属性,接收的是value属性。

总结

现在我们再来看看最开始讲的流程图,我想你应该已经能将整个流程串起来了。如下图:
full-progress

transform函数的执行过程主要分为下面这几步:

  • transform函数中调用createTransformContext函数生成上下文对象。在上下文对象中存储了当前正在转换的node节点的信息,后面的traverseNodetraverseChildrennodeTransforms数组中的转换函数、directiveTransforms对象中的转换函数都会依赖这个上下文对象。

  • 然后执行traverseNode函数,traverseNode函数是一个典型的洋葱模型。第一次执行traverseNode函数的时候会进入洋葱模型的第一层,先将nodeTransforms数组中的转换函数全部执行一遍,对第一层的node节点进行第一次转换,将转换函数返回的回调函数存到第一层的exitFns数组中。经过第一次转换后v-for等指令已经被初次处理了。

  • 然后执行traverseChildren函数,在traverseChildren函数中对当前node节点的子节点执行traverseNode函数。此时就会进入洋葱模型的第二层,和上一步一样会将nodeTransforms数组中的转换函数全部执行一遍,对第二层的node节点进行第一次转换,将转换函数返回的回调函数存到第二层的exitFns数组中。

  • 假如第二层的node节点已经没有了子节点,洋葱模型就会从“进入阶段”变成“出去阶段”。将第二层的exitFns数组中存的回调函数全部执行一遍,对node节点进行第二次转换,然后出去到第一层的洋葱模型。经过第二次转换后v-for等指令已经被完全处理了。

  • 同样将第一层中的exitFns数组中存的回调函数全部执行一遍,由于此时第二层的node节点已经全部处理完了,所以在exitFns数组中存的回调函数中就可以根据子节点的情况来处理父节点。

  • 执行nodeTransforms数组中的transformElement转换函数,会返回一个回调函数。在回调函数中会调用buildProps函数,在buildProps函数中只有当node节点中有对应的指令才会执行directiveTransforms对象中对应的转换函数。比如当前node节点有v-model指令,才会去执行transformModel转换函数。v-model等指令也就被处理了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/553838.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

国外GIS软件排名简介<30个>

简介 国外gisgeography网站进行了一次GIS软件排名&#xff0c;通过分析、制图、编辑等因素进行测试&#xff0c;具体规则如下&#xff1a; 分析&#xff1a;矢量/栅格工具、时态、地统计、网络分析和脚本。 制图&#xff1a;地图类型、坐标系、地图布局/元素、标注/注记、3D …

请勿假设你的用户都有管理员权限

有些人觉得自己很聪明&#xff0c;他们在程序中做了这样一项”优化”。 在程序的安装阶段&#xff0c;他们不会安装某些程序功能&#xff0c;而是等到用户第一次使用的时候才执行&#xff0c;也即所谓的 “按需加载”。 问题在于&#xff0c;第一次使用的时候&#xff0c;用户…

CSS-布局

display display 属性是用于控制 布局 的最重要的 CSS 属性。display 属性规定是否/如何显示元素。 每个 HTML 元素都有一个默认的 display 值&#xff0c;具体取决于它的元素类型。大多数元素的默认 display 值为 block 或 inline。 block block&#xff1a;块级元素。块级…

从二本调剂到上海互联网公司算法工程师:我的成长故事

探讨选择成为一名程序员的原因&#xff0c;是出于兴趣还是职业发展&#xff1f; 在这个科技飞速发展的时代&#xff0c;程序员这一职业无疑成为了许多人眼中的香饽饽。那么&#xff0c;是什么驱使着越来越多的人选择投身于这一行业呢&#xff1f;是出于对编程的热爱&#xff0…

三步教你怎么把icloud照片恢复至iphone!

“我手机里面照片被优化后&#xff0c;然后不小心把所有被优化的模糊照片从手机中删除了&#xff0c;但是iCloud还有&#xff0c;我应该怎样把iCloud的照片重新放回手机&#xff1f;谢谢。” 在使用iPhone时&#xff0c;iCloud照片库是一个非常方便的功能&#xff0c;它允许你在…

文化=知识+素质!电动车限制多!——早读(逆天打工人爬取热门微信文章解读)

你是一个有文化的人&#xff01; 引言Python 代码第一篇 洞见 一个人有没有文化&#xff0c;就看这五点第二篇 人民日报 来啦 新闻早班车要闻社会政策 结尾 知耻近乎勇 文化教会我们自省 以羞耻心为镜 照见自我 不断向善向上。 引言 绝了 昨天晚上早早上床 10点左右就睡眠模…

微信小程序自定义导航栏定位及胶囊按钮图解

在自定义小程序导航栏时&#xff0c;右上角的胶囊&#xff08;MenuButton&#xff09;在不同机型测试&#xff0c;会发现很难适配。 实测中 不同的手机&#xff0c;胶囊高度不一样、状态栏高度不一样。与模拟器显示的情况是不一样的。 由于小程序在不同的手机上顶部布局会发生…

单片机入门还能从51开始吗?

选择从51单片机开始入门还是直接学习基于ARM核或RISC核的单片机&#xff0c;取决于学习目标、项目需求以及个人兴趣。每种单片机都有其特定的优势和应用场景&#xff0c;了解它们的特点可以帮助你做出更合适的选择。 首先&#xff0c;我们说一下51单片机的优势&#xff1a; 成熟…

设计模式之模板方法模式详解(上)

模板方法模式 1&#xff09;概述 1.定义 定义一个操作中算法的框架&#xff0c;而将一些步骤延迟到子类中&#xff0c;模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 2.方案 背景&#xff1a;某个方法的实现需要多个步骤&#xff08;类似…

Postman之接口测试

接口测试的必要条件 &#xff1a;请求方式、请求协议、请求地址、请求头、请求参数 常用请求方式 &#xff1a;Get请求&#xff08;get请求一般是获取数据&#xff09;、Post请求&#xff08;post请求一般是提交数据&#xff09; 传参格式 &#xff1a;表单提交、请求体提交 注…

Golang入门教程(非常详细)从零基础入门到精通,看完这一篇就够了

文章目录 一、golang 简介 1. go 语言特点2. go 语言应用领域3. 使用 go 语言的公司有哪些 二、安装 golang 1. golang 下载安装2. 配置环境变量 三、golang 开发工具 1. 安装 VSCode2. 下载所需插件 四、第一个 golang 应用 1. main 包的含义2. 示例 一、golang 简介 Go 是一…

uniapp开发微信小程序:用户手机号授权获取全流程详解与实战示例

随着多端小程序研发工具的日益普及&#xff0c;诸如uniapp、Taro、Flutter等跨平台解决方案使得开发者能够高效地构建同时适配多个主流小程序平台&#xff08;如微信、支付宝、百度、字节跳动等&#xff09;的应用。尽管各平台间存在一定的差异性&#xff0c;但在获取用户手机号…

批量插入10w数据方法对比

环境准备(mysql5.7) CREATE TABLE user (id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 唯一id,user_id bigint(10) DEFAULT NULL COMMENT 用户id-uuid,user_name varchar(100) NOT NULL COMMENT 用户名,user_age bigint(10) DEFAULT NULL COMMENT 用户年龄,create_time time…

【Linux】应用层协议序列化和反序列化

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;折纸花满衣 &#x1f3e0;个人专栏&#xff1a;题目解析 &#x1f30e;推荐文章&#xff1a;C【智能指针】 前言 在正式代码开始前&#xff0c;会有一些前提知识引入 目录 &#x1f449;&#x1f3fb;序列…

人造石墨电极下游应用集中在钢铁冶炼领域 行业市场份额集中在少数企业

人造石墨电极下游应用集中在钢铁冶炼领域 行业市场份额集中在少数企业 人造石墨电极是以石油焦、针状焦为主要原材料&#xff0c;煤沥青为粘结剂&#xff0c;经过煅烧、粉碎磨粉、配料混捏、挤压成形、焙烧、沥青浸渍、石墨化、机械加工等一系列工序生产出来的一种耐高温石墨质…

第47篇:简易处理器<一>

Q&#xff1a;本期我们开始介绍一种数字系统----简易处理器&#xff0c;可以执行由指令指定的各种操作。 A&#xff1a;简易处理器包含多个9位寄存器、一个数据选择器、一个加/减法器单元和一个控制单元(有限状态机)。 数据选择器&#xff1a;可以将输入数据加载到各种寄存器&…

Linux(磁盘管理与文件系统)

目录 1. 磁盘基础 1.1 磁盘结构 1.2 MBR 1.3 磁盘分区结构 2. 文件系统类型 2.1 XFS文件系统 2.2 SWAP 2.3 fdisk命令 2.4 创建新硬盘 3.创建文件系统 3.1 mkfs 3.2 挂载、卸载文件系统 3.3 查看磁盘使用情况 1. 磁盘基础 1.1 磁盘结构 磁盘的物理结构 盘片:硬…

活动理论的散点图

import pandas as pd import matplotlib.pyplot as plt# 假设您已经有一个名为 data.xlsx 的 Excel 文件 # 您可以使用以下代码读取数据# 读取 Excel 文件 try:data pd.read_excel(data.xlsx) except Exception as e:print(f"Error: {e}")# 假设您的数据包含以下列:…

网络安全事件频发,让态势感知来提前洞察快速防护

一、引言 随着信息技术的飞速发展&#xff0c;网络安全问题日益凸显&#xff0c;成为社会各界普遍关注的焦点。近年来&#xff0c;网络安全事件频发&#xff0c;给个人、企业乃至国家带来了严重的损失。这些事件不仅揭示了网络安全领域的严峻挑战&#xff0c;也敲响了信息安全…

使用Docker,【快速】搭建个人博客【WordPress】

目录 1.安装Mysql&#xff0c;创建&#xff08;WordPress&#xff09;用的数据库 1.1.安装 1.2.创建数据库 2.安装Docker 3.安装WodPress&#xff08;使用Docker&#xff09; 3.1.创建文件夹 3.2.查看镜像 3.3.获取镜像 3.4.查看我的镜像 3.5.使用下载的镜像&#xf…