zoukankan      html  css  js  c++  java
  • [Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串

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

    这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascript-series-code-analyzing。觉得有用记得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的代码逻辑进行了说明。

    相关

  • 相关阅读:
    滑动窗口(单调队列)
    蒜头君的排序
    P2024 食物链
    STL
    P2880 [USACO07JAN]平衡的阵容Balanced Lineup(RMQ的倍增模板)
    LCA的两种写法
    P1439 最长公共子序列(nlognLCS问题)
    P3383 【模板】线性筛素数
    P2158 [SDOI2008] 仪仗队(欧拉函数模板)
    alifhab
  • 原文地址:https://www.cnblogs.com/yzsunlei/p/12118360.html
Copyright © 2011-2022 走看看