对编译过程的了解会让我们对 Vue 的指令、内置组件等有更好的理解。不过由于编译的过程是一个相对复杂的过程,我们只要求理解整体的流程、输入和输出即可,对于细节我们不必抠太细。由于篇幅较长,这里会用三篇文章来讲 Vue 的编译。这是第三篇,**render code 生成**。
前言
前面两篇文章分别分享了 Vue 编译三部曲的前两曲:「 parse,template 转换为 AST」,「optimize,模型树优化」。
我们先简单回顾一下parse和optimize。parse 将开发者写的 template 模板字符串转换成抽象语法树 AST ,AST 就这里来说就是一个树状结构的 JavaScript 对象,描述了这个模板,这个对象包含了每一个元素的上下文关系。那么整个 parse 的过程是利用很多正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。optimize 就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点表示生成的 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用,提升了运行效率。
而编译的最后一步就是把优化后的 AST 树转换成可执行的代码,即 generate生成 render code。 再执行 render渲染函数生成 vnode。
接下来我们来看看 Vue generate如何将 AST 树转换为render渲染函数。
前置知识,渲染函数
在这之前我们需要先了解一个前置的知识渲染函数,Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。
为什么默认推荐的模板语法,引用一段 Vue 官网的原话如下: 任何合乎规范的 HTML 都是合法的 Vue 模板,这也带来了一些特有的优势:
- 对于很多习惯了 HTML 的开发者来说,模板比起 JSX 读写起来更自然。这里当然有主观偏好的成分,但如果这种区别会导致开发效率的提升,那么它就有客观的价值存在。
- 基于 HTML 的模板使得将已有的应用逐步迁移到 Vue 更为容易。
- 这也使得设计师和新人开发者更容易理解和参与到项目中。
- 你甚至可以使用其他模板预处理器,比如 Pug 来书写 Vue 的模板。
有些开发者认为模板意味着需要学习额外的 DSL (Domain-Specific Language 领域特定语言) 才能进行开发——我们认为这种区别是比较肤浅的。首先,JSX 并不是没有学习成本的——它是基于 JS 之上的一套额外语法。同时,正如同熟悉 JS 的人学习 JSX 会很容易一样,熟悉 HTML 的人学习 Vue 的模板语法也是很容易的。最后,DSL 的存在使得我们可以让开发者用更少的代码做更多的事,比如 v-on 的各种修饰符,在 JSX 中实现对应的功能会需要多得多的代码。 更抽象一点来看,我们可以把组件区分为两类:一类是偏视图表现的 (presentational),一类则是偏逻辑的 (logical)。我们推荐在前者中使用模板,在后者中使用 JSX 或渲染函数。这两类组件的比例会根据应用类型的不同有所变化,但整体来说我们发现表现类的组件远远多于逻辑类组件。
然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。在之前的文章我们也讨论过为什么Vue 推荐模板,但是有一些场景我们更愿意使用渲染函数? 
在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML 为例:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
上述 HTML 对应的 DOM 节点树如下图所示:
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。在之前的编译三部曲第一步中我们也介绍了,template生成 AST时,会把元素、文字、注释都创建成节点描述对象。
- type = 1的- 基础元素节点
- type = 2含有- expression和- tokens的- 文本节点
- type = 3的- 纯文本节点或者是- 注释节点```javascript child = { type: 1, tag:”div”, parent: null, children: [], attrsList: [] };
child = { type: 2, expression: res.expression, tokens: res.tokens, text: text };
child = { type: 3, text: text }; child = { type: 3, text: text, isComment: true };
每一个节点都是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点、兄弟节点 (也就是说每个部分可以包含其它的一些部分)。<br />高效地更新所有这些节点会是比较困难的,不过所幸你不必手动完成这个工作。你只需要告诉 Vue 你希望页面上的 HTML 是什么,这可以是在一个模板里:
```javascript
<h1>{{ blogTitle }}</h1>
或者一个渲染函数里:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle 发生了改变。
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:
return createElement('h1', this.blogTitle)
createElement到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
createElement
接下来我们看看createElement,在 createElement函数中可以传递标签名、属性和子节点。子节点又可以传递createElement。
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
...
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
这种结构是否似曾相识,如果你还记得 template 编译成AST后的结构,你会觉得它们是如此的类似。
这也呈上了为什么说「渲染函数,更加接近渲染器」。
而本文的重点generate编译器,它的作用和目的就是将 AST 转换为渲染函数。接下来我们就一起走进 generate 源码世界。
generate
我们先举一个例子,用一个简单的示例来看看 AST进过 generate 之后生成的render code到底长什么样?
例如有这样一段模块:
data: {
isShow: true,
list: [
'小白',
'小黄',
'小黑',
'小绿'
],
}
<div>
<ul class="list" v-if="isShow">
<li
v-for="(item, index) in list"
@click="clickItem(item)"
>
{{item}}:{{index}}
</li>
</ul>
</div>
它经过编译,执行const code = generate(ast, options),生成的 render code就会是如下这样一个字符串,注意这是一个字符串,只是为了方便大家阅读,我进行了格式化。
with (this) {
return _c('div', [
isShow
? _c(
'ul',
{ staticClass: 'list' },
_l(list, function (item, index) {
return _c(
'li',
{
on: {
click: function ($event) {
return clickItem(item);
},
},
},
[_v('\n ' + _s(item) + ':' + _s(index) + '\n ')],
);
}),
0,
)
: _e(),
]);
}
with 字符串?
大家发现生成的渲染函数字符串居然是一个with包裹的字符串,这样做的原因是with的作用域和模板的作用域是契合的,可以极大的简化编译流程。
但是肯定会有同学质疑**with**不是不推荐使用?并且有性能问题吗?为什么还要用?
尤雨溪本人的回答是这样的:
“ 因为没有什么太明显的坏处(经测试性能影响几乎可以忽略),但是 with 的作用域和模板的作用域正好契合,可以极大地简化模板编译过程。Vue 1.x 使用的正则替换 identifier path 是一个本质上 unsound 的方案,不能涵盖所有的 edge case;而走正经的 parse 到 AST 的路线会使得编译器代码量爆炸。虽然 Vue 2 的编译器是可以分离的,但凡是可能跑在浏览器里的部分,还是要考虑到尺寸问题。用 with 代码量可以很少,而且把作用域的处理交给 js 引擎来做也更可靠。
用 with 的主要副作用是生成的代码不能在 strict mode / ES module 中运行,但直接在浏览器里编译的时候因为用了 new Function(),等同于 eval,不受这一点影响。
当然,最理想的情况还是可以把 with 去掉,所以在使用预编译的时候(vue-loader 或 vueify),会自动把第一遍编译生成的代码进行一次额外处理,用完整的 AST 分析来处理作用域,把 with 拿掉,顺便支持模板中的 ES2015 语法。也就是说如果用 webpack + vue 的时候,最终生成的代码是没有 with 的。”
而对于性能的影响,使用 with 的确会造成一定的性能减低。但是真实 DOM 的渲染时间比 Virtual DOM 要长,而是否使用 with 只是影响了 Virtual DOM 的渲染,对真实 DOM 的渲染没有影响。所以对于普通需求来说,这种性能的影响比较小。
并且使用 with,  就不需要在模板里面写 this 了。而编译生成的 with(this) 可以在某种程度上实现对于作用域的动态注入。这样写方便有简单,极大的简化编译流程,虽然有小的性能影响,但是权衡之下肯定利大于弊。
_c 函数
并且在生成的渲染字符串中有这样一些醒目的标记,例如:_c
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
c 其实在源码中对于的就是createElement函数,用于创建vnode。
而还有一些常用的`(下划线)函数如:_l 对应renderList渲染列表;_v对应createTextVNode创建文本 VNode;_e对于createEmptyVNode创建空的 VNode。它们都被定义在installRenderHelpers`中。
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
generate 函数
const code = generate(ast, options)
function generate (
ast,
options
) {
var state = new CodegenState(options);
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
进入generate流程调用 generate函数,代码不是很复杂,参数也比较简单,AST转换优化后的语法树,options编译器运行时的配置项。
函数首先调用 CodegenState 构造函数,创建实例对象 state 初始化编译的状态。CodegenState 的主要作用就是给实例初始化一些相关的属性。
- options:基础的配置项
- warn:警告函数
- transforms:静态样式和属性、非静态样式和属性的处理函数引用
- dataGenFns:模块数据函数的引用
- directives:v-bind、v-model、v-text、v-html、v-on、内置指令对应 处理函数
- isReservedTag:检查是否是保留标签
- maybeComponent:检查元素是否为组件
- staticRenderFns:存放静态节点的 render 函数
- pre:记录标签是否适用了 v-pre - var CodegenState = function CodegenState (options) {
- this.options = options;
- this.warn = options.warn || baseWarn;
- this.transforms = pluckModuleFunction(options.modules, 'transformCode');
- this.dataGenFns = pluckModuleFunction(options.modules, 'genData');
- this.directives = extend(extend({}, baseDirectives), options.directives);
- var isReservedTag = options.isReservedTag || no;
- this.maybeComponent = function (el) { return !!el.component || !isReservedTag(el.tag); };
- this.onceId = 0;
- this.staticRenderFns = [];
- this.pre = false;
- };
 - 接下来就是最重要的一步是,生成 - render code string。- var code = ast ? genElement(ast, state) : '_c("div")';
 - 有 - AST就调用- genElement函数,没有的话,默认就创建一个- div。然后我们来重点看看- genElement干了些什么?- genElement 
 当调用- genElement函数时,传入已经优化处理好的- ast。然后在函数中根据不同的节点属性执行不同的生成函数。
 ①,判断- el.parent是否有值,- parent由于存储父节点信息。
 ②,如果节点是一个静态根节点- staticRoot = ture,并且节点还没有被解析过- staticProcessed = undefined就会调用- genStatic函数。此函数用于生成静态节点的渲染函数字符串。生成一个- _m的函数字符串。详情请看- genStatic函数解析↓。
 ③,如果节点存在- v-once,并且节点还没有被解析过- onceProcessed = undefined就会调用- genOnce函数。此函数用于生成- v-once节点的渲染函数字符串。生成一个- _o的函数字符串。详情请看- genonce函数解析↓。
 ④,如果存在- v-for循环,并且节点还没有被解析过- forProcessed = undefined就会调用- genFor函数。此函数用于节点存在循环的情况,生成一个- _l的函数字符串。详情请看- genFor函数解析↓。
 ⑤,如果存在- v-if循环,并且节点还没有被解析过- ifProcessed = undefined就会调用- genIf函数。此函数用于节点存在- v-if、v-else-if、v-else的情况,生成一个包含- 三目表达式的字符串(或者是嵌套的三目表达式:- a ? b ? ... : c : d),详情请看- genIf函数解析↓。
 ⑥,如果元素为- template,并且节点- !el.slotTarget && !state.pre就调用- genChildren根据子节点信息进行- render code生成,详情请看- genChildren函数解析↓。
 ⑦,如果元素为- slot,调用- genSlot函数生成一个- _t的函数字符串。详情请看- genSlot函数解析↓。
 ⑧,当以上条件都不满足进入 else 在此检测当前元素是否为组件,如果是调用- genComponent函数并且返回生成虚拟- dom渲染函数所需对应的参数格式- function genElement (el, state) {
- // ①
- if (el.parent) {
- el.pre = el.pre || el.parent.pre;
- }
- // ②
- if (el.staticRoot && !el.staticProcessed) {
- return genStatic(el, state)
- // ③
- } else if (el.once && !el.onceProcessed) {
- return genOnce(el, state)
- // ④
- } else if (el.for && !el.forProcessed) {
- return genFor(el, state)
- // ⑤
- } else if (el.if && !el.ifProcessed) {
- return genIf(el, state)
- // ⑥
- } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
- return genChildren(el, state) || 'void 0'
- // ⑦
- } else if (el.tag === 'slot') {
- return genSlot(el, state)
- } else {
- // component or element
- var code;
- if (el.component) {
- code = genComponent(el.component, el, state);
- } else {
- var data;
- if (!el.plain || (el.pre && state.maybeComponent(el))) {
- data = genData$2(el, state);
- }
- var children = el.inlineTemplate ? null : genChildren(el, state, true);
- code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
- }
- // module transforms
- for (var i = 0; i < state.transforms.length; i++) {
- code = state.transforms[i](el, code);
- }
- return code
- }
- }
 - genStatic- function genStatic (el, state) {
- el.staticProcessed = true;
- if (el.pre) {
- state.pre = el.pre;
- }
- // 将静态元素添加到 staticRenderFns 中
- state.staticRenderFns.push(("with(this){return " + (genElement(el, state)) + "}"));
- state.pre = originalPreState;
- return ("_m(" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")")
- }
 - 用于静态元素的 - render code生成,生成的- render code是一个- _m的函数字符串。
 为了方便理解举一个例子如下:- <div>
- <div><span>一则头条</span></div>
- <div><span>{{text}}</span></div>
- <div><span>一则头条</span></div>
- </div>
 - 经过 - parse后,生成这样的一段- AST描述对象。 
 再经过- generate后,就会生成如下的- render code。- _m(0)
- _m(1)
 - 这里会发现,为什么函数字符串有一个数字? 
 这是因为在执行- renderStatic函数时,也就是生成静态元素- vnode时,会从- staticRenderFns数组中读取序列元素,- staticRenderFns存放的是静态节点的- render 函数。
 上面的模板示例,就会生成这样一个- staticRenderFns。- staticRenderFns = [
- "with(this){return _c('div',[_c('span',[_v(\"一则头条\")])])}",
- "with(this){return _c('div',[_c('span',[_v(\"一则头条\")])])}"
- ]
 - 当生成 render 时是就会读取指定的渲染字符串。 - function renderStatic (
- index,
- isInFor
- ) {
- var cached = this._staticTrees || (this._staticTrees = []);
- var tree = cached[index];
- // 如果缓存存在,就直接返回
- if (tree && !isInFor) {
- return tree
- }
- // 这里是执行 render 的地方,读取 staticRenderFns 对应的静态元素
- tree = cached[index] = this.$options.staticRenderFns[index].call(
- this._renderProxy,
- null,
- this
- );
- markStatic(tree, ("__static__" + index), false);
- return tree
- }
 - 并且最后将我们静态节点,放入到 - Vue 实例的- _staticTrees中。  - genOnce- function genOnce (el, state) {
- el.onceProcessed = true;
- if (el.if && !el.ifProcessed) {
- return genIf(el, state)
- } else if (el.staticInFor) {
- ...
- if (!key) {
- return genElement(el, state)
- }
- return ("_o(" + (genElement(el, state)) + "," + (state.onceId++) + "," + key + ")")
- } else {
- return genStatic(el, state)
- }
- }
 - 用于生成包含 - v-once元素的- render code,生成的- render code是一个- _o的函数字符串。- genOnce函数本身逻辑会根据其他的元素属性来做处理。
- 如果包含属性 - v-if就会将逻辑分发到- genIf中
- 如果是一个静态节点包含在for循环中,就会生成_o的函数字符串
- 除开上面的情况就会当做静态节点处理
v-once只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once attribute 以确保这些内容只计算一次然后缓存起来。
不过,请试着不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。
genFor
function genFor (
el,
state,
altGen,
altHelper
) {
var exp = el.for;
var alias = el.alias;
var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';
var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';
...
el.forProcessed = true; // avoid recursion
return (altHelper || '_l') + "((" + exp + ")," +
"function(" + alias + iterator1 + iterator2 + "){" +
"return " + ((altGen || genElement)(el, state)) +
'})'
}
用于生成节点存在循环的render code,生成的render code一个_l的函数字符串。_l源码中对应的是 renderList函数。
为了方便理解举一个例子如下:
<div>
<ul v-for="(item, index, arr) in list">
<li>{{ item }}</li>
</ul>
</div>
经过parse后,生成这样的一段 AST 描述对象。包含了一些genFor需要用到的信息,我进行了标记。
再经过generate后,就会生成如下的 render code(为方便阅读,render code已被格式化过)。
_l(list, function (item, index, arr) {
return _c('ul', [_c('li', [_v(_s(item))])]);
});
genFor 的处理逻辑很简单,从 AST 元素节点中获取了和 for相关的一些属性,然后拼接成一个代码字符串。
genIf
function genIf (
el,
state,
altGen,
altEmpty
) {
el.ifProcessed = true;
return genIfConditions(el.ifConditions.slice(), ...)
}
用于生成节点存在条件判断的render code,genIf 主要是通过执行 genIfConditions来执行,获取节点信息中 ifConditions列表进行解析。ifConditions是一个存储条件判断相关信息的数组。这个ifConditions是怎么来的了?来源于编译的第一步中optimize解析标签时,如果节点存在属性v-if、v-else-if、v-else时,就会将表达式和节点的信息分析放入存储到ifConditions中。
为了方便理解,举一个小例子:
<div>
<div v-if="isShow === 1">1</div>
<div v-else-if="isShow === 2">2</div>
<div v-else>3</div>
</div>
有这样一段模板代码,在解析的过程中就会将v-if、v-else-if、v-else分成抽离成一个含有表达式和节点信息的对象,存储到 ifConditions 中,如下所示,这样在genIfConditions执行时,就可以在ifConditions中去读取节点和属性表达式的相关信息。
ifConditions = [
{exp: 'isShow === 1', block: ...},
{exp: 'isShow === 2', block: ...},
{exp: undefined, block: ...}
]

接着回来,看看genIfConditions中具体是怎么利用存储在ifConditions中的信息。
function genIfConditions (
conditions,
state,
altGen,
altEmpty
) {
...
//
var condition = conditions.shift();
if (condition.exp) {
return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions, state, altGen, altEmpty)))
} else {
return ("" + (genTernaryExp(condition.block)))
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
conditions就是ifConditions
它是依次从 conditions 获取第一个元素信息,然后通过对 condition.exp去生成一段三元运算符的代码,:  后是递归调用 genIfConditions,这样如果有多个 conditions,就生成多层嵌套的三元运算逻辑。
上面的例子就会生成一个嵌套的三元运算逻辑字符串表达式(为方便阅读,render code已被格式化过)。
(isShow === 1)
? _c('div',[_v("1")])
: (isShow === 2)
? _c('div',[_v("2")])
: _c('div',[_v("3")])
genChildren
function genChildren (
el,
state,
checkSkip,
altGenElement,
altGenNode
) {
var children = el.children;
if (children.length) {
...
return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
}
}
用于生成子级虚拟节点信息字符串。核心关键就是这样一段代码。
'[' +
children
.map(function (c) {
return gen(c, state);
})
.join(',') +
']' +
(normalizationType$1 ? ',' + normalizationType$1 : '');
生成返回字符串格式数组对象。children是根节点(相对)下子级节点的数组对象。通过 map再对子级节点进行字符串格式的处理。
对子节点的处理调用 gen函数,也就是源码中对于的 genNode函数。
function genNode (node, state) {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
在 genNode函数中:
- 当是元素节点时递归调用genElement() 函数,
- 当是注释节点时调用genComment 函数,
- 当是文本节点时调用genText 函数。
genCommentgenComment 函数逻辑很简单,把注释JSON 成字符串,包含在 _e的函数字符串中。
function genComment (comment) {
return ("_e(" + (JSON.stringify(comment.text)) + ")")
}
genTextgenText 函数会处理含有表达式的文本或者是纯文本。表达式的文本将text.expression包裹在_v函数字符串中,纯文本就格式化后包裹在_v函数字符串中。
纯文本的处理会将文本中的
\u2028-行分隔符和\u2029-段落分隔符进行全局转义。原因是这两个特殊字符会导致程序报错。 详情请阅读 issues:
function genText (text) {
return ("_v(" + (text.type === 2
? text.expression
: transformSpecialNewlines(JSON.stringify(text.text))) + ")")
}
function transformSpecialNewlines (text) {
return text
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029')
}
为了加深理解,举一个小例子:
<div>
<template>
<!-- 注释 -->
<div>{{text}}</div>
</template>
</div>
上面这段模板经过parse后,生成这样的一段 AST 描述对象。
再经过generate后,就会生成如下的 render code(为方便阅读,render code已被格式化过)。也就是上文说的生成字符串格式数组对象
[_e(' 注释 '), _v(' '), _c('div', [_v(_s(text))])];
genData
var code;
if (el.component) {
code = genComponent(el.component, el, state);
} else {
var data;
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData$2(el, state);
}
var children = el.inlineTemplate ? null : genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code
这里发现一个有意思的属性 el.plain,如果标签既没有使用特性key,又没有任何属性,那么该标签的元素描述对象的 plain 属性将始终为true”。这个属性是在 processElement 阶段给 ast 对象进行的扩展。
当节点不是staticRoot、没有once、没有if、没有for、不是 template、不是 slot ,那就会走到最后的判断逻辑中,在此检测当前元素是否为组件,如果是调用 genComponent函数并且返回生成虚拟dom渲染函数所需对应的参数格式。
这里有三个重点逻辑。
- 组件,genComponent函数调用
- genData对元素属性解析
- genChildren对元素的子元素解析
对于 genComponent函数内部其实就是对 genData和genChildren的封装处理,一个组件本身其实也就是很多元素的集合,只是这些元素套在了一个名为某某某的组件内部。所以用genComponent用 genData和genChildren封装也就想得通了。其中genChildren中又会递归调用genElement来处理元素。最后生成对于的 render code。
function genComponent (
componentName,
el,
state
) {
var children = el.inlineTemplate ? null : genChildren(el, state, true);
return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
}
而genChildren上一小结已经讲过,所以这里就把重点放在genData上。
在 Vue 中一个 VNode代表一个虚拟节点,而该节点的虚拟属性信息用 VNodeData描述。而VNodeData的生成就是用 genData函数来实现的。genData 函数就是根据 AST 元素节点的属性构造出一个data 对象字符串,这个在后面创建 VNode 的时候的时候会作为参数传入。
Vue 中对处理节点属性其实有三个 genData 函数。分别是 **genData**、**genData$1**和**genData$2**。三个函数分别处理不同类型的节点属性。
在 Vue 中可以使用 绑定 Class 与 绑定 Style来生成动态class 列表和内联样式。在源码中genData的作用就是处理静态的class和绑定的 class,genData$1用来处理静态的 style和绑定的 style。genData$2用来处理其他属性。
为了便于理解,我引入一个例子作为下面解析的用例:
<div
ref="test-ref"
id="testId"
key="test-key"
class="text-clasee"
:class="{ active: isActive, bindClass: hasBind }"
style="color: red"
:style="{ color: activeColor, fontSize: fontSize + 'px' }"
data-a="test-a"
data-b="test-b"
>
{{text}}
</div>
上面这段模板经过parse后,生成这样的一段 AST 描述对象。
再经过generate后,就会生成如下的 render code(为方便阅读,render code已被格式化过)。
_c(
'div',
{
key: 'test-key',
ref: 'test-ref',
staticClass: 'text-clasee',
class: { active: isActive, bindClass: hasBind },
staticStyle: { color: 'red' },
style: { color: activeColor, fontSize: fontSize + 'px' },
attrs: { id: 'testId', 'data-a': 'test-a', 'data-b': 'test-b' },
},
[_v('\n ' + _s(text) + '\n ')],
);
上面的示例告诉了我们属性生成的开头和结尾,接下来我们来看看中间过程。
属性是如何添加到 AST 中的了?
在这之前我们先来回忆一下之前 template 生成 AST 的过程中关于属性挂载的过程,我们写的这么多属性,是如何添加到**AST**中的?template 生成 AST 的过程中会对标签的开始标记和结束标记进行解析,在解析时,会利用正则来匹配元素中的静态属性和动态属性。
// 匹配属性,例如 id、class
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配动态属性,例如 v-if、v-else
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
并将正则匹配到信息存储到attrs中。
然后在回调 start 钩子函数进行createASTElement创建AST,并所有解析到的属性存储到attrsMap中。
{
"ref": "test-ref",
"id": "testId",
"key": "test-key",
"class": "text-clasee",
":class": "{ active: isActive, bindClass: hasBind }",
"style": "color: red",
":style": "{ color: activeColor, fontSize: fontSize + 'px' }",
"data-a": "test-a",
"data-b": "test-b"
}
然后在processElement中将所有的属性通过转换函数转换成相应的元素属性描述。class属性的转换函数是transformNode。
function transformNode (el, options) {
var warn = options.warn || baseWarn;
var staticClass = getAndRemoveAttr(el, 'class');
...
if (staticClass) {
el.staticClass = JSON.stringify(staticClass);
}
var classBinding = getBindingAttr(el, 'class', false /* getStatic */);
if (classBinding) {
el.classBinding = classBinding;
}
}
style的转换函数是transformNode$1。
function transformNode$1 (el, options) {
var warn = options.warn || baseWarn;
var staticStyle = getAndRemoveAttr(el, 'style');
if (staticStyle) {
...
el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
}
var styleBinding = getBindingAttr(el, 'style', false /* getStatic */);
if (styleBinding) {
el.styleBinding = styleBinding;
}
}
其他属性的转换函数有:
- processKey,转换节点中的 key 属性
- processRef,转换节点中的 ref 属性
- 等等…

经过转换函数的转换之后,我们 AST节点描述对象中的属性信息也就挂载完成了。
{
"type": 1,
"tag": "div",
"attrsList": [...],
"attrsMap": {
"ref": "test-ref",
"id": "testId",
"key": "test-key",
"class": "text-clasee",
":class": "{ active: isActive, bindClass: hasBind }",
"style": "color: red",
":style": "{ color: activeColor, fontSize: fontSize + 'px' }",
"data-a": "test-a",
"data-b": "test-b"
},
"rawAttrsMap": {...},
"children": [...],
"start": 0,
"end": 363,
"key": "\"test-key\"",
"plain": false,
"ref": "\"test-ref\"",
"refInFor": false,
"staticClass": "\"text-clasee\"",
"classBinding": "{ active: isActive, bindClass: hasBind }",
"staticStyle": "{\"color\":\"red\"}",
"styleBinding": "{ color: activeColor, fontSize: fontSize + 'px' }",
"attrs": [...],
"static": false,
"staticRoot": false
}
只有AST中属性信息挂载完之后才能在 generate时,将属性添加到render code中。
属性是如何解析到 render code 中的了?
在render code的生成过程中,全部逻辑都在genData$2函数中。函数就是根据 AST 元素节点的属性构造出一个 data 对象字符串,这个在后面创建 VNode 的时候的时候会作为参数传入。data 对象字符串的拼接场景会根据不同属性进行不同的操作。我列举两个。
directives
指令的解析,例如我们在节点上写了一个自定义的指令:
v-has:a:b:c={isShow}
在解析时用 genDirectives函数解析,生成指令描述的字符串。
directives:[{name:"has",rawName:"v-has:a:b:c",value:({isShow}),expression:"{isShow}",arg:"a:b:c"}]
dataGenFns
for (var i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el);
}
这里的处理主要是针对我们节点属性中的绑定 class、静态 class,绑定 style、静态style的处理。分别调用 genData函数和genData$1函数。这两个函数其实也是做的data 对象字符串的拼接。
function genData (el) {
var data = '';
if (el.staticClass) {
data += "staticClass:" + (el.staticClass) + ",";
}
if (el.classBinding) {
data += "class:" + (el.classBinding) + ",";
}
return data
}
function genData$1 (el) {
var data = '';
if (el.staticStyle) {
data += "staticStyle:" + (el.staticStyle) + ",";
}
if (el.styleBinding) {
data += "style:(" + (el.styleBinding) + "),";
}
return data
}
小结
到这里整个 genElement的解析流程也基本完成了,genElement的流程其实本身也是 generate流程的核心。当然整个流程并没有我本文写到的这么简单,篇幅有限很多细节点并没有很细致的深入,但是如有有兴趣可以自己去深入,有问题也可以和我一起交流,我们一起成长起来。
总结
整个generate也算分析完了,这也是编译三部曲的最后一步,算上前面的两篇文章,编译三部曲也全部完结了。
我们在整体来回顾一下三部曲:parse、optimize、generate。
三部曲第一步**parse** :将开发者写的 template 模板字符串转换成抽象语法树 AST ,AST 就这里来说就是一个树状结构的 JavaScript 对象,描述了这个模板,这个对象包含了每一个元素的上下文关系。
三部曲第二步**optimize** :深度遍历**parse**流程生成的 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点表示生成的 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用,提升了运行效率。
三部曲第三步**generate**:也就是本文解析,把优化后的 AST 树转换成可执行的代码,即生成 render code。 为后续 vnode生成提供基础。
到此三部曲全部都分析完毕了,这里也留下一个小问题,Vue2.x 版本的编译流程是否有什么不足了?在 Vue 3.0 版本又是如何去优化的了?欢迎评论区一起交流讨论。
参考
- https://cn.vuejs.org/v2/guide/render-function.html
- https://www.zhihu.com/question/49929356/answer/118534768
- https://github.com/meowtec/vue/tree/next-no-with
- https://zhuanlan.zhihu.com/p/93604511
- https://cn.vuejs.org/v2/api/#v-once
- https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E9%80%9A%E8%BF%87-v-once-%E5%88%9B%E5%BB%BA%E4%BD%8E%E5%BC%80%E9%94%80%E7%9A%84%E9%9D%99%E6%80%81%E7%BB%84%E4%BB%B6
- https://blog.csdn.net/Crazymryan/article/details/109234252
- https://github.com/vuejs/vue/issues/3895
- https://github.com/vuejs/vue/issues/4268
- https://www.codeprj.com/blog/51a25a1.html
- http://ask.sov5.cn/q/3Im9f9rf7B
 
 
                         
                                

