看不懂来打我,vue3如何将template编译成render函数
前言
在之前的 通过debug搞清楚.vue文件怎么变成.js文件 文章中我们讲过了vue文件是如何编译成js文件,通过那篇文章我们知道了,template编译为render函数底层就是调用了@vue/compiler-sfc
包暴露出来的compileTemplate
函数。由于文章篇幅有限,我们没有去深入探索compileTemplate
函数是如何将template模块编译为render
函数,在这篇文章中我们来了解一下。
@vue
下面的几个包
先来介绍一下本文中涉及到vue下的几个包,分别是:@vue/compiler-sfc
、@vue/compiler-dom
、@vue/compiler-core
。
@vue/compiler-sfc
:用于编译vue的SFC文件,这个包依赖vue下的其他包,比如@vue/compiler-dom
和@vue/compiler-core
。这个包一般是给vue-loader 和 @vitejs/plugin-vue使用的。@vue/compiler-dom
:这个包专注于浏览器端的编译,处理浏览器dom相关的逻辑都在这里面。@vue/compiler-core
:从名字你也能看出来这个包是vue编译部分的核心,提供了通用的编译逻辑,不管是浏览器端还是服务端编译最终都会走到这个包里面来。
先来看个流程图
先来看一下我画的template模块编译为render
函数这一过程的流程图,让你对整个流程有个大概的印象,后面的内容看着就不费劲了。如下图:
从上面的流程图可以看到整个流程可以分为7步:
执行
@vue/compiler-sfc
包的compileTemplate
函数,里面会调用同一个包的doCompileTemplate
函数。执行
@vue/compiler-sfc
包的doCompileTemplate
函数,里面会调用@vue/compiler-dom
包中的compile
函数。执行
@vue/compiler-dom
包中的compile
函数,里面会对options
进行了扩展,塞了一些处理dom的转换函数进去。分别塞到了options.nodeTransforms
数组和options.directiveTransforms
对象中。然后以扩展后的options
去调用@vue/compiler-core
包的baseCompile
函数。执行
@vue/compiler-core
包的baseCompile
函数,在这个函数中主要分为4部分。第一部分为检查传入的source是不是html字符串,如果是就调用同一个包下的baseParse
函数生成模版AST抽象语法树
。否则就直接使用传入的模版AST抽象语法树
。此时node节点中还有v-for
、v-model
等指令。这里的模版AST抽象语法树
结构和template模块中的代码结构是一模一样的,所以说模版AST抽象语法树
就是对template模块中的结构进行描述。第二部分为执行
getBaseTransformPreset
函数拿到@vue/compiler-core
包中内置的nodeTransforms
和directiveTransforms
转换函数。第三部分为将传入的
options.nodeTransforms
、options.directiveTransforms
分别和本地的nodeTransforms
、directiveTransforms
进行合并得到一堆新的转换函数,和模版AST抽象语法树
一起传入到transform
函数中执行,就会得到转换后的javascript AST抽象语法树
。在这一过程中v-for
、v-model
等指令已经被转换函数给处理了。得到的javascript AST抽象语法树
的结构和将要生成的render
函数的结构是一模一样的,所以说javascript AST抽象语法树
就是对render
函数的结构进行描述。第四部分为由于已经拿到了和render函数的结构一模一样的
javascript AST抽象语法树
,只需要在generate
函数中遍历javascript AST抽象语法树
进行字符串拼接就可以得到render
函数了。
关注公众号:前端欧阳
,解锁我更多vue
干货文章。还可以加我微信,私信我想看哪些vue
原理文章,我会根据大家的反馈进行创作。
@vue/compiler-sfc
包的compileTemplate
函数
还是同样的套路,我们通过debug一个简单的demo来搞清楚compileTemplate
函数是如何将template编译成render函数的。demo代码如下:
<template>
<input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>
<script setup lang="ts">
import { ref } from "vue";
const msgList = ref([
{
id: 1,
value: "",
},
{
id: 2,
value: "",
},
{
id: 3,
value: "",
},
]);
</script>
通过debug搞清楚.vue文件怎么变成.js文件 文章中我们已经知道了在使用vite的情况下template编译为render函数是在node端完成的。所以我们需要启动一个debug
终端,才可以在node端打断点。这里以vscode举例,首先我们需要打开终端,然后点击终端中的+
号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal
就可以启动一个debug
终端。
compileTemplate
函数在node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js
文件中,找到compileTemplate
函数打上断点,然后在debug
终端中执行yarn dev
(这里是以vite
举例)。在浏览器中访问 http://localhost:5173/,此时断点就会走到compileTemplate
函数中了。在我们这个场景中compileTemplate
函数简化后的代码非常简单,代码如下:
function compileTemplate(options) {
return doCompileTemplate(options);
}
@vue/compiler-sfc
包的doCompileTemplate
函数
我们接着将断点走进doCompileTemplate
函数中,看看里面的代码是什么样的,简化后的代码如下:
import * as CompilerDOM from '@vue/compiler-dom'
function doCompileTemplate({
source,
ast: inAST,
compiler
}) {
const defaultCompiler = CompilerDOM;
compiler = compiler || defaultCompiler;
let { code, ast, preamble, map } = compiler.compile(inAST || source, {
// ...省略传入的options
});
return { code, ast, preamble, source, errors, tips, map };
}
在doCompileTemplate
函数中代码同样也很简单,我们在debug终端中看看compiler
、source
、inAST
这三个变量的值是长什么样的。如下图:
从上图中我们可以看到此时的compiler
变量的值为undefined
,source
变量的值为template模块中的代码,inAST
的值为由template模块编译而来的AST抽象语法树。不是说好的要经过parse
函数处理后才会得到AST抽象语法树,为什么这里就已经有了AST抽象语法树?不要着急接着向下看,后面我会解释。
由于这里的compiler
变量的值为undefined
,所以compiler
会被赋值为CompilerDOM
。而CompilerDOM
就是@vue/compiler-dom
包中暴露的所有内容。执行compiler.compile
函数,就是执行@vue/compiler-dom
包中的compile
函数。compile
函数接收的第一个参数为inAST || source
,从这里我们知道第一个参数既可能是AST抽象语法树,也有可能是template模块中的html代码字符串。compile
函数的返回值对象中的code
字段就是编译好的render
函数,然后return出去。
@vue/compiler-dom
包中的compile
函数
我们接着将断点走进@vue/compiler-dom
包中的compile
函数,发现代码同样也很简单,简化后的代码如下:
import {
baseCompile,
} from '@vue/compiler-core'
function compile(src, options = {}) {
return baseCompile(
src,
Object.assign({}, parserOptions, options, {
nodeTransforms: [
...DOMNodeTransforms,
...options.nodeTransforms || []
],
directiveTransforms: shared.extend(
{},
DOMDirectiveTransforms,
options.directiveTransforms || {}
)
})
);
}
从上面的代码中可以看到这里的compile
函数也不是具体实现的地方,在这里调用的是@vue/compiler-core
包的baseCompile
函数。看到这里你可能会有疑问,为什么不在上一步的doCompileTemplate
函数中直接调用@vue/compiler-core
包的baseCompile
函数,而是要从@vue/compiler-dom
包中绕一圈再来调用呢baseCompile
函数呢?
答案是baseCompile
函数是一个处于@vue/compiler-core
包中的API,而@vue/compiler-core
可以运行在各种 JavaScript 环境下,比如浏览器端、服务端等各个平台。baseCompile
函数接收这些平台专有的一些options,而我们这里的demo是浏览器平台。所以才需要从@vue/compiler-dom
包中绕一圈去调用@vue/compiler-core
包中的baseCompile
函数传入一些浏览器中特有的options。在上面的代码中我们看到使用DOMNodeTransforms
数组对options
中的nodeTransforms
属性进行了扩展,使用DOMDirectiveTransforms
对象对options
中的directiveTransforms
属性进行了扩展。
我们先来看看DOMNodeTransforms
数组:
const DOMNodeTransforms = [
transformStyle
];
options
对象中的nodeTransforms
属性是一个数组,里面包含了许多transform
转换函数用于处理AST抽象语法树。经过@vue/compiler-dom
的compile
函数处理后nodeTransforms
数组中多了一个处理style的transformStyle
函数。这里的transformStyle
是一个转换函数用于处理dom
上面的style,比如style="color: red"
。
我们再来看看DOMDirectiveTransforms
对象:
const DOMDirectiveTransforms = {
cloak: compilerCore.noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel,
on: transformOn,
show: transformShow
};
options
对象中的directiveTransforms
属性是一个对象,经过@vue/compiler-dom
的compile
函数处理后directiveTransforms
对象中增加了处理v-cloak
、v-html
、v-text
、v-model
、v-on
、v-show
等指令的transform
转换函数。很明显我们这个demo中input
标签上面的v-model
指令就是由这里的transformModel
转换函数处理。
你发现了没,不管是nodeTransforms
数组还是directiveTransforms
对象,增加的transform
转换函数都是处理dom相关的。经过@vue/compiler-dom
的compile
函数处理后,再调用baseCompile
函数就有了处理dom相关的转换函数了。
@vue/compiler-core
包的baseCompile
函数
继续将断点走进vue/compiler-core
包的baseCompile
函数,简化后的baseCompile
函数代码如下:
function baseCompile(
source: string | RootNode,
options: CompilerOptions = {},
): CodegenResult {
const ast = isString(source) ? baseParse(source, options) : source
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
transform(
ast,
Object.assign({}, options, {
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: Object.assign(
{},
directiveTransforms,
options.directiveTransforms || {}, // user transforms
),
}),
)
return generate(ast, options)
}
我们先来看看baseCompile
函数接收的参数,第一个参数为source
,类型为string | RootNode
。这句话的意思是接收的source
变量可能是html字符串,也有可能是html字符串编译后的AST抽象语法树。再来看看第二个参数options
,我们这里只关注options.nodeTransforms
数组属性和options.directiveTransforms
对象属性,这两个里面都是存了一堆转换函数,区别就是一个是数组,一个是对象。
我们再来看看返回值类型CodegenResult
,定义如下:
interface CodegenResult {
code: string
preamble: string
ast: RootNode
map?: RawSourceMap
}
从类型中我们可以看到返回值对象中的code
属性就是编译好的render
函数,而这个返回值就是最后调用generate
函数返回的。
明白了baseCompile
函数接收的参数和返回值,我们再来看函数内的代码。主要分为四块内容:
拿到由html字符串转换成的AST抽象语法树。
拿到由一堆转换函数组成的
nodeTransforms
数组,和拿到由一堆转换函数组成的directiveTransforms
对象。执行
transform
函数,使用合并后的nodeTransforms
中的所有转换函数处理AST抽象语法树中的所有node节点,使用合并后的directiveTransforms
中的转换函数对会生成props的指令进行处理,得到处理后的javascript AST抽象语法树
。调用
generate
函数根据上一步处理后的javascript AST抽象语法树
进行字符串拼接,拼成render
函数。
获取AST抽象语法树
我们先来看第一块的内容,代码如下:
const ast = isString(source) ? baseParse(source, options) : source
如果传入的source
是html字符串,那就调用baseParse
函数根据html字符串生成对应的AST抽象语法树,如果传入的就是AST抽象语法树那么就直接赋值给ast
变量。为什么这里有这两种情况呢?
原因是baseCompile
函数可以被直接调用,也可以像我们这样由vite的@vitejs/plugin-vue
包发起,经过层层调用后最终执行baseCompile
函数。在我们这个场景中,在前面我们就知道了走进compileTemplate
函数之前就已经有了编译后的AST抽象语法树,所以这里不会再调用baseParse
函数去生成AST抽象语法树了。那么又是什么时候生成的AST抽象语法树呢?
在之前的 通过debug搞清楚.vue文件怎么变成.js文件 文章中我们讲了调用createDescriptor
函数会将vue
代码字符串转换为descriptor
对象,descriptor
对象中拥有template
属性、scriptSetup
属性、styles
属性,分别对应vue文件中的template
模块、<script setup>
模块、<style>
模块。如下图:
createDescriptor
函数在生成template
属性的时候底层同样也会调用@vue/compiler-core
包的baseParse
函数,将template模块中的html字符串编译为AST抽象语法树。
所以在我们这个场景中走到baseCompile
函数时就已经有了AST抽象语法树了,其实底层都调用的是@vue/compiler-core
包的baseParse
函数。
获取转换函数
接着将断点走到第二块内容处,代码如下:
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
从上面的代码可以看到getBaseTransformPreset
函数的返回值是一个数组,对返回的数组进行解构,数组的第一项赋值给nodeTransforms
变量,数组的第二项赋值给directiveTransforms
变量。
将断点走进getBaseTransformPreset
函数,代码如下:
function getBaseTransformPreset() {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
transformFilter,
trackVForSlotScopes,
transformExpression
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
];
}
从上面的代码中不难看出由getBaseTransformPreset
函数的返回值解构出来的nodeTransforms
变量是一个数组,数组中包含一堆transform转换函数,比如处理v-once
、v-if
、v-memo
、v-for
等指令的转换函数。很明显我们这个demo中input
标签上面的v-for
指令就是由这里的transformFor
转换函数处理。
同理由getBaseTransformPreset
函数的返回值解构出来的directiveTransforms
变量是一个对象,对象中包含处理v-on
、v-bind
、v-model
指令的转换函数。
经过这一步的处理我们就拿到了由一系列转换函数组成的nodeTransforms
数组,和由一系列转换函数组成的directiveTransforms
对象。看到这里我想你可能有一些疑问,为什么nodeTransforms
是数组,directiveTransforms
却是对象呢?为什么有的指令转换转换函数是在nodeTransforms
数组中,有的却是在directiveTransforms
对象中呢?别着急,我们下面会讲。
transform
函数
接着将断点走到第三块内容,transform
函数处,代码如下:
transform(
ast,
Object.assign({}, options, {
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: Object.assign(
{},
directiveTransforms,
options.directiveTransforms || {}, // user transforms
),
}),
)
调用transform
函数时传入了两个参数,第一个参数为当前的AST抽象语法树,第二个参数为传入的options
,在options
中我们主要看两个属性:nodeTransforms
数组和directiveTransforms
对象。
nodeTransforms
数组由两部分组成,分别是上一步拿到的nodeTransforms
数组,和之前在options.nodeTransforms
数组中塞进去的转换函数。
directiveTransforms
对象就不一样了,如果上一步拿到的directiveTransforms
对象和options.directiveTransforms
对象拥有相同的key,那么后者就会覆盖前者。以我们这个例子举例:在上一步中拿到的directiveTransforms
对象中有key为model
的处理v-model
指令的转换函数,但是我们在@vue/compiler-dom
包中的compile
函数同样也给options.directiveTransforms
对象中塞了一个key为model
的处理v-model
指令的转换函数。那么@vue/compiler-dom
包中的v-model
转换函数就会覆盖上一步中定义的v-model
转换函数,那么@vue/compiler-core
包中v-model
转换函数是不是就没用了呢?答案是当然有用,在@vue/compiler-dom
包中的v-model
转换函数会手动调用@vue/compiler-core
包中v-model
转换函数。这样设计的目的是对于一些指令的处理支持不同的平台传入不同的转换函数,并且在这些平台中也可以手动调用@vue/compiler-core
包中提供的指令转换函数,根据手动调用的结果再针对各自平台进行一些特别的处理。
我们先来回忆一下前面demo中的代码:
<template>
<input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>
<script setup lang="ts">
import { ref } from "vue";
const msgList = ref([
{
id: 1,
value: "",
},
{
id: 2,
value: "",
},
{
id: 3,
value: "",
},
]);
</script>
接着在debug终端中看看执行transform
函数前的AST抽象语法树是什么样的,如下图:
从上图中可以看到AST抽象语法树根节点下面只有一个children节点,这个children节点对应的就是input标签。在input标签上面有三个props,分别对应的是input标签上面的v-for
指令、:key
属性、v-model
指令。说明在生成AST抽象语法树的阶段不会对指令进行处理,而是当做普通的属性一样使用正则匹配出来,然后塞到props数组中。
既然在生成AST抽象语法树的过程中没有对v-model
、v-for
等指令进行处理,那么又是在什么时候处理的呢?答案是在执行transform
函数的时候处理的,在transform
函数中会递归遍历整个AST抽象语法树,在遍历每个node节点时都会将nodeTransforms
数组中的所有转换函数按照顺序取出来执行一遍,在执行时将当前的node节点和上下文作为参数传入。经过nodeTransforms
数组中全部的转换函数处理后,vue提供的许多内置指令、语法糖、内置组件等也就被处理了,接下来只需要执行generate
函数生成render
函数就行了。
nodeTransforms
数组
nodeTransforms
主要是对 node节点 进行操作,可能会替换或者移动节点。每个node节点都会将nodeTransforms
数组中的转换函数按照顺序全部执行一遍,比如处理v-if
指令的transformIf
转换函数就要比处理v-for
指令的transformFor
函数先执行。所以nodeTransforms
是一个数组,而且数组中的转换函数的顺序还是有讲究的。
在我们这个demo中input标签上面的v-for
指令是由nodeTransforms
数组中的transformFor
转换函数处理的,很简单就可以找到transformFor
转换函数。在函数开始的地方打一个断点,代码就会走到这个断点中,在debug终端上面看看此时的node
节点是什么样的,如下图:
从上图中可以看到在执行transformFor
转换函数之前的node节点和上一张图打印的node节点是一样的。
我们在执行完transformFor
转换函数的地方打一个断点,看看执行完transformFor
转换函数后node节点变成什么样了,如下图:
从上图我们可以看到经过transformFor
转换函数处理后当前的node节点已经变成了一个新的node节点,而原来的input的node节点变成了这个节点的children子节点。新节点的source.content
里存的是v-for="item in msgList"
中的msgList
变量。新节点的valueAlias.content
里存的是v-for="item in msgList"
中的item
。我们发现input子节点的props数组现在只有两项了,原本的v-for
指令的props经过transformFor
转换函数的处理后已经被消费掉了,所以就只有两项了。
看到这里你可能会有疑问,为什么执行transform
函数后会将AST抽象语法树的结构都改变了呢?
这样做的目的是在后续的generate
函数中递归遍历AST抽象语法树时,只想进行字符串拼接就可以拼成render函数。这里涉及到模版AST抽象语法树
和Javascript AST抽象语法树
的概念。
我们来回忆一下template模块中的代码:
<template>
<input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>
template模版经过parse
函数拿到AST抽象语法树,此时的AST抽象语法树的结构和template模版的结构是一模一样的,所以我们称之为模版AST抽象语法树
。模版AST抽象语法树
其实就是描述template
模版的结构。如下图:
我们再来看看生成的render
函数的代码:
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(true), _createElementBlock(
_Fragment,
null,
_renderList($setup.msgList, (item) => {
return _withDirectives((_openBlock(), _createElementBlock("input", {
key: item.id,
"onUpdate:modelValue": ($event) => item.value = $event
}, null, 8, _hoisted_1)), [
[_vModelText, item.value]
]);
}),
128
/* KEYED_FRAGMENT */
);
}
很明显模版AST抽象语法树
无法通过简单的字符串拼接就可以拼成上面的render
函数,所以我们需要一个结构和上面的render函数一模一样的Javascript AST抽象语法树
,Javascript AST抽象语法树
的作用就是描述render
函数的结构。如下图:
上面这个Javascript AST抽象语法树
就是执行transform
函数时根据模版AST抽象语法树
生成的。有了Javascript AST抽象语法树
后再来执行generate
函数时就可以只进行简单的字符串拼接,就能得到render
函数了。
directiveTransforms
对象
directiveTransforms
对象的作用是对指令进行转换,给node
节点生成对应的props
。比如给子组件上面使用了v-model
指令,经过directiveTransforms
对象中的transformModel
转换函数处理后,v-mode
节点上面就会多两个props属性:modelValue
和onUpdate:modelValue
属性。directiveTransforms
对象中的转换函数不会每次都全部执行,而是要node节点中有对应的指令,才会执行指令的转换函数。所以directiveTransforms
是对象,而不是数组。
那为什么有的指令转换函数在directiveTransforms
对象中,有的又在nodeTransforms
数组中呢?
答案是在directiveTransforms
对象中的指令全部都是会给node节点生成props属性的,那些不生成props属性的就在nodeTransforms
数组中。
很容易就可以找到@vue/compiler-dom
包的transformModel
函数,然后打一个断点,让断点走进transformModel
函数中,如下图:
从上面的图中我们可以看到在@vue/compiler-dom
包的transformModel
函数中会调用@vue/compiler-core
包的transformModel
函数,拿到返回的baseResult
对象后再一些其他操作后直接return baseResult
。从左边的call stack调用栈中我们可以看到transformModel
函数是由一个buildProps
函数调用的,看名字你应该猜到了buildProps
函数的作用是生成props属性的。点击Step Out将断点跳出transformModel
函数,走进buildProps
函数中,可以看到buildProps
函数中调用transformModel
函数的代码如下图:
从上图中可以看到,name
变量的值为model
。context.directiveTransforms[name]
的返回值就是transformModel
函数,所以执行directiveTransform(prop, node, context)
其实就是在执行transformModel
函数。在debug终端中可以看到返回的props2
是一个数组,里面存的是v-model
指令被处理后生成的props属性。props属性数组中只有一项是onUpdate:modelValue
属性,看到这里有的小伙伴会疑惑了v-model
指令不是会生成modelValue
和onUpdate:modelValue
两个属性,为什么这里只有一个呢?答案是只有给自定义组件上面使用v-model
指令才会生成modelValue
和onUpdate:modelValue
两个属性,对于这种原生input标签是不需要生成modelValue
属性的,因为input标签本身是不接收名为modelValue
属性,接收的是value属性。
其实transform
函数中的内容是非常复杂的,里面包含了vue提供的指令、filter、slot等功能的处理逻辑。transform
函数的设计高明之处就在于插件化,将处理这些功能的transform转换函数以插件的形式插入的,这样逻辑就会非常清晰了。比如我想看v-model
指令是如何实现的,我只需要去看对应的transformModel
转换函数就行了。又比如哪天vue需要实现一个v-xxx
指令,要实现这个指令只需要增加一个transformXxx
的转换函数就行了。
generate
函数
经过上一步transform
函数的处理后,已经将描述模版结构的模版AST抽象语法树
转换为了描述render
函数结构的Javascript AST抽象语法树
。在前面我们已经讲过了Javascript AST抽象语法树
就是描述了最终生成render
函数的样子。所以在generate
函数中只需要递归遍历Javascript AST抽象语法树
,通过字符串拼接的方式就可以生成render
函数了。
将断点走到执行generate
函数前,看看这会儿的Javascript AST抽象语法树
是什么样的,如下图:
从上面的图中可以看到Javascript AST
和模版AST
的区别主要有两个:
node节点中多了一个
codegenNode
属性,这个属性中存了许多node节点信息,比如codegenNode.props
中就存了key
和onUpdate:modelValue
属性的信息。在generate
函数中遍历每个node节点时就会读取这个codegenNode
属性生成render
函数模版AST
中根节点下面的children节点就是input标签,但是在这里Javascript AST
中却是根节点下面的children节点,再下面的children节点才是input标签。多了一层节点,在前面的transform
函数中我们已经讲了多的这层节点是由v-for
指令生成的,用于给v-for
循环出来的多个节点当父节点。
将断点走到generate
函数执行之后,可以看到已经生成render
函数啦,如下图:
总结
现在我们再来看看最开始讲的流程图,我想你应该已经能将整个流程串起来了。如下图:
将template编译为render函数可以分为7步:
执行
@vue/compiler-sfc
包的compileTemplate
函数,里面会调用同一个包的doCompileTemplate
函数。这一步存在的目的是作为一个入口函数给外部调用。执行
@vue/compiler-sfc
包的doCompileTemplate
函数,里面会调用@vue/compiler-dom
包中的compile
函数。这一步存在的目的是入口函数的具体实现。执行
@vue/compiler-dom
包中的compile
函数,里面会对options
进行了扩展,塞了一些处理dom的转换函数进去。给options.nodeTransforms
数组中塞了处理style的转换函数,和给options.directiveTransforms
对象中塞了处理v-cloak
、v-html
、v-text
、v-model
、v-on
、v-show
等指令的转换函数。然后以扩展后的options
去调用@vue/compiler-core
包的baseCompile
函数。执行
@vue/compiler-core
包的baseCompile
函数,在这个函数中主要分为4部分。第一部分为检查传入的source是不是html字符串,如果是就调用同一个包下的baseParse
函数生成模版AST抽象语法树
。否则就直接使用传入的模版AST抽象语法树
。此时node节点中还有v-for
、v-model
等指令,并没有被处理掉。这里的模版AST抽象语法树
的结构和template中的结构一模一样,模版AST抽象语法树
是对template中的结构进行描述。第二部分为执行
getBaseTransformPreset
函数拿到@vue/compiler-core
包中内置的nodeTransforms
和directiveTransforms
转换函数。nodeTransforms
数组中的为一堆处理node节点的转换函数,比如处理v-on
指令的transformOnce
转换函数、处理v-if
指令的transformIf
转换函数。directiveTransforms
对象中存的是对一些“会生成props的指令”进行转换的函数,用于给node
节点生成对应的props
。比如处理v-model
指令的transformModel
转换函数。第三部分为将传入的
options.nodeTransforms
、options.directiveTransforms
分别和本地的nodeTransforms
、directiveTransforms
进行合并得到一堆新的转换函数。其中由于nodeTransforms
是数组,所以在合并的过程中会将options.nodeTransforms
和nodeTransforms
中的转换函数全部合并进去。由于directiveTransforms
是对象,如果directiveTransforms
对象和options.directiveTransforms
对象拥有相同的key,那么后者就会覆盖前者。然后将合并的结果和模版AST抽象语法树
一起传入到transform
函数中执行,就可以得到转换后的javascript AST抽象语法树
。在这一过程中v-for
、v-model
等指令已经被转换函数给处理了。得到的javascript AST抽象语法树
的结构和render函数的结构一模一样,javascript AST抽象语法树
就是对render
函数的结构进行描述。第四部分为由于已经拿到了和render函数的结构一模一样的
javascript AST抽象语法树
,只需要在generate
函数中遍历javascript AST抽象语法树
进行字符串拼接就可以得到render
函数了。
关注公众号:前端欧阳
,解锁我更多vue
干货文章。还可以加我微信,私信我想看哪些vue
原理文章,我会根据大家的反馈进行创作。
看不懂来打我,vue3如何将template编译成render函数的更多相关文章
- 构建前端第9篇之(下)---vue3.0将template转化为render的过程
vue3.0将template转化为render的过程 这里是简单标记下,如何将.vue转换成js文件 具体的,先不研究了,太深,能力有限,达不到呢
- 何时/如何使用 Vue3 render 函数
什么是 DOM? 如果我们把这个 HTML 加载到浏览器中,浏览器创建这些节点,用来显示网页.所以这个HTML映射到一系列DOM节点,然后我们可以使用JavaScript进行操作.例如: let it ...
- 对于挑战书上的很久之前都看不懂的DP看懂的突破
突破一..牢记问题概念 并且牢记dp状态方程 突破二..一直有一个求和dp转化成O1dp递推的式子看不懂.. 看不懂的原因是..没有分清求和符号作用的范围 提醒:以后遇到求和符号一定明确其求和的式子的 ...
- QQ地图api里的 地址解析函数 看不懂 javascript_百度知道
QQ地图api里的 地址解析函数 看不懂 javascript_百度知道 QQ地图api里的 地址解析函数 看不懂 javascript 2011-09-18 12:18 匿名 ...
- thinkphp学习笔记10—看不懂的路由规则
原文:thinkphp学习笔记10-看不懂的路由规则 路由这部分貌似在实际工作中没有怎么设计过,只是在用默认的设置,在手册里面看到部分,艰涩难懂. 1.路由定义 要使用路由功能需要支持PATH_INF ...
- Dynamics 365-CRM又报看不懂的错误了
在CRM上执行各种操作,时不时会碰到各种问题,尤其是CRM环境里包含越来越多定制的时候.有的问题在CRM弹出的错误提示框,一目了然:而有的,可能就是简单的提示:SQL Error. 这个时候我们可能都 ...
- 一篇自己都看不懂的Matrix tree总结
Matrix tree定理用于连通图生成树计数,由于博主太菜看不懂定理证明,所以本篇博客不提供\(Matrix\ tree\)定理的证明内容(反正这个东西背结论就可以了是吧) 理解\(Matrix\ ...
- 让你看不懂的swift语法
一.Swift杂谈 Swift语法出来时间不长,网络上的各种教程已经铺天盖地,可是基本上全部的教程都是来自官方翻译. 从Swift出来到如今.每天都在学习Swift.以下给出个人感受 Swift中的非 ...
- Java 游戏报错 看不懂求教
Java 飞机小游戏 报错 看不懂求救 at java.awt.Component.dispatchEvent(Unknown Source)at java.awt.EventQueue.dispat ...
- 还看不懂同事的代码?Lambda 表达式、函数接口了解一下
当前时间:2019年 11月 11日,距离 JDK 14 发布时间(2020年3月17日)还有多少天? // 距离JDK 14 发布还有多少天? LocalDate jdk14 = LocalDate ...
随机推荐
- Linux安装Docker教程
介绍 Docker利用Linux核心中的资源分脱机制,例如cgroups,以及Linux核心名字空间(name space),来创建独立的软件容器(containers).可以在单一Linux实体下运 ...
- Java MVC 模式
MVC 模式 MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式.这种模式用于应用程序的分层开发. Model(模型) - 模型代表一个存取数据的对象或 JAVA ...
- AT_abc342_d 题解
UD 2024/2/24 22:36 感谢 Lixiang_is_potato 指出一处笔误. 本文同步发表于洛谷. 赛时挂了,但是赛后 3min AC,我是飞舞. 题意 给你一个长度为 \(N\) ...
- 快速带你入门css
css复习笔记 1. css样式值 1.1 文字样式 1 p{ 2 font-size: 30px;/*设置文字大小*/ 3 font-weight: bold;/*文字加粗*/ 4 font-sty ...
- OpenCV开发笔记(七十七):相机标定(二):通过棋盘标定计算相机内参矩阵矫正畸变摄像头图像
前言 通过相机图片可以识别出棋盘角点了,这时候我们需要通过角点去计算相机内参矩阵,通过上篇得知畸变的原理,所以我们尽可能要全方位都能获取标定图片,全方位意思是提供的多张图综合起来基本覆盖了相机所有 ...
- vim技巧--提取文本与文本替换
前几天遇到一个使用情景,需要从一个包含各个读取代码文件路径及名字的文件中把文件路径提取出来,做一个filelist,这里用到了文本的提取和替换,这里做个小总结记录一下. 从网上找了一个作者写的代码用来 ...
- 使用ScottPlot库在.NET WinForms中快速实现大型数据集的交互式显示
前言 在.NET应用开发中数据集的交互式显示是一个非常常见的功能,如需要创建折线图.柱状图.饼图.散点图等不同类型的图表将数据呈现出来,帮助人们更好地理解数据.发现规律,并支持决策和沟通.本文我们将一 ...
- 基于六轴传感器MPU6050的物体移动监测报警系统
一 系统简介 1.简介 MPU-60x0 是全球首例 9 轴运动处理传感器.它集成了 3 轴MEMS陀螺仪,3 轴MEMS加速度计,以及一个可扩展的数字运动处理器 DMP(Digital Motion ...
- python 音频通道分离的源码实现
一 前记 作为一个音频工程师,仅仅依靠鼠标点击,没有一些自己的小工具的话,肯定是不合格的了. 最近用到了一个音频通道分离的功能,这里就用python敲击了一下,这里做个备忘吧,给有需求的小伙伴抛砖引玉 ...
- 启动Eclipse 弹出Failed to load the JNI shared library jvm.dll解决方案
原因:eclipse的版本与jdk版本不一致 解决方案:两者都安装64位的,或者都安装32位的,不能一个是32位一个是64位.