对编译过程的了解会让我们对 Vue 的指令、内置组件等有更好的理解。不过由于编译的过程是一个相对复杂的过程,我们只要求理解整体的流程、输入和输出即可,对于细节我们不必抠太细。由于篇幅较长,这里会用三篇文章来讲 Vue 的编译。这是第三篇,**render code 生成**

前言

前面两篇文章分别分享了 Vue 编译三部曲的前两曲:「 parse,template 转换为 AST」,「optimize,模型树优化」。
我们先简单回顾一下parseoptimize
parse 将开发者写的 template 模板字符串转换成抽象语法树 AST ,AST 就这里来说就是一个树状结构的 JavaScript 对象,描述了这个模板,这个对象包含了每一个元素的上下文关系。那么整个 parse 的过程是利用很多正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。
optimize 就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点表示生成的 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用,提升了运行效率。
而编译的最后一步就是把优化后的 AST 树转换成可执行的代码,即 generate生成 render code。 再执行 render渲染函数生成 vnode
Vue 编译三部曲:render code 生成 - 图1
接下来我们来看看 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 为例:

  1. <div>
  2. <h1>My title</h1>
  3. Some text content
  4. <!-- TODO: Add tagline -->
  5. </div>

上述 HTML 对应的 DOM 节点树如下图所示:
image.png
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。在之前的编译三部曲第一步中我们也介绍了,template生成 AST时,会把元素、文字、注释都创建成节点描述对象

  • type = 1基础元素节点
  • type = 2含有expressiontokens文本节点
  • 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 };

  1. 每一个节点都是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点、兄弟节点 (也就是说每个部分可以包含其它的一些部分)。<br />高效地更新所有这些节点会是比较困难的,不过所幸你不必手动完成这个工作。你只需要告诉 Vue 你希望页面上的 HTML 是什么,这可以是在一个模板里:
  2. ```javascript
  3. <h1>{{ blogTitle }}</h1>

或者一个渲染函数里:

  1. render: function (createElement) {
  2. return createElement('h1', this.blogTitle)
  3. }

在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle 发生了改变。
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:

  1. return createElement('h1', this.blogTitle)

createElement到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

createElement

接下来我们看看createElement,在 createElement函数中可以传递标签名、属性和子节点。子节点又可以传递createElement

  1. // @returns {VNode}
  2. createElement(
  3. // {String | Object | Function}
  4. // 一个 HTML 标签名、组件选项对象,或者
  5. // resolve 了上述任何一种的一个 async 函数。必填项。
  6. 'div',
  7. // {Object}
  8. // 一个与模板中 attribute 对应的数据对象。可选。
  9. {
  10. ...
  11. },
  12. // {String | Array}
  13. // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  14. // 也可以使用字符串来生成“文本虚拟节点”。可选。
  15. [
  16. '先写一些文字',
  17. createElement('h1', '一则头条'),
  18. createElement(MyComponent, {
  19. props: {
  20. someProp: 'foobar'
  21. }
  22. })
  23. ]
  24. )

这种结构是否似曾相识,如果你还记得 template 编译成AST后的结构,你会觉得它们是如此的类似。
image.png
这也呈上了为什么说「渲染函数,更加接近渲染器」。
而本文的重点generate编译器,它的作用和目的就是将 AST 转换为渲染函数。接下来我们就一起走进 generate 源码世界。

generate

我们先举一个例子,用一个简单的示例来看看 AST进过 generate 之后生成的render code到底长什么样?
例如有这样一段模块:

  1. data: {
  2. isShow: true,
  3. list: [
  4. '小白',
  5. '小黄',
  6. '小黑',
  7. '小绿'
  8. ],
  9. }
  10. <div>
  11. <ul class="list" v-if="isShow">
  12. <li
  13. v-for="(item, index) in list"
  14. @click="clickItem(item)"
  15. >
  16. {{item}}:{{index}}
  17. </li>
  18. </ul>
  19. </div>

它经过编译,执行const code = generate(ast, options),生成的 render code就会是如下这样一个字符串,注意这是一个字符串,只是为了方便大家阅读,我进行了格式化。

  1. with (this) {
  2. return _c('div', [
  3. isShow
  4. ? _c(
  5. 'ul',
  6. { staticClass: 'list' },
  7. _l(list, function (item, index) {
  8. return _c(
  9. 'li',
  10. {
  11. on: {
  12. click: function ($event) {
  13. return clickItem(item);
  14. },
  15. },
  16. },
  17. [_v('\n ' + _s(item) + ':' + _s(index) + '\n ')],
  18. );
  19. }),
  20. 0,
  21. )
  22. : _e(),
  23. ]);
  24. }

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

  1. 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`中。

  1. export function installRenderHelpers (target: any) {
  2. target._o = markOnce
  3. target._n = toNumber
  4. target._s = toString
  5. target._l = renderList
  6. target._t = renderSlot
  7. target._q = looseEqual
  8. target._i = looseIndexOf
  9. target._m = renderStatic
  10. target._f = resolveFilter
  11. target._k = checkKeyCodes
  12. target._b = bindObjectProps
  13. target._v = createTextVNode
  14. target._e = createEmptyVNode
  15. target._u = resolveScopedSlots
  16. target._g = bindObjectListeners
  17. }

好,接下来,我们回到 generate 源码中来。

generate 函数

  1. const code = generate(ast, options)
  1. function generate (
  2. ast,
  3. options
  4. ) {
  5. var state = new CodegenState(options);
  6. var code = ast ? genElement(ast, state) : '_c("div")';
  7. return {
  8. render: ("with(this){return " + code + "}"),
  9. staticRenderFns: state.staticRenderFns
  10. }
  11. }

进入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

    1. var CodegenState = function CodegenState (options) {
    2. this.options = options;
    3. this.warn = options.warn || baseWarn;
    4. this.transforms = pluckModuleFunction(options.modules, 'transformCode');
    5. this.dataGenFns = pluckModuleFunction(options.modules, 'genData');
    6. this.directives = extend(extend({}, baseDirectives), options.directives);
    7. var isReservedTag = options.isReservedTag || no;
    8. this.maybeComponent = function (el) { return !!el.component || !isReservedTag(el.tag); };
    9. this.onceId = 0;
    10. this.staticRenderFns = [];
    11. this.pre = false;
    12. };

    接下来就是最重要的一步是,生成render code string

    1. var code = ast ? genElement(ast, state) : '_c("div")';

    AST就调用 genElement函数,没有的话,默认就创建一个div。然后我们来重点看看 genElement干了些什么?

    genElement

    Vue 编译三部曲:render code 生成 - 图4
    当调用 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渲染函数所需对应的参数格式

    1. function genElement (el, state) {
    2. // ①
    3. if (el.parent) {
    4. el.pre = el.pre || el.parent.pre;
    5. }
    6. // ②
    7. if (el.staticRoot && !el.staticProcessed) {
    8. return genStatic(el, state)
    9. // ③
    10. } else if (el.once && !el.onceProcessed) {
    11. return genOnce(el, state)
    12. // ④
    13. } else if (el.for && !el.forProcessed) {
    14. return genFor(el, state)
    15. // ⑤
    16. } else if (el.if && !el.ifProcessed) {
    17. return genIf(el, state)
    18. // ⑥
    19. } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    20. return genChildren(el, state) || 'void 0'
    21. // ⑦
    22. } else if (el.tag === 'slot') {
    23. return genSlot(el, state)
    24. } else {
    25. // component or element
    26. var code;
    27. if (el.component) {
    28. code = genComponent(el.component, el, state);
    29. } else {
    30. var data;
    31. if (!el.plain || (el.pre && state.maybeComponent(el))) {
    32. data = genData$2(el, state);
    33. }
    34. var children = el.inlineTemplate ? null : genChildren(el, state, true);
    35. code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
    36. }
    37. // module transforms
    38. for (var i = 0; i < state.transforms.length; i++) {
    39. code = state.transforms[i](el, code);
    40. }
    41. return code
    42. }
    43. }

    genStatic

    1. function genStatic (el, state) {
    2. el.staticProcessed = true;
    3. if (el.pre) {
    4. state.pre = el.pre;
    5. }
    6. // 将静态元素添加到 staticRenderFns 中
    7. state.staticRenderFns.push(("with(this){return " + (genElement(el, state)) + "}"));
    8. state.pre = originalPreState;
    9. return ("_m(" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")")
    10. }

    用于静态元素的 render code生成,生成的render code是一个 _m的函数字符串。
    为了方便理解举一个例子如下:

    1. <div>
    2. <div><span>一则头条</span></div>
    3. <div><span>{{text}}</span></div>
    4. <div><span>一则头条</span></div>
    5. </div>

    经过parse后,生成这样的一段 AST 描述对象。
    image.png
    再经过generate后,就会生成如下的 render code

    1. _m(0)
    2. _m(1)

    这里会发现,为什么函数字符串有一个数字?
    这是因为在执行renderStatic函数时,也就是生成静态元素 vnode时,会从staticRenderFns数组中读取序列元素,staticRenderFns存放的是静态节点的 render 函数
    上面的模板示例,就会生成这样一个 staticRenderFns

    1. staticRenderFns = [
    2. "with(this){return _c('div',[_c('span',[_v(\"一则头条\")])])}",
    3. "with(this){return _c('div',[_c('span',[_v(\"一则头条\")])])}"
    4. ]

    当生成 render 时是就会读取指定的渲染字符串。

    1. function renderStatic (
    2. index,
    3. isInFor
    4. ) {
    5. var cached = this._staticTrees || (this._staticTrees = []);
    6. var tree = cached[index];
    7. // 如果缓存存在,就直接返回
    8. if (tree && !isInFor) {
    9. return tree
    10. }
    11. // 这里是执行 render 的地方,读取 staticRenderFns 对应的静态元素
    12. tree = cached[index] = this.$options.staticRenderFns[index].call(
    13. this._renderProxy,
    14. null,
    15. this
    16. );
    17. markStatic(tree, ("__static__" + index), false);
    18. return tree
    19. }

    并且最后将我们静态节点,放入到 Vue 实例_staticTrees中。
    image.png

    genOnce

    1. function genOnce (el, state) {
    2. el.onceProcessed = true;
    3. if (el.if && !el.ifProcessed) {
    4. return genIf(el, state)
    5. } else if (el.staticInFor) {
    6. ...
    7. if (!key) {
    8. return genElement(el, state)
    9. }
    10. return ("_o(" + (genElement(el, state)) + "," + (state.onceId++) + "," + key + ")")
    11. } else {
    12. return genStatic(el, state)
    13. }
    14. }

    用于生成包含v-once元素的 render code,生成的render code是一个 _o的函数字符串。genOnce函数本身逻辑会根据其他的元素属性来做处理。

  • 如果包含属性 v-if就会将逻辑分发到 genIf

  • 如果是一个静态节点包含在for循环中,就会生成_o的函数字符串
  • 除开上面的情况就会当做静态节点处理

v-once只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once attribute 以确保这些内容只计算一次然后缓存起来。
不过,请试着不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。

genFor

  1. function genFor (
  2. el,
  3. state,
  4. altGen,
  5. altHelper
  6. ) {
  7. var exp = el.for;
  8. var alias = el.alias;
  9. var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';
  10. var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';
  11. ...
  12. el.forProcessed = true; // avoid recursion
  13. return (altHelper || '_l') + "((" + exp + ")," +
  14. "function(" + alias + iterator1 + iterator2 + "){" +
  15. "return " + ((altGen || genElement)(el, state)) +
  16. '})'
  17. }

用于生成节点存在循环的render code,生成的render code一个_l的函数字符串。_l源码中对应的是 renderList函数。
为了方便理解举一个例子如下:

  1. <div>
  2. <ul v-for="(item, index, arr) in list">
  3. <li>{{ item }}</li>
  4. </ul>
  5. </div>

经过parse后,生成这样的一段 AST 描述对象。包含了一些genFor需要用到的信息,我进行了标记。
image.png
再经过generate后,就会生成如下的 render code(为方便阅读,render code已被格式化过)。

  1. _l(list, function (item, index, arr) {
  2. return _c('ul', [_c('li', [_v(_s(item))])]);
  3. });

genFor 的处理逻辑很简单,从 AST 元素节点中获取了和 for相关的一些属性,然后拼接成一个代码字符串。

genIf

  1. function genIf (
  2. el,
  3. state,
  4. altGen,
  5. altEmpty
  6. ) {
  7. el.ifProcessed = true;
  8. return genIfConditions(el.ifConditions.slice(), ...)
  9. }

用于生成节点存在条件判断的render codegenIf 主要是通过执行 genIfConditions来执行,获取节点信息中 ifConditions列表进行解析。ifConditions是一个存储条件判断相关信息的数组。这个ifConditions是怎么来的了?来源于编译的第一步中optimize解析标签时,如果节点存在属性v-if、v-else-if、v-else时,就会将表达式和节点的信息分析放入存储到ifConditions中。
为了方便理解,举一个小例子:

  1. <div>
  2. <div v-if="isShow === 1">1</div>
  3. <div v-else-if="isShow === 2">2</div>
  4. <div v-else>3</div>
  5. </div>

有这样一段模板代码,在解析的过程中就会将v-if、v-else-if、v-else分成抽离成一个含有表达式和节点信息的对象,存储到 ifConditions 中,如下所示,这样在genIfConditions执行时,就可以在ifConditions中去读取节点和属性表达式的相关信息。

  1. ifConditions = [
  2. {exp: 'isShow === 1', block: ...},
  3. {exp: 'isShow === 2', block: ...},
  4. {exp: undefined, block: ...}
  5. ]

image.png
接着回来,看看genIfConditions中具体是怎么利用存储在ifConditions中的信息。

  1. function genIfConditions (
  2. conditions,
  3. state,
  4. altGen,
  5. altEmpty
  6. ) {
  7. ...
  8. //
  9. var condition = conditions.shift();
  10. if (condition.exp) {
  11. return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions, state, altGen, altEmpty)))
  12. } else {
  13. return ("" + (genTernaryExp(condition.block)))
  14. }
  15. // v-if with v-once should generate code like (a)?_m(0):_m(1)
  16. function genTernaryExp (el) {
  17. return altGen
  18. ? altGen(el, state)
  19. : el.once
  20. ? genOnce(el, state)
  21. : genElement(el, state)
  22. }
  23. }

conditions 就是 ifConditions

它是依次从 conditions 获取第一个元素信息,然后通过对 condition.exp去生成一段三元运算符的代码,: 后是递归调用 genIfConditions,这样如果有多个 conditions,就生成多层嵌套的三元运算逻辑。
上面的例子就会生成一个嵌套的三元运算逻辑字符串表达式(为方便阅读,render code已被格式化过)。

  1. (isShow === 1)
  2. ? _c('div',[_v("1")])
  3. : (isShow === 2)
  4. ? _c('div',[_v("2")])
  5. : _c('div',[_v("3")])

genChildren

  1. function genChildren (
  2. el,
  3. state,
  4. checkSkip,
  5. altGenElement,
  6. altGenNode
  7. ) {
  8. var children = el.children;
  9. if (children.length) {
  10. ...
  11. return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
  12. }
  13. }

用于生成子级虚拟节点信息字符串。核心关键就是这样一段代码。

  1. '[' +
  2. children
  3. .map(function (c) {
  4. return gen(c, state);
  5. })
  6. .join(',') +
  7. ']' +
  8. (normalizationType$1 ? ',' + normalizationType$1 : '');

生成返回字符串格式数组对象children是根节点(相对)下子级节点的数组对象。通过 map再对子级节点进行字符串格式的处理。
对子节点的处理调用 gen函数,也就是源码中对于的 genNode函数。

  1. function genNode (node, state) {
  2. if (node.type === 1) {
  3. return genElement(node, state)
  4. } else if (node.type === 3 && node.isComment) {
  5. return genComment(node)
  6. } else {
  7. return genText(node)
  8. }
  9. }

genNode函数中:

  • 当是元素节点时递归调用genElement() 函数
  • 当是注释节点时调用genComment 函数
  • 当是文本节点时调用genText 函数

genComment
genComment 函数逻辑很简单,把注释JSON 成字符串,包含在 _e的函数字符串中。

  1. function genComment (comment) {
  2. return ("_e(" + (JSON.stringify(comment.text)) + ")")
  3. }

genText
genText 函数会处理含有表达式的文本或者是纯文本。表达式的文本将text.expression包裹在_v函数字符串中,纯文本就格式化后包裹在_v函数字符串中。

纯文本的处理会将文本中的\u2028-行分隔符\u2029-段落分隔符进行全局转义。原因是这两个特殊字符会导致程序报错。 详情请阅读 issues:

  1. function genText (text) {
  2. return ("_v(" + (text.type === 2
  3. ? text.expression
  4. : transformSpecialNewlines(JSON.stringify(text.text))) + ")")
  5. }
  6. function transformSpecialNewlines (text) {
  7. return text
  8. .replace(/\u2028/g, '\\u2028')
  9. .replace(/\u2029/g, '\\u2029')
  10. }

为了加深理解,举一个小例子:

  1. <div>
  2. <template>
  3. <!-- 注释 -->
  4. <div>{{text}}</div>
  5. </template>
  6. </div>

上面这段模板经过parse后,生成这样的一段 AST 描述对象。
image.png
再经过generate后,就会生成如下的 render code(为方便阅读,render code已被格式化过)。也就是上文说的生成字符串格式数组对象

  1. [_e(' 注释 '), _v(' '), _c('div', [_v(_s(text))])];

genData

  1. var code;
  2. if (el.component) {
  3. code = genComponent(el.component, el, state);
  4. } else {
  5. var data;
  6. if (!el.plain || (el.pre && state.maybeComponent(el))) {
  7. data = genData$2(el, state);
  8. }
  9. var children = el.inlineTemplate ? null : genChildren(el, state, true);
  10. code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
  11. }
  12. for (var i = 0; i < state.transforms.length; i++) {
  13. code = state.transforms[i](el, code);
  14. }
  15. return code

这里发现一个有意思的属性 el.plain,如果标签既没有使用特性key,又没有任何属性,那么该标签的元素描述对象的 plain 属性将始终为true”。这个属性是在 processElement 阶段给 ast 对象进行的扩展。

当节点不是staticRoot没有once没有if没有for不是 template不是 slot ,那就会走到最后的判断逻辑中,在此检测当前元素是否为组件,如果是调用 genComponent函数并且返回生成虚拟dom渲染函数所需对应的参数格式。
这里有三个重点逻辑。

  • 组件,genComponent函数调用
  • genData对元素属性解析
  • genChildren对元素的子元素解析

对于 genComponent函数内部其实就是对 genDatagenChildren的封装处理,一个组件本身其实也就是很多元素的集合,只是这些元素套在了一个名为某某某的组件内部。所以用genComponentgenDatagenChildren封装也就想得通了。其中genChildren中又会递归调用genElement来处理元素。最后生成对于的 render code

  1. function genComponent (
  2. componentName,
  3. el,
  4. state
  5. ) {
  6. var children = el.inlineTemplate ? null : genChildren(el, state, true);
  7. return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
  8. }

genChildren上一小结已经讲过,所以这里就把重点放在genData上。
在 Vue 中一个 VNode代表一个虚拟节点,而该节点的虚拟属性信息用 VNodeData描述。而VNodeData的生成就是用 genData函数来实现的。
genData 函数就是根据 AST 元素节点的属性构造出一个data 对象字符串,这个在后面创建 VNode 的时候的时候会作为参数传入。

Vue 中对处理节点属性其实有三个 genData 函数。分别是 **genData****genData$1****genData$2**。三个函数分别处理不同类型的节点属性。
在 Vue 中可以使用 绑定 Class 与 绑定 Style来生成动态class 列表内联样式。在源码中genData的作用就是处理静态的class绑定的 classgenData$1用来处理静态的 style绑定的 stylegenData$2用来处理其他属性。
为了便于理解,我引入一个例子作为下面解析的用例:

  1. <div
  2. ref="test-ref"
  3. id="testId"
  4. key="test-key"
  5. class="text-clasee"
  6. :class="{ active: isActive, bindClass: hasBind }"
  7. style="color: red"
  8. :style="{ color: activeColor, fontSize: fontSize + 'px' }"
  9. data-a="test-a"
  10. data-b="test-b"
  11. >
  12. {{text}}
  13. </div>

上面这段模板经过parse后,生成这样的一段 AST 描述对象。
image.png
再经过generate后,就会生成如下的 render code(为方便阅读,render code已被格式化过)。

  1. _c(
  2. 'div',
  3. {
  4. key: 'test-key',
  5. ref: 'test-ref',
  6. staticClass: 'text-clasee',
  7. class: { active: isActive, bindClass: hasBind },
  8. staticStyle: { color: 'red' },
  9. style: { color: activeColor, fontSize: fontSize + 'px' },
  10. attrs: { id: 'testId', 'data-a': 'test-a', 'data-b': 'test-b' },
  11. },
  12. [_v('\n ' + _s(text) + '\n ')],
  13. );

上面的示例告诉了我们属性生成的开头和结尾,接下来我们来看看中间过程。

属性是如何添加到 AST 中的了?

在这之前我们先来回忆一下之前 template 生成 AST 的过程中关于属性挂载的过程,我们写的这么多属性,是如何添加到**AST**中的?
template 生成 AST 的过程中会对标签的开始标记和结束标记进行解析,在解析时,会利用正则来匹配元素中的静态属性和动态属性。

  1. // 匹配属性,例如 id、class
  2. var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
  3. // 匹配动态属性,例如 v-if、v-else
  4. var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

并将正则匹配到信息存储到attrs中。
image.png
然后在回调 start 钩子函数进行createASTElement创建AST,并所有解析到的属性存储到attrsMap中。

  1. {
  2. "ref": "test-ref",
  3. "id": "testId",
  4. "key": "test-key",
  5. "class": "text-clasee",
  6. ":class": "{ active: isActive, bindClass: hasBind }",
  7. "style": "color: red",
  8. ":style": "{ color: activeColor, fontSize: fontSize + 'px' }",
  9. "data-a": "test-a",
  10. "data-b": "test-b"
  11. }

然后在processElement将所有的属性通过转换函数转换成相应的元素属性描述。
class属性的转换函数是transformNode

  1. function transformNode (el, options) {
  2. var warn = options.warn || baseWarn;
  3. var staticClass = getAndRemoveAttr(el, 'class');
  4. ...
  5. if (staticClass) {
  6. el.staticClass = JSON.stringify(staticClass);
  7. }
  8. var classBinding = getBindingAttr(el, 'class', false /* getStatic */);
  9. if (classBinding) {
  10. el.classBinding = classBinding;
  11. }
  12. }

style的转换函数是transformNode$1

  1. function transformNode$1 (el, options) {
  2. var warn = options.warn || baseWarn;
  3. var staticStyle = getAndRemoveAttr(el, 'style');
  4. if (staticStyle) {
  5. ...
  6. el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
  7. }
  8. var styleBinding = getBindingAttr(el, 'style', false /* getStatic */);
  9. if (styleBinding) {
  10. el.styleBinding = styleBinding;
  11. }
  12. }

其他属性的转换函数有:

  • processKey,转换节点中的 key 属性
  • processRef,转换节点中的 ref 属性
  • 等等…

Vue 编译三部曲:render code 生成 - 图12
经过转换函数的转换之后,我们 AST节点描述对象中的属性信息也就挂载完成了。

  1. {
  2. "type": 1,
  3. "tag": "div",
  4. "attrsList": [...],
  5. "attrsMap": {
  6. "ref": "test-ref",
  7. "id": "testId",
  8. "key": "test-key",
  9. "class": "text-clasee",
  10. ":class": "{ active: isActive, bindClass: hasBind }",
  11. "style": "color: red",
  12. ":style": "{ color: activeColor, fontSize: fontSize + 'px' }",
  13. "data-a": "test-a",
  14. "data-b": "test-b"
  15. },
  16. "rawAttrsMap": {...},
  17. "children": [...],
  18. "start": 0,
  19. "end": 363,
  20. "key": "\"test-key\"",
  21. "plain": false,
  22. "ref": "\"test-ref\"",
  23. "refInFor": false,
  24. "staticClass": "\"text-clasee\"",
  25. "classBinding": "{ active: isActive, bindClass: hasBind }",
  26. "staticStyle": "{\"color\":\"red\"}",
  27. "styleBinding": "{ color: activeColor, fontSize: fontSize + 'px' }",
  28. "attrs": [...],
  29. "static": false,
  30. "staticRoot": false
  31. }

只有AST中属性信息挂载完之后才能在 generate时,将属性添加到render code中。

属性是如何解析到 render code 中的了?

render code的生成过程中,全部逻辑都在genData$2函数中。函数就是根据 AST 元素节点的属性构造出一个 data 对象字符串,这个在后面创建 VNode 的时候的时候会作为参数传入。
data 对象字符串的拼接场景会根据不同属性进行不同的操作。我列举两个。

directives

指令的解析,例如我们在节点上写了一个自定义的指令:

  1. v-has:a:b:c={isShow}

在解析时用 genDirectives函数解析,生成指令描述的字符串。

  1. directives:[{name:"has",rawName:"v-has:a:b:c",value:({isShow}),expression:"{isShow}",arg:"a:b:c"}]

dataGenFns
  1. for (var i = 0; i < state.dataGenFns.length; i++) {
  2. data += state.dataGenFns[i](el);
  3. }

这里的处理主要是针对我们节点属性中的绑定 class、静态 class,绑定 style、静态style的处理。分别调用 genData函数和genData$1函数。这两个函数其实也是做的data 对象字符串的拼接。

  1. function genData (el) {
  2. var data = '';
  3. if (el.staticClass) {
  4. data += "staticClass:" + (el.staticClass) + ",";
  5. }
  6. if (el.classBinding) {
  7. data += "class:" + (el.classBinding) + ",";
  8. }
  9. return data
  10. }
  1. function genData$1 (el) {
  2. var data = '';
  3. if (el.staticStyle) {
  4. data += "staticStyle:" + (el.staticStyle) + ",";
  5. }
  6. if (el.styleBinding) {
  7. data += "style:(" + (el.styleBinding) + "),";
  8. }
  9. return data
  10. }

Vue 编译三部曲:render code 生成 - 图13

小结

到这里整个 genElement的解析流程也基本完成了,genElement的流程其实本身也是 generate流程的核心。当然整个流程并没有我本文写到的这么简单,篇幅有限很多细节点并没有很细致的深入,但是如有有兴趣可以自己去深入,有问题也可以和我一起交流,我们一起成长起来。

总结

整个generate也算分析完了,这也是编译三部曲的最后一步,算上前面的两篇文章,编译三部曲也全部完结了。
Vue 编译三部曲:render code 生成 - 图14
我们在整体来回顾一下三部曲:parseoptimizegenerate
三部曲第一步**parse**将开发者写的 template 模板字符串转换成抽象语法树 AST ,AST 就这里来说就是一个树状结构的 JavaScript 对象,描述了这个模板,这个对象包含了每一个元素的上下文关系。
三部曲第二步**optimize**深度遍历**parse**流程生成的 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点表示生成的 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用,提升了运行效率。
三部曲第三步**generate**也就是本文解析把优化后的 AST 树转换成可执行的代码,即生成 render code。 为后续 vnode生成提供基础。

到此三部曲全部都分析完毕了,这里也留下一个小问题,Vue2.x 版本的编译流程是否有什么不足了?在 Vue 3.0 版本又是如何去优化的了?欢迎评论区一起交流讨论。

参考