学习vue源码(9)手写代码生成器

前面的学习vue源码(6)熟悉模板编译原理 我们谈到,模板编译分为解析器,优化器,代码生成器。

前面两部分已经实现,现在 就来看看 代码生成器怎么实现吧。

代码生成器的作用是使用 AST 生成 render 函数代码字符串。

解析器主要干的事是将 模板字符串 转换成 element ASTs,例如:

<div>
  <p>{{name}}</p>
</div>

上面这样一个简单的 模板 转换成 AST 后是这样的:

{
  tag: "div"
  type: 1,
  staticRoot: false,
  static: false,
  plain: true,
  parent: undefined,
  attrsList: [],
  attrsMap: {},
  children: [
      {
      tag: "p"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: {tag: "div", ...},
      attrsList: [],
      attrsMap: {},
      children: [{
          type: 2,
          text: "{{name}}",
          static: false,
          expression: "_s(name)"
      }]
    }
  ]
}

使用例子中的模板生成后的 AST 来生成 render 后是这样的:

{
  render: `with(this){return _c('div',[_c('p',[_v(_s(name))])])}`
}

格式化后是这样的:

with(this){
  return _c(
    'div',
    [
      _c(
        'p',
        [
          _v(_s(name))
        ]
      )
    ]
  )
}

生成后的代码字符串中看到了有几个函数调用 _c,_v,_s。

_c 对应的是 createElement,它的作用是创建一个元素。

  • 第一个参数是一个HTML标签名
  • 第二个参数是元素上使用的属性所对应的数据对象,可选项
  • 第三个参数是 children

例如:

一个简单的模板:

<p title="Berwin" @click="c">1</p>

生成后的代码字符串是:

`with(this){return _c('p',{attrs:{"title":"Berwin"},on:{"click":c}},[_v("1")])}`

格式化后:

with(this){
  return _c(
    'p',
    {
      attrs:{"title":"Berwin"},
      on:{"click":c}
    },
    [_v("1")]
  )
}

_v 的意思是创建一个文本节点。

_s 是返回参数中的字符串。

可能有同学觉得这个格式化后的代码很陌生,其实把with去掉后,就很熟悉了(其实with是用来改变作用域的,去掉也不会影响我们的理解)

return _c(
    'p',
    {
      attrs:{"title":"Berwin"},
      on:{"click":c}
    },
    [_v("1")]
  )

有没有发现 这样很熟悉?没错我们去看vue官网的 render渲染函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sJtun0qJ-1591176301693)(https://imgkr.cn-bj.ufileos.com/681bf1eb-bc26-429d-8ede-52ddfd99f2e8.png)]

有没发现,其实生成的代码字符串 就是 vue官网中介绍的render函数里的 createElement函数。

而参数也是对应的

  • 第一个参数:标签名

  • 第二个参数:节点数据

  • 第三个参数:子节点数组

代码生成器的总体逻辑其实就是使用 element ASTs 去递归,然后拼出这样的 _c('div',[_c('p',[_v(_s(name))])]) 字符串。

那如何拼这个字符串呢??

请看下面的代码:

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

因为 _c 的参数需要 tagName、data 和 children。

所以上面这段代码的主要逻辑就是用 genData 和 genChildren 获取 data 和 children,然后拼到 _c 中去,拼完后把拼好的 "_c(tagName, data, children)" 返回。

el.plain 为true该节点没有属性。因此就不需要 执行genData。

所以我们现在比较关心的两个问题:

data 如何生成的(genData 的实现逻辑)?
children 如何生成的(genChildren 的实现逻辑)?
我们先看 genData 是怎样的实现逻辑:

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},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // ... 类似的还有很多种情况
  data = data.replace(/,$/, '') + '}'
  return data
}

可以看到,就是根据 AST 上当前节点上都有什么属性,然后针对不同的属性做一些不同的处理,最后拼出一个字符串~

然后我们在看看 genChildren 是怎样的实现的:

function genChildren (
  el: ASTElement,
  state: CodegenState
): string | void {
  const children = el.children
  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)
  }
}

从上面代码中可以看出,生成 children 的过程其实就是循环 AST 中当前节点的 children,然后把每一项在重新按不同的节点类型去执行 genElement genComment genText。如果 genElement 中又有 children 在循环生成,如此反复递归,最后一圈跑完之后能拿到一个完整的 render 函数代码字符串,就是类似下面这个样子。

"_c('div',[_c('p',[_v(_s(name))])])"

最后把生成的 code 装到 with 里。

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // 如果ast为空,则创建一个空div
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`
  }
}

关于代码生成器的部分到这里就说完了,其实源码中远不止这么简单,很多细节我都没有去说,我只说了一个大体的流程,对具体细节感兴趣的同学可以自己去看源码了解详情。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值