深入浅出Vue.js阅读——模板编译原理——代码生成器

深入浅出Vue.js阅读——模板编译原理——代码生成器

  代码生成器是模板编译的最后一步,它的作用作用是将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。
  代码字符串可以被包装在函数中执行,这个函数就是我们通常说的渲染函数。
  渲染函数被执行之后,可以生成一份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",
            "valeu":"'el'"
        }
    ],
    "static":false,
    "staticRoot":false
}

  代码生成器可以通过上面这个AST来生成代码字符串,生成后的代码字符串是这样的:

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

  仔细观察生成后的代码字符串,我们会发现,这其实是一个嵌套的函数调用。函数_c的参数中执行了函数_v,而函数_v的参数中又执行了函数_s
  代码字符串中的_c其实是createElement的别名。crearteElement是虚拟DOM中所提到的方法,它的作用是创建虚拟节点,有三个参数,分别是:

  • 标签名
  • 一个包含模板相关属性的数据对象
  • 子节点列表
      调用createElement方法,我们可以得到一个VNode。
      这也就知道了渲染函数可以生成VNode的原因:渲染函数其实是执行了createElement,而createElement可以创建一个VNode。

1. 通过AST生成代码字符串

  生成代码字符串是一个递归的过程,从顶向下一次处理,每一个AST节点。
节点有三种类型,分别对应三种不同的创建方法与别名:

类型创建方法别名
元素节点createElement_c
文本节点createTextVNode_v
元素节点createEmptyVNode_e

  在递归的过程中,每处理一个AST节点,就会生成一个与节点类型相对应的代码字符串。
  如果节点是元素节点,那么代码字符串是这样的:

_c(<tagname>,<data>,<children>)

  元素节点通常有子节点,当处理它的子节点时,创建出来的代码字符串会放在上面例子中<children>的位置。
  使用AST生成代码字符串时,最先生成根节点div
生成后是这样的:

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

  然后继续生成它的子节点,生成出来的子节点字符串会放在_c函数第三个参数的位置。
  在前面的例子中,根节点div下面又是一个div节点,所以会生成一个div节点放在_c函数的第三个参数的位置。生成后的代码字符串如下:

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

  可以看到,在_c的第三个参数位置,多了一个数组,里面又有一个_c
  在模板中,第二个div下面是一个p节点,所以会产生一个p节点的代码字符串,如下:

_c('p')

  当这一段p节点的代码字符串被插入到整体代码字符串中之后,是下面这个样子:

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

  p节点下面是一个带变量的文本,生成的代码字符串如下:

_v('Hello '+_s(name))

  同样,它会插入到p节点的子节点列表的位置。插入之后的代码字符串如下:

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

  当递归结束时,我们就可以得到一个完整的代码字符串。这段代码字符串会被包裹在with语句中,伪代码如下:

`with(this){return ${code}}`

  在代码中,code是我们通过递归得到的完整的代码字符串。代码生成器的作用就是生成上面伪代码中所展示的一段字符串。

2.代码生成器的原理

  节点有不同的类型,例如元素节点、文本节点和注释节点。
  不同类型节点的生成方式是不一样的,下面我们分别介绍如何生成每个类型的节点。

1. 元素节点

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

function genElement(el,state){
    // 如果el.plain是true,则说明节点没有属性
    const data = el.plain ? undefined : genElement(el,state)
    const children = genElement(el,state)
    code = `_c('${el.tag}'${
        data ?  `,${data}` :  ''  //dara
    }${
        children ?  `,${children}` :  '' // children
    })`
    return code;
}

  代码中elplain属性是在编译时发现的。如果节点没有属性,就会把plain设置为true。这里我们可以通过plain来判断是否需要获取节点的属性数据。
  代码中的主要逻辑是用getDatagenChildren分别获取datachildren,然后将它们分别拼到字符串中指定的位置。最后把拼好的“_c(tagName,data,children)”返回,这样一个元素节点的代码字符串就生成好了。
  datachildren也是字符串,那么它们是如何生成的呢?
  我们先看data是如何生成的:

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;
}

  其实也是拼字符串。先给data赋值一个’{’,然后发现节点存在哪些属性数据,就将这些数据拼接到data中,最后拼接一个’}’,此时一个完整的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)
    }
}

  从上面的代码可以看到,通过循环子节点列表,然后分别调用不同节点类型的生成方法来生成字符串,最后将其凭借到一起并返回。
  这其实是一个递归过程。如果子节点存在子节点,那么会重复这个过程来生成子节点的子节点。
  从代码中可以看到,生成子节点时,会根据其类型的不同调用不同的生成方法。

2. 文本节点

  生成文本节点很简单,我们只需要把文本放在_v这个函数的参数中即可:

function genText(text){
    return `_v(${text.type===2
        ? text.expression
        :JSON.stringify(text.text)
    })`
}

  在上面的代码中,我们会把文本放在_v的参数中。这里会判断文本的类型:如果是动态文本,则使用expression;如果是静态文本,则使用text

3. 注释节点

  注释节点与文本节点相同,只需要把文本放在_e参数中即可,代码如下:

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

3. 总结

  我们介绍了代码生成器的作用及其内部原理,了解了代码生成器其实就是字符串拼接的过程。通过递归AST来生成字符串,最先生成根节点,然后在子节点字符串生成后,将其拼接在根节点的参数上,子节点的子节点拼接在子节点的参数上中,这样一层一层地拼接,直到最后拼接完整的字符串。
  同时还介绍了三种类型的节点,分别是元素节点、文本节点和注释节点。而不同类型的节点生成字符串的方式是不同的。
  最后,我们介绍了当字符串拼接好后,会将字符串拼接在with中返回给调用者。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值