对编译过程的了解会让我们对 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