代码生成器的作用是将AST转换成渲染函数中的内容,也就是代码字符串,代码字符串被渲染函数执行createElement后生成VNode,虚拟DOM通过VNode的渲染视图。

举个例子:

模板

<div id="el">Hello {{name}}</div>

转成AST后:

{
  'type': 1,
  'tag': 'div',
  'attrsList': [
    {
      'name': 'id',
      'value': 'el'
    }
  ],
  'attrsMap': {
    'id': 'el'
  },
  'children': [
    {
      'type': 2,
      'expression': '"Hello "+_s(name)',
      'text': 'Hello {{name}}',
      'static': false
    }
  ],
  'plain': false,
  'attrs': [
    {
      'name': 'id',
      'value': '"el"'
    }
  ],
  'static': false,
  'staticRoot': false
}

生成代码字符串后:

'with(this){return _c("div",{attrs:{"id":"el"}},[_v("Hello "+_s(name))])}'

# 通过AST生成代码字符串

类型 创建方法 别名
元素节点 createElement _c
文本节点 createTextVNode _v
注释节点 createEmptyVNode _e

例子:

<div id="el">
  <div>
    <p>Hello {{name}}</p>
  </div>
</div>

生成的格式:

_c('div',{attrs:{"id":"el"}},[_c('div',[_c('p',[_v("Hello "+_s(name))])])])

# 代码生成器的原理

# 元素节点

function genElement (el, state) {
  // 如果el.plain是true,则说明节点没有属性
  const data = el.plain ? undefined : genData(el, state)

  const children = genChildren(el, state)
  code = `_c('${el.tag}'${
    data ? `,${data}` : '' // data
  }${
    children ? `,${children}` : '' // children
  })`
  return code
}

function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // 类似的还有很多种情况
  data = data.replace(/,$/, '') + '}'
  return data
}

function genChildren (el, state) {
  const children = el.children
  if (children.length) {
    return `[${children.map(c => genNode(c, state)).join(',')}]`
  }
}

// 调用不同节点类型的生成方法来生成字符串
function genNode (node, state) {
  if (node.type === 1) {
    return genElement(node, state)
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}

通过genData生成data的过程:先给data赋值一个 '{',然后发现节点存在哪些属性数据,就将这些数据拼接到data中,最后拼接一个 '}',此时一个完整的data就拼好了

生成子节点列表字符串的逻辑也是拼字符串。通过循环子节点列表,根据不同的子节点类型生成不同的节点字符串并将其拼接到一起

# 文本节点

文本放在 _v这个函数的参数中

function genText (text) {
  // 如果是动态文本,则使用expression;如果是静态文本,则使用text。
  // JSON.stringify可以给文本包装一层字符串
  return `_v(${text.type === 2
    ? text.expression
    : JSON.stringify(text.text)
  })`
}

# 注释节点

注释拼接到函数 _e的参数中

function genComment (comment) {
  return `_e(${JSON.stringify(comment.text)})`
}

# 总结

本章中,我们介绍了代码生成器的作用及其内部原理,了解了代码生成器其实就是字符串拼接的过程。通过递归AST来生成字符串,最先生成根节点,然后在子节点字符串生成后,将其拼接在根节点的参数中,子节点的子节点拼接在子节点的参数中,这样一层一层地拼接,直到最后拼接成完整的字符串。

同时还介绍了三种类型的节点,分别是元素节点、文本节点与注释节点。而不同类型的节点生成字符串的方式是不同的。

最后,我们介绍了当字符串拼接好后,会将字符串拼在with中返回给调用者。