对编译过程的了解会让我们对 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"><liv-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 = markOncetarget._n = toNumbertarget._s = toStringtarget._l = renderListtarget._t = renderSlottarget._q = looseEqualtarget._i = looseIndexOftarget._m = renderStatictarget._f = resolveFiltertarget._k = checkKeyCodestarget._b = bindObjectPropstarget._v = createTextVNodetarget._e = createEmptyVNodetarget._u = resolveScopedSlotstarget._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 elementvar 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 transformsfor (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 recursionreturn (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用来处理其他属性。
为了便于理解,我引入一个例子作为下面解析的用例:
<divref="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、classvar attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;// 匹配动态属性,例如 v-if、v-elsevar 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
