本文基于
Vue 3.2.30
版本源码进行分析为了增加可读性,会对源码进行删减、调整顺序、改变部分分支条件的操作,文中所有源码均可视作为伪代码
由于ts版本代码携带参数过多,不利于展示,大部分伪代码会取编译后的js代码,而不是原来的ts代码
文章内容
从《Vue3源码-整体渲染流程》这篇文章中,我们可以知道,一开始会将我们编写的<template></template>
内容最终会被编译成为render()
函数,render()
函数中执行createBaseVNode(_createElementVNode)
/createVNode(_createVNode)
返回渲染结果形成vnode
数据,本文将分析和总结:
- 从源码的角度,分析运行时编译
<template></template>
为render()
的流程 - 从源码的角度,进行
Vue3
相关编译优化的总结 - 从源码的角度,进行
Vue2
相关编译优化的总结
前置知识
单文件组件
Vue 的单文件组件 (即 *.vue
文件,英文 Single-File Component,简称 SFC) 是一种特殊的文件格式,使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中,如:
<script>
export default {
data() {
return {
greeting: 'Hello World!'
}
}
}
</script>
<template>
<p class="greeting">{{ greeting }}</p>
</template>
<style>
.greeting {
color: red;
font-weight: bold;
}
</style>
Vue SFC 是一个框架指定的文件格式,因此必须交由 @vue/compiler-sfc 编译为标准的 JavaScript 和 CSS,一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块,这也意味着在构建配置正确的前提下,你可以像导入其他 ES 模块一样导入 SFC:
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent
}
}
在实际项目中,我们一般会使用集成了 SFC 编译器的构建工具,比如 Vite 或者 Vue CLI (基于 webpack)
运行时编译模板
当以无构建步骤方式使用 Vue 时,组件模板要么是写在页面的 HTML 中,或者是内联的 JavaScript 字符串。在这些场景中,为了执行动态模板编译,Vue 需要将模板编译器运行在浏览器中。相对的,如果我们使用了构建步骤,由于提前编译了模板,那么就无须再在浏览器中运行了。为了减小打包出的客户端代码体积,Vue 提供了多种格式的“构建文件”以适配不同场景下的优化需求:
- 前缀为
vue.runtime.*
的文件是只包含运行时的版本:不包含编译器,当使用这个版本时,所有的模板都必须由构建步骤预先编译 - 名称中不包含
.runtime
的文件则是完全版:即包含了编译器,并支持在浏览器中直接编译模板。然而,体积也会因此增长大约 14kb
如上图所示,vue3
源码最后打包生成的文件中,有vue.xxx.js
和vue.runtime.xxx.js
,比如vue.global.js
和vue.runtime.global.js
注意:名称中不包含.runtime
的文件包含了编译器,并支持在浏览器中直接编译模板,仅仅是支持编译模板,不是支持编译.vue
文件,.vue
文件还是需要构建工具,比如webpack
集成vue-loader
解析.vue
文件
在vue.runtime.global.js
中,我们可以发现这样的注释,如下所示,该版本不支持运行时编译
const compile$1 = () => {
{
warn$1(`Runtime compilation is not supported in this build of Vue.` +
(` Use "vue.global.js" instead.`
) /* should not happen */);
}
};
在vue.global.js
中,如下面代码所示,会进行compile
方法的注册
function registerRuntimeCompiler(_compile) {
compile = _compile;
installWithProxy = i => {
if (i.render._rc) {
i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers);
}
};
}
registerRuntimeCompiler(compileToFunction);
在之前的文章Vue3源码-整体渲染流程浅析中,如下图所示,在触发渲染时,会触发finishComponentSetup()
的执行
在finishComponentSetup()
中,会检测是否存在instance.render
以及compile()
方法是否存在,如果不存在instance.render
并且compile()
方法存在,则会触发compile()
方法执行编译流程
function finishComponentSetup(instance, isSSR, skipOptions) {
const Component = instance.type;
// template / render function normalization
// could be already set when returned from setup()
if (!instance.render) {
// only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
// is done by server-renderer
if (!isSSR && compile && !Component.render) {
const template = Component.template;
if (template) {
{
startMeasure(instance, `compile`);
}
const { isCustomElement, compilerOptions } = instance.appContext.config;
const { delimiters, compilerOptions: componentCompilerOptions } = Component;
const finalCompilerOptions = extend(extend({
isCustomElement,
delimiters
}, compilerOptions), componentCompilerOptions);
Component.render = compile(template, finalCompilerOptions);
{
endMeasure(instance, `compile`);
}
}
}
instance.render = (Component.render || NOOP);
// for runtime-compiled render functions using `with` blocks, the render
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
if (installWithProxy) {
installWithProxy(instance);
}
}
}
接下来我们将针对compile()
方法执行编译流程进行具体的源码分析
1. Vue3的编译流程
不会执迷于具体的编译细节,只是对一个整体的流程做一个分析
1.1 整体概述
从下面的代码块可以知道,整个编译流程只要经历了三个阶段
baseParse()
:转化<template></template>
为ast
语法树transform()
:转化ast
数据generate()
:根据上面两个流程的ast
数据生成render()
函数
function compile(template, options = {}) {
return baseCompile(template, extend({}, parserOptions, options, { ...}));
}
function baseCompile(template, options = {}) {
const ast = isString(template) ? baseParse(template, options) : template;
transform(ast, extend({}, options, { ...}));
return generate(ast, extend({}, options, { ...}));
}
注:抽象语法树(abstract syntax tree或者缩写为AST),是源代码的抽象语法结构的树状表现形式,比如下面代码块,它可以描述一段HTML的语法结构
const ast = {
type: 0,
children: [
{
type: 1,
tag: "div",
props:[{type: 3, name: "class", value: {}}],
children: []
},
{
type: 1,
content: "这是一段文本"
}
],
loc: {}
}
1.2 baseParse():生成AST
function baseParse(content, options = {}) {
const context = createParserContext(content, options);
const start = getCursor(context);
const root = parseChildren(context, 0 /* DATA */, []);
return createRoot(root, getSelection(context, start));
}
1.2.1 createParserContext():创建上下文context
本质上是创建一个类似全局的上下文context
,将一些方法和状态放入这个context
中,然后将这个context
传入每一个构建AST
的方法中,后面每一次子节点的AST
构建,都会维护和更新这个context
以及使用它所声明的一些基础方法
function createParserContext(content, rawOptions) {
const options = extend({}, defaultParserOptions);
let key;
for (key in rawOptions) {
options[key] = rawOptions[key] === undefined
? defaultParserOptions[key]
: rawOptions[key];
}
return {
options,
column: 1,
line: 1,
offset: 0,
source: content
};
}
function getCursor(context) {
const { column, line, offset } = context;
return { column, line, offset };
}
1.2.2 parseChildren-根据不同情况构建nodes数组
parseChildren
主要是根据不同的条件,比如以"<"
开头、以">"
结尾,比如以"<"
开头,以"/>"
结尾,比如以"<"
开头,以"/>"
结尾,然后构建出对应的node
数组存放在nodes
数组中,最后再处理空白字符
整体概述
function parseChildren(context, mode, ancestors) {
const parent = last(ancestors);
const ns = parent ? parent.ns : 0 /* HTML */;
const nodes = [];
while (!isEnd(context, mode, ancestors)) {
const s = context.source;
let node = undefined;
if (mode === 0 /* DATA */ || mode === 1 /* RCDATA */) {
// 根据不同条件情况去调用不同的解析方法
node = xxxxxx
}
if (!node) {
node = parseText(context, mode);
}
if (isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node[i]);
}
}
else {
pushNode(nodes, node);
}
}
// ...省略处理nodes每一个元素空白字符的逻辑
return removedWhitespace ? nodes.filter(Boolean) : nodes;
}
如下面流程图所示,主要是根据不同的条件,调用不同的方法进行node
的构建,主要分为:
- 标签以及标签对应的属性解析,比如
<div class="test1"></div>
- 插值的解析,如
{{refCount.count}}
- 注释节点的解析,如
<!-- 这是一段注释内容 -->
- 纯文本内容的解析,如
<div>我是一个纯文本</div>
中的我是一个纯文本
在经过不同条件的解析之后,我们可以得到每一种类型的AST
数据,比如下面代码块所示,最终<template></template>
中的每一个元素都会转化为一个AST
节点数据
AST节点会根据<template></template>
的父子关系进行数据的构建,比如children也是一个AST
数组集合
{
type: 1,
tag,
tagType,
props,
children: []
}
为了更好理解每一个条件的处理逻辑,我们下面将对其中一种情况调用的方法parseElement()
进行具体的分析
parseElement分析
暂时移除Pre和VPre相关逻辑
这个方法是为了处理<div class="test1"><p></p><span></span>
,为了处理这种标签,我们一步一步地解析
- 处理
Start tag
:使用parseTag()
处理<div class="test1">
,包括标签名div
、属性值class="test1"
- 处理
children
:ancestors
压入当前的element
(为了后续解析时能正确建立父子关系),然后递归调用parseChildren()
处理<p></p><span></span>
,获取nodes
数组,弹出ancestors
压入的element
,将nodes
赋值给element.children
- 处理
End tag
:检测end Tag
是否跟element.tag
对应,如果可以对应,则更新context
存储的解析状态(类似指针一样,将指针后移到没有解析的部分)
function parseElement(context, ancestors) {
// Start tag.
const parent = last(ancestors); //ancestors[ancestors.length - 1];
const element = parseTag(context, 0 /* Start */, parent);
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element;
}
// Children.
ancestors.push(element);
const mode = context.options.getTextMode(element, parent);
const children = parseChildren(context, mode, ancestors);
ancestors.pop();
element.children = children;
// End tag.
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, 1 /* End */, parent);
} else {
// 找不到endTag,进行错误的emit
}
element.loc = getSelection(context, element.loc.start);
return element;
}
处理空白字符
主要使用正则表达式进行空白字符的剔除
空白字符也是一个文本内容,剔除空白字符可以减少后续对这些没有意义内容的编译处理效率
function parseChildren(context, mode, ancestors) {
// ...省略构建nodes数组的逻辑
// Whitespace handling strategy like v2
let removedWhitespace = false;
if (mode !== 2 /* RAWTEXT */ && mode !== 1 /* RCDATA */) {
const shouldCondense = context.options.whitespace !== 'preserve';
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// 删除空白字符...
}
if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
// 根据 html 规范删除前导换行符
const first = nodes[0];
if (first && first.type === 2 /* TEXT */) {
first.content = first.content.replace(/^\r?\n/, '');
}
}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes;
}
1.2.3 createRoot-返回root对象的AST数据
从上面parseChildren()
获取成功nodes
数组后,作为children
参数传入到createRoot()
,构建出一个ROOT
的AST
对象返回
function baseParse(content, options = {}) {
const context = createParserContext(content, options);
const start = getCursor(context);
const root = parseChildren(context, 0 /* DATA */, []);
return createRoot(root, getSelection(context, start));
}
function createRoot(children, loc = locStub) {
return {
type: 0 /* ROOT */,
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc
};
}
1.3 transform()
从一开始的编译阶段,如下面的代码块所示,我们可以知道,当我们执行完成ast
的构建,我们会执行transform()
进行ast
的转化工作
function baseCompile(template, options = {}) {
const ast = ...
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
transform(ast, extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} // user transforms
)
}));
return ...
}
function getBaseTransformPreset(prefixIdentifiers) {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
...([]),
...([transformExpression]
),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
];
}
1.3.1 整体概述
在经过baseParse()
将<template></template>
转化为基础的AST
数据后,会进入一个AST
语法分析阶段
在这个语法分析阶段中,我们会为AST
数据进行转化,为其添加编译优化相关的属性,比如静态提升、Block
的构建等等
语法分析阶段会构建出信息更加丰富的AST
数据,为后面的generate()
(render()
函数的生成)做准备
function transform(root, options) {
const context = createTransformContext(root, options);
traverseNode(root, context);
if (options.hoistStatic) {
hoistStatic(root, context);
}
if (!options.ssr) {
createRootCodegen(root, context);
}
// finalize meta information
root.helpers = [...context.helpers.keys()];
root.components = [...context.components];
root.directives = [...context.directives];
root.imports = context.imports;
root.hoists = context.hoists;
root.temps = context.temps;
root.cached = context.cached;
}
1.3.2 createTransformContext()
建立一个transform
的上下文,本质也是一个贯穿transform()
流程的一个全局变量,用来提供全局的辅助方法和存储对应的状态
其中nodeTransforms
,directiveTransforms
是在baseCompile()
中通过getBaseTransformPreset()
得到的节点和指令转化辅助方法,比如transformIf
、on:transformOn
等辅助方法,对应<template></template>
的v-if
和v-on
形成的AST
基础数据的transform
的转化
function createTransformContext(...) {
const context = {
//...
hoists: [],
parent: null,
currentNode: root,
nodeTransforms,
directiveTransforms,
// methods
helper(name) {
const count = context.helpers.get(name) || 0;
context.helpers.set(name, count + 1);
return name;
},
replaceNode(node) {
context.parent.children[context.childIndex] = context.currentNode = node;
}
//...
};
return context;
}
1.3.3 traverseNode()
如下面代码块所示,traverseNode()
逻辑主要分为三个部分:
- 执行
nodeTransforms
函数进行node
数据的处理,会根据目前node
的类型,比如是v-for
而触发对应的transformFor()
转化方法,执行完毕后,拿到对应的onExit
方法,存入数组中 - 处理
node.children
,更新context.parent
和context.childIndex
,然后递归调用traverseNode(childNode)
- 处理一些
node
需要等待children
处理完毕后,再处理的情况,调用parentNode
的onExit
方法(onExit
方法也就是第一部分我们缓存的数组)
function transform(root, options) {
const context = createTransformContext(root, options);
traverseNode(root, context);
if (options.hoistStatic) {
hoistStatic(root, context);
}
if (!options.ssr) {
createRootCodegen(root, context);
}
//...
}
function traverseNode(node, context) {
context.currentNode = node;
const { nodeTransforms } = context;
for (let i = 0; i < nodeTransforms.length; i++) {
// 执行nodeTransforms函数进行node数据的处理
const onExit = nodeTransforms[i](node, context);
// 缓存onExit方法
// 处理被删除或者被替换的情况
}
// 根据不同条件处理node.children,本质是递归调用traverseNode()
traverseChildren(node, context); //node.type=IF_BRANCH/FOR/ELEMENT/ROOT
// 处理一些node需要等待children处理完毕后,再处理的情况
// 即调用parentNode的onExit方法
context.currentNode = node;
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
traverseNode第1部分:nodeTransforms
nodeTransforms到底是什么东西?又是如何找到指定的node?
在一开始的初始化中,我们就可以知道,会默认初始化getBaseTransformPreset()
,如下面代码块所示,会初始化几个类型的处理方法
function baseCompile(template, options = {}) {
const ast = ...
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
}
function getBaseTransformPreset(prefixIdentifiers) {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
...([]),
...([transformExpression]
),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
];
}
我们随机取一个,比如transformFor
这个处理方法
const transformFor = createStructuralDirectiveTransform('for', (node, dir, context) => {
const { helper, removeHelper } = context;
return processFor(node, dir, context, forNode => {
return () => {
};
});
});
从下面代码块中,我们可以发现,每一个处理方法都会传入一个name
字符串,而只有node.prop.name
跟处理方法传入的name
匹配上(matches(prop.name)
)才会执行createStructuralDirectiveTransform()
的第二个参数fn
,也就是上面代码块中的return processFor(xxx,xxx,xxx)
所返回的方法
function createStructuralDirectiveTransform(name, fn) {
// name = "for"
const matches = isString(name)
? (n) => n === name
: (n) => name.test(n);
return (node, context) => {
if (node.type === 1 /* ELEMENT */) {
const { props } = node;
if (node.tagType === 3 /* TEMPLATE */ && props.some(isVSlot)) {
return;
}
const exitFns = [];
for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (prop.type === 7 /* DIRECTIVE */ && matches(prop.name)) {
props.splice(i, 1);
i--;
const onExit = fn(node, prop, context);
if (onExit)
exitFns.push(onExit);
}
}
return exitFns;
}
};
}
而执行processFor()
最后一个参数processCodegen
也是一个方法,从下面两个代码块,我们可以发现processFor
的执行顺序为
- 进行
forNode
的数据的初始化,然后调用processCodegen()
方法触发createVNodeCall()
构建forNode.codegenNode
属性 - 使用
context.replaceNode(forNode)
替换当前的node
对应的AST
基础数据 - 最后返回
const onExit = nodeTransforms[i](node, context)
对应的onExit
方法,进行缓存
在下面traverseNode第3部分的逻辑中,会执行上面缓存的onExit
方法,实际上调用的是processFor
的forNode
参数中所对应的返回的箭头函数,如下面所示
const transformFor = createStructuralDirectiveTransform('for', (node, dir, context) => {
const { helper, removeHelper } = context;
// processCodegen()就是forNode=>{}
return processFor(node, dir, context, forNode => {
// 下面代码块processFor()执行完成后,在执行这个createVNodeCall()方法构建forNode.codegenNode
forNode.codegenNode = createVNodeCall(context, helper(FRAGMENT), ...);
return () => {
// 实际onExit方法执行的地方
};
});
});
function processFor(node, dir, context, processCodegen) {
const parseResult = parseForExpression(dir.exp, context);
const { source, value, key, index } = parseResult;
const forNode = {
type: 11 /* FOR */,
loc: dir.loc,
source,
...
parseResult,
children: isTemplateNode(node) ? node.children : [node]
};
//context.parent.children[context.childIndex] = context.currentNode = node;
context.replaceNode(forNode);
scopes.vFor++;
const onExit = processCodegen && processCodegen(forNode);
return () => {
scopes.vFor--;
if (onExit)
onExit();
};
}
function createVNodeCall(...) {
//...
return {
type: 13 /* VNODE_CALL */,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
isComponent,
loc
};
}
traverseNode第2部分:traverseChildren
根据不同条件处理node.children
,当node.type
=IF_BRANCH
/FOR
/ELEMENT
/ROOT
时调用traverseChildren()
,本质上是递归调用traverseNode()
function traverseNode(node, context) {
//...
switch (node.type) {
case 3 /* COMMENT */:
if (!context.ssr) {
context.helper(CREATE_COMMENT);
}
break;
case 5 /* INTERPOLATION */:
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING);
}
break;
case 9 /* IF */:
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context);
}
break;
case 10 /* IF_BRANCH */:
case 11 /* FOR */:
case 1 /* ELEMENT */:
case 0 /* ROOT */:
traverseChildren(node, context);
break;
}
//...
}
function traverseChildren(parent, context) {
let i = 0;
const nodeRemoved = () => {
i--;
};
for (; i < parent.children.length; i++) {
const child = parent.children[i];
if (isString(child))
continue;
context.parent = parent;
context.childIndex = i;
context.onNodeRemoved = nodeRemoved;
traverseNode(child, context);
}
}
traverseNode第3部分:exitFns
由下面代码块可以知道,nodeTransforms()
会返回一个onExit()
方法,然后在处理完成node.children
后,再进行逐步调用
function traverseNode(node, context) {
context.currentNode = node;
// apply transform plugins
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);
}
}
}
//...省略处理node.children的逻辑
// exit transforms
context.currentNode = node;
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
而这种onExit()
执行的内容,则根据不同条件而不同,正如我们上面使用transformFor
作为nodeTransforms[i]
其中一个方法分析一样,最终的onExit()
执行的内容就是processFor
的forNode
方法中所返回的方法所执行的内容
这里不再详细分析每一个nodeTransforms[i]
所对应的onExit()
执行的内容以及作用,请读者自行参考其它文章
const transformFor = createStructuralDirectiveTransform('for', (node, dir, context) => {
const { helper, removeHelper } = context;
return processFor(node, dir, context, forNode => {
// 下面代码块processFor()执行完成后,在执行这个createVNodeCall()方法构建forNode.codegenNode
forNode.codegenNode = createVNodeCall(context, helper(FRAGMENT), ...);
return () => {
// 实际onExit方法执行的地方
};
});
});
1.3.4 hoistStatic()
function transform(root, options) {
const context = createTransformContext(root, options);
traverseNode(root, context);
if (options.hoistStatic) {
hoistStatic(root, context);
}
if (!options.ssr) {
createRootCodegen(root, context);
}
// finalize meta information
root.helpers = [...context.helpers.keys()];
root.components = [...context.components];
root.directives = [...context.directives];
root.imports = context.imports;
root.hoists = context.hoists;
root.temps = context.temps;
root.cached = context.cached;
}
function hoistStatic(root, context) {
walk(root, context,
// Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes.
isSingleElementRoot(root, root.children[0]));
}
getConstantType()返回一共有4种类型,为0 (NOT_CONSTANT