【Vue2深度学习】模板编译篇-代码生成器

406 阅读3分钟

代码生成器是模板编译的最后一步,它的作用是将AST转换成渲染函数中的内容(代码字符串),当渲染函数被执行后,可以生成一份VNode,虚拟DOM就是通过这个VNode来渲染视图。

那么,如何根据AST来生成代码字符串? 我们来详细了解一下。

一、代码字符串生成过程

生成代码字符串是一个递归的过程,从上向下依次处理每一个AST节点。在递归的过程中,每处理一个AST节点,就会生成一个与节点类型相对应的代码字符串。

假如现在有如下模板:

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

它转换成AST并经过优化器的优化后是下面的样子。

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

然后我们来遍历上面的这个AST来生成对应的代码字符串。

1、生成根节点div,如下:

_c('div',{ attrs: { "id":"el"}})

2、生成div的子节点p,如下:

_c('div',{ attrs: { "id":"el"}}, [ _c('p')])

3、生成p节点下的文本,如下:

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

到此,整个AST就遍历完毕了,我们将得到的代码再包装一下,如下:

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

观察生成后的代码字符串,我们会发现,这其实是一个嵌套的函数调用。函数_c的参数中执行了函数_v, 而函数_v的参数中又执行了函数_s。

其中,

  • _c函数对应createElement方法,用来创建元素节点,它有三个参数,分别是:标签名、一个包含模板相关属性的数据对象、子节点列表;
  • _v函数对应createTextVNode方法,用来创建文本节点,而_s函数则用来处理文本中的变量;

如果模板中有注释节点,则调用_e函数来处理。 _e函数对应createEmptyNode方法,用来创建注释节点。

二、代码字符串生成原理

不同类型节点的生成方式是不一样的,下面我们分别介绍如何生成每个类型的节点。

1、元素节点

生成元素节点,其实就是生成一个_c的函数调用字符串,相关代码如下:


export function genElement (el: ASTElement, state: CodegenState): string {
   const data = el.plain ? undefined : genData(el, state)
   const children = el.inlineTemplate ? null : genChildren(el, state, true)
   code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    return code
}

可以看出,生成元素节点,最关键的就是生成节点属性data和子节点列表children。

在生成节点属性data时,先判断plain属性是否为true,若为true则表示节点没有属性,将data赋值为undefined;如果不为true则调用genData函数获取节点属性data数据。

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

    // key
    if (el.key) {
        data += `key:${el.key},`
    }
    // ref
    if (el.ref) {
        data += `ref:${el.ref},`
    }
    if (el.refInFor) {
        data += `refInFor:true,`
    }
    // pre
    if (el.pre) {
        data += `pre:true,`
    }
    // 篇幅所限,省略其他情况的判断
    data = data.replace(/,$/, '') + '}'
    return data
}

可以看到,源码中genData,就是在拼接字符串,先给data赋值为一个{,然后判断存在哪些属性数据,就将这些数据拼接到data中,最后再加一个},最终得到节点全部属性data

节点属性data获取好了,再来获取一下子节点列表children。

获取子节点列表children其实就是遍历ASTchildren属性中的元素,然后根据元素属性的不同生成不同的VNode创建函数调用字符串,如下:

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

function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}

2、文本节点

文本型的VNode可以调用_v(text)函数来创建,所以生成文本节点的render函数就是生成一个_v(text)函数调用的字符串。_v()函数接收文本内容作为参数,如果文本是动态文本,则使用动态文本AST节点的expression属性,如果是纯静态文本,则使用text属性。其生成代码如下:

export function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

3、注释节点

注释型的VNode可以调用_e(text)函数来创建,所以生成注释节点的render函数就是生成一个_e(text)函数调用的字符串。_e()函数接收注释内容作为参数,其生成代码如下:

export function genComment (comment: ASTText): string {
  return `_e(${JSON.stringify(comment.text)})`
}