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的代码逻辑进行了说明。

    相关

  • 相关阅读:
    线段树专辑—— pku 1436 Horizontally Visible Segments
    线段树专辑——pku 3667 Hotel
    线段树专辑——hdu 1540 Tunnel Warfare
    线段树专辑—— hdu 1828 Picture
    线段树专辑—— hdu 1542 Atlantis
    线段树专辑 —— pku 2482 Stars in Your Window
    线段树专辑 —— pku 3225 Help with Intervals
    线段树专辑—— hdu 1255 覆盖的面积
    线段树专辑—— hdu 3016 Man Down
    Ajax跨域访问
  • 原文地址:https://www.cnblogs.com/yzsunlei/p/12118360.html
Copyright © 2011-2022 走看看