欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

[Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串

程序员文章站 2023-02-06 11:29:56
本文我们一起通过学习Vue模板编译原理(二) AST生成Render字符串来分析Vue源码。预计接下来会围绕Vue源码来整理一些文章,如下。 "一起来学Vue双向绑定原理 数据劫持和发布订阅" "一起来学Vue模板编译原理(一) Template生成AST" "一起来学Vue模板编译原理(二) AS ......

本文我们一起通过学习vue模板编译原理(二)-ast生成render字符串来分析vue源码。预计接下来会围绕vue源码来整理一些文章,如下。

这些文章统一放在我的git仓库:。觉得有用记得star收藏。

编译过程

模板编译是vue中比较核心的一部分。关于 vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:

第一步:将模板字符串转换成element asts(解析器)

第二步:对 ast 进行静态节点标记,主要用来做虚拟dom的渲染优化(优化器)

第三步:使用element asts生成render函数代码字符串(代码生成器)

对应的vue源码如下,源码位置在src/compiler/index.js

export const createcompiler = createcompilercreator(function basecompile (
  template: string,
  options: compileroptions
): compiledresult {
  // 1.parse,模板字符串 转换成 抽象语法树(ast)
  const ast = parse(template.trim(), options)
  // 2.optimize,对 ast 进行静态节点标记
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 3.generate,抽象语法树(ast) 生成 render函数代码字符串
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticrenderfns: code.staticrenderfns
  }
})

这篇文档主要讲第三步使用element asts生成render函数代码字符串,对应的源码实现我们通常称之为代码生成器。

代码生成器运行过程

在分析代码生成器的原理前,我们先举例看下代码生成器的具体作用。

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

在上节"template生成ast"中,我们已经说了通过解析器会把上面模板解析成抽象语法树(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转换成可以直接执行的javascript字符串,最终结果如下:
with(this) {
    return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])
}

现在看不懂不要紧。其实生成器的过程就是 generate 函数拿到解析好的 ast 对象,递归 ast 树,为不同的 ast 节点创建不同的内部调用方法,然后组合成可执行的javascript字符串,等待后面的调用。

内部调用方法

我们看上面示例生成的javascript字符串,会发现里面会有_v_c_s这样的东西,这些其实就是vue内部定义的一些调用方法。

其中 _c 函数定义在 src/core/instance/render.js 中。

  vm.$slots = resolveslots(options._renderchildren, rendercontext)
  vm.$scopedslots = emptyobject
  // 定义的_c函数是用来创建元素的
  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)

而其他 _s_v 是定义在 src/core/instance/render-helpers/index.js 中:

export function installrenderhelpers (target: any) {
  target._o = markonce
  target._n = tonumber
  target._s = tostring
  target._l = renderlist //生成列表vnode
  target._t = renderslot //生成解析slot节点
  target._q = looseequal
  target._i = looseindexof
  target._m = renderstatic //生成静态元素
  target._f = resolvefilter
  target._k = checkkeycodes
  target._b = bindobjectprops //绑定对象属性
  target._v = createtextvnode //创建文本vnode
  target._e = createemptyvnode //创建空节点vnode
  target._u = resolvescopedslots
  target._g = bindobjectlisteners
  target._d = binddynamickeys
  target._p = prependmodifier
}

以上都是会在生成的javascript字符串中用到的,像比较常用的 _c 执行 createelement 去创建 vnode_l 对应 renderlist 渲染列表;_v 对应 createtextvnode 创建文本 vnode_e 对于 createemptyvnode 创建空的 vnode

整体逻辑

代码生成器的入口函数是generate,具体定义如下,源码位置在src/compiler/codegen/index.js

export function generate (
  ast: astelement | void,
  options: compileroptions
): codegenresult {
  // 初始化一些options
  const state = new codegenstate(options)
  // 传入ast和options进行生成
  const code = ast ? genelement(ast, state) : '_c("div")' //重点
  return {
    // 最外层包一个 with(this) 之后返回
    render: `with(this){return ${code}}`,
    // 这个数组中的函数与 vdom 中的 diff 算法优化相关
    // 那些被标记为 staticroot 节点的 vnode 就会单独生成 staticrenderfns
    staticrenderfns: state.staticrenderfns
  }
}

生成器入口函数就比较简单,先初始化一些配置options,然后传入ast进行生成,没有ast时直接生成一个空div,最后返回生成的javascript字符串和静态节点的渲染函数。这里会把静态节点区分出来,便于后面的vnode diff,即vue比较算法更新dom,现在先略过。

现在我们重点看看genelement函数,代码位置在src/compiler/codegen/index.js

export function genelement (el: astelement, state: codegenstate): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticroot && !el.staticprocessed) {
    return genstatic(el, state) // 静态节点处理生成函数
  } else if (el.once && !el.onceprocessed) {
    return genonce(el, state)   // v-once处理生成函数
  } else if (el.for && !el.forprocessed) {
    return genfor(el, state)    // v-for处理生成函数
  } else if (el.if && !el.ifprocessed) {
    return genif(el, state)     // v-if处理生成函数
  } else if (el.tag === 'template' && !el.slottarget && !state.pre) {
    return genchildren(el, state) || 'void 0' // 子节点处理生成函数
  } else if (el.tag === 'slot') {
    return genslot(el, state)   // slot处理生成函数
  } else {
    // component or element
    let code
    if (el.component) {
      code = gencomponent(el.component, el, state)  // component组件处理生成函数
    } else {
      let data
      if (!el.plain || (el.pre && state.maybecomponent(el))) {
        data = gendata(el, state)  // attributes节点属性处理生成函数
      }

      const children = el.inlinetemplate ? null : genchildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

你会发现genelement函数里面有很多条件判断。这是因为vue里面的指令写法实在太多,像 v-ifv-forv-slot等,每种指令写法都分离出一个函数来单独处理,这样代码阅读起来也清晰明了。下面,我们重点来看看 genforgendata 的具体处理逻辑。

genfor

genfor 函数是用来处理 v-for 指令写法的,源码位置在src/compiler/codegen/index.js

export function genfor (
  el: any,
  state: codegenstate,
  altgen?: function,
  althelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  if (process.env.node_env !== 'production' &&
    state.maybecomponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `see https://vuejs.org/guide/list.html#key for more info.`,
      el.rawattrsmap['v-for'],
      true /* tip */
    )
  }

  el.forprocessed = true // avoid recursion
  return `${althelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altgen || genelement)(el, state)}` +
    '})'
}

genfor 的逻辑其实就是,首先 ast 元素节点中获取了和 for 相关的一些属性,然后返回了一个代码字符串。

gendata

gendata 函数是用来处理节点属性的,源码位置在src/compiler/codegen/index.js

export function gendata (el: astelement, state: codegenstate): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  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,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // module data generation functions
  for (let i = 0; i < state.datagenfns.length; i++) {
    data += state.datagenfns[i](el)
  }
  // attributes
  if (el.attrs) {
    data += `attrs:${genprops(el.attrs)},`
  }
  // dom props
  if (el.props) {
    data += `domprops:${genprops(el.props)},`
  }
  // event handlers
  if (el.events) {
    data += `${genhandlers(el.events, false)},`
  }
  if (el.nativeevents) {
    data += `${genhandlers(el.nativeevents, true)},`
  }
  // slot target,only for non-scoped slots
  if (el.slottarget && !el.slotscope) {
    data += `slot:${el.slottarget},`
  }
  // scoped slots
  if (el.scopedslots) {
    data += `${genscopedslots(el, el.scopedslots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
  // inline-template
  if (el.inlinetemplate) {
    const inlinetemplate = geninlinetemplate(el, state)
    if (inlinetemplate) {
      data += `${inlinetemplate},`
    }
  }
  data = data.replace(/,$/, '') + '}'
  // v-bind dynamic argument wrap
  if (el.dynamicattrs) {
    data = `_b(${data},"${el.tag}",${genprops(el.dynamicattrs)})`
  }
  // v-bind data wrap
  if (el.wrapdata) {
    data = el.wrapdata(data)
  }
  // v-on data wrap
  if (el.wraplisteners) {
    data = el.wraplisteners(data)
  }
  return data
}

总结一下

代码生成是模板编译的第三步,它完成了ast到render的转换,即将抽象语法树ast转换成可以直接执行的javascript字符串。

其中genelement的代码比较多,因为需要分别处理的情况非常多,这里只是对genfor和gendata的代码逻辑进行了说明。

相关