Skip to content

编译过程之generate方法

先简单复习一下Vue的编译过程,整个过程分为三个步骤:

第一步是parse解析阶段,前面的章节已经讲过parse方法的具体实现,该方法主要是将template解析成易于处理的抽象语法树(ast)。

第二步是optimize优化阶段,该阶段主要是将template中的静态节点在ast中标记出来,方便后续生成虚拟Dom的时候进行复用。

第三步是generate生成代码阶段,也是我们这章要学习的部分。该阶段是将ast树转换成能生成对应虚拟Domrender函数。

img

这一章我们主要讨论generate阶段,看看抽象语法树是如何转换成render函数的。

with语法

我们先看下最终生成的代码代表的是什么意思:

javascript
with(this){return _c('div',[_v("编译")])}

从生成的代码可以看出,render函数是以with开头的,我们先了解下with是如何使用的。

javascript
const obj = { a: 3 }
with (obj) {
  console.log(a) // 3
  console.log(this.a) // undefined
  console.log(b) // 报错
}

借用mdn上的一句话:with语句扩展一个语句的作用域链。本来访问a的时候是访问的全局作用域,加了with之后,访问a的作用域则是objwith使用起来相对比较简单。回到Vuerender函数中,可以知道render函数里的作用域就是Vue的实例vm,所以render执行的时候,访问变量实质上就是访问Vue实例上的属性。至于为什么要使用with语法,可以看下尤大大的一篇回答

renderHelpers

还是那段编译生成的代码,在with内部的代码中有_c_v这样的方法。由于this是指向Vue实例的,所以访问_c的时候其实访问的是vm._c。但是_c_v这些方法是哪里定义的呢?

javascript
with(this){return _c('div',[_v("编译")])}

这就要追溯到Vue构造函数与原型的章节了,在src/core/instance/render.js文件中找到renderMixin方法,这个方法是在我们学习Vue原型设计的时候提到的:

javascript
export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)
  Vue.prototype.$nextTick = function (fn: Function) {...}
  Vue.prototype._render = function (): VNode {...}
}

./render-helpers找到installRenderHelpers函数:

javascript
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
  target._d = bindDynamicKeys
  target._p = prependModifier
}

installRenderHelpers主要任务就是挂载了渲染需要用到的函数!所以调用_v就是调用createTextVNode方法,从字面意思我们就可以知道,其实它就是创建一个文本的Vnode节点。但是这里并没有定义_c,那么_c在哪呢?还记得数据初始化环节吗,同样是在src/core/instance/render.js文件中,不过是initRender方法:

javascript
export function initRender (vm: Component) {
  ...
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
 ...
}

_c同样是创建Vnode节点的函数。所以render函数就是由一堆生成Vnode节点函数的集合。接下来我们看下具体是如何使用这些函数的。

generate

generate过程的代码都是在src/compiler/codegen目录中,里面只有两个文件,相对来说比较少。在index.js中找到generate函数:

javascript
export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast 
   ? (ast.tag === 'script' ? 'null' 
   : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

里面的核心实现是调用了genElement方法:

javascript
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 {...}

可以看出,genElement里面有很多判断逻辑,根据不同的判断条件调用不同的生成函数。我们分别看下各个生成函数是怎样的。

genStatic

staticRoot是在optimize优化阶段添加的。当el.staticRoottrue时,代表el是静态节点:

javascript
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)

return `_m(${
  state.staticRenderFns.length - 1
}${
  el.staticInFor ? ',true' : ''
})`

这里主要的代码就两行。with(this){return ${genElement(el, state)}}就是将el生成Vnode的代码,然后将其添加到staticRenderFns中,再使用_m函数进行封装一遍,_m函数又是调用的renderStatic函数:

javascript
const cached = this._staticTrees || (this._staticTrees = [])
let tree = cached[index]
if (tree && !isInFor) {
  return tree
}

tree = cached[index] = this.$options.staticRenderFns[index].call(
  this._renderProxy,
  null,
  this // for render fns generated for functional component templates
)
markStatic(tree, `__static__${index}`, false)
return tree

通过代码不难看出,该方法将staticRenderFns[index].call()生成的Vnode缓存到_staticTrees上了。_staticTrees键为index,值为生成的Vnode,下次执行renderStatic函数的时候,就会从_staticTrees取出相应的Vnode,从而跳过执行staticRenderFns[index].call(),达到优化编译速度的目的。另外这里的_staticTrees也是在initRender的时候初始化的。

genFor

javascript
return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'

genFor方法比较简单,主要是调用了_l方法(即renderList方法),将解析的exp,alias,iterator1,iterator2作为参数传入,然后循环exp调用genElement生成Vnode数组。

genIf/genIfConditions

parse解析template的时候,会将if/else-if/else三种情况解析成一个数组,类似于下方这样的形式:

javascript
// 解析前
<div v-if="isTrue">编译1</div>
<div v-else-if="isFalse">编译2</div>
<div v-else>编译3</div>

// 解析后
[
  {
    "exp": "isTrue",
    "block": "存放isTrue为true的时候显示的 el"
  },
  {
    "exp": "isFalse",
    "block": "存放isFalse为true的时候显示的 el"
  },
  {
    "block": "存放其他情况显示的 el
  }
]

所以我们需要遍历这个数组,然后依次判断exp是否为true,如果为true就显示block里存放的el。回到genIf函数,它调用的则是genIfConditions函数:

javascript
const condition = conditions.shift()
if (condition.exp) {
  return `(${condition.exp})?${
    genTernaryExp(condition.block)
  }:${
    genIfConditions(conditions, state, altGen, altEmpty)
  }`
} else {
  return `${genTernaryExp(condition.block)}`
}

genIfConditions每次会从数组中取一个条件出来,判断是否成立。如果不成立就递归调用genIfConditions,直到所有的判断条件取完。

genOnce

genOnce主要分为三种情况:

  • if并存,这个时候就先执行genIf再执行genOnce
  • for循环中且为静态节点,用_o方法进行标记。
  • 否则使用genStatic方法生成节点,genStatic具有缓存性。

genChildren/genNode

genChildren是一个递归的过程,通过遍历children节点然后调用genNode方法生成Vnode:

javascript
if (node.type === 1) {
  return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
  return genComment(node)
} else {
  return genText(node)
}

如果是node.type为1,那么又会回到genElement方法继续生成Vnode。否则生成文本节点或注释节点。

genComponent

javascript
const children = el.inlineTemplate ? null : genChildren(el, state, true)
return `_c(${componentName},${genData(el, state)}${
    children ? `,${children}` : ''
  })`

genComponent比较简单,和普通节点生成类似,不同点则是标签名称是组件名称。

genData

genData的主要作用是处理属性相关的内容,如指令、属性、props、绑定事件等等,它们分别通过genDirectivesgenPropsgenHandlers等函数进行分别处理的。这里主要看一下slot相关的方法:genScopedSlots

javascript
// 编译前
<div>
  <template v-slot:header="header">编译header</template>
  <template v-slot:footer="footer">编译footer</template>
</div>

// 编译后 ast 大致内容
{
  scopedSlots: {
    header: {
      ...
      节点header
    },
    footer: {
      ...
      节点footer
    }
  }
}

genScopedSlots则是遍历scopedSlots生成节点:

javascript
const generatedSlots = Object.keys(slots)
  .map(key => genScopedSlot(slots[key], state))
  .join(',')

单个节点生成使用的是genScopedSlot方法:

javascript
function genScopedSlot (
  el: ASTElement,
  state: CodegenState
): string {
 ...
  const fn = `function(${slotScope}){` +
    `return ${el.tag === 'template'
      ? el.if && isLegacySyntax
        ? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
        : genChildren(el, state) || 'undefined'
      : genElement(el, state)
    }}`
  ...
  return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
}

该方法的作用是将插槽内要插入的children生成了Vnode,最终的生成结果如下:

javascript
{
  scopedSlots: _u([
    {
      key: 'header',
      fn: function (header) {
        return [_v('编译1')]
      }
    },
    {
      key: 'footer',
      fn: function (footer) {
        return [_v('编译2')]
      }
    }
  ])
}

_u方法调用的是resolveScopedSlots方法,该方法主要将上面的数组形式转换成了对象形式,如:

javascript
{
 header: function (header) {
        return [_v('编译1')]
      },
 footer: function (footer) {
        return [_v('编译2')]
      }
}

genSlot

genSlot的核心过程是使用了_t方法:

javascript
let res = `_t(${slotName}${children ? `,${children}` : ''}`

我们看一下_t也就是renderSlot方法:

javascript
// $scopeSlots 是父组件中定义的 slot,这里取到对应的 Vnode 或 生成 Vnode 的函数
const scopedSlotFn = this.$scopedSlots[name]
let nodes
if (scopedSlotFn) { // scoped slot
  // props 为在 slot 标签上定义的 属性
  props = props || {}
  // bindObject 为 slot 上使用 v-bind 的属性
  if (bindObject) {
    if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
      warn(
        'slot v-bind without argument expects an Object',
        this
      )
    }
    props = extend(extend({}, bindObject), props)
  }
  // 传入参数,使得父组件中 v-slot 能够接受参数
  nodes = scopedSlotFn(props) || fallback
} else {
  // 直接使用 vnode
  nodes = this.$slots[name] || fallback
}

const target = props && props.slot
if (target) {
  return this.$createElement('template', { slot: target }, nodes)
} else {
  return nodes
}

首先this.$scopedSlots[name]就是拿到父组件中可以将插槽内容生成Vnode的函数,然后执行生成Vnode数组,添加到子组件中。这里还有其他判断,主要对应于是否是具名插槽,是否有插槽传值等情况。这个在后面插槽章节再详细讲解,这里先简单了解一下。

总结

编译过程中generate的阶段,实际上就是将抽象语法树根据不同的情景使用不同的render工具方法拼接成可以生成Vnoderender函数。类似于for/if这些语法,经过generate处理后,都会根据相应的数据逻辑生成对应的Vnode。事实上,这个Vnode的结构和我们真实Dom的结构相差无几,下一步要做的就是将虚拟Dom映射成真实Dom了。