最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue2.0源码分析:编译原理(下)

    正文概述 掘金(汪图南)   2020-12-04   495

    编译原理(下)

    如果觉得写得不错,请到GitHub给我一个Star

    上一篇:Vue2.0源码分析:编译原理(上)

    由于掘金文章字数限制,不得不拆分上、下两篇文章。

    optimize优化

    在经过parse模板编译完成后,我们可以得到一个ast树形结构,接下来进行optimize优化第二大步骤。这个过程相比较其它两个步骤,是最简单的。

    import { parse } from './parser/index'
    import { optimize } from './optimizer'
    
    function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      const ast = parse(template.trim(), options)
      if (options.optimize !== false) {
        optimize(ast, options)
      }
      // ...
    }
    

    优化的目的在于,在编译的时候有些节点自首次渲染完毕后,在后续的派发更新过程中不会随着数据的变动而变动,因此我们在进行节点对比的时候,可以直接跳过这些节点,进一步加快组件渲染的速度。

    optimize优化的过程中,它的处理方式是深度遍历ast树形结构,遇到静态节点的时候把它的ast.static属性设置为true。同时对于一个父ast节点来说,当其children子节点全部为静态节点的时候,那么其本身也是一个静态节点,我们把它的ast.staticRoot设置为true

    在介绍optimize优化这个章节的时候,我们以下面这个例子为例:

    let html = `
      <div>
        <p>{{msg}}</p>
        <span>静态节点</span>
      </div>
    `
    

    在以上例子中,span节点因为其内容是纯文本,因此它的ast.static一定为true

    我们回过头来看一下optimize方法的定义,其代码如下:

    export function optimize (root: ?ASTElement, options: CompilerOptions) {
      if (!root) return
      isStaticKey = genStaticKeysCached(options.staticKeys || '')
      isPlatformReservedTag = options.isReservedTag || no
      // first pass: mark all non-static nodes.
      markStatic(root)
      // second pass: mark static roots.
      markStaticRoots(root, false)
    }
    

    在这个方法中,它首先调用markStatic标记静态节点,然后调用markStaticRoots来标记静态根节点。

    静态节点类型

    markStatic方法中,它首先调用isStatic方法来判断当前ast是否为一个静态节点,其代码如下:

    function markStatic (node: ASTNode) {
      node.static = isStatic(node)
      // ...
    }
    function isStatic (node: ASTNode): boolean {
      if (node.type === 2) { // expression
        return false
      }
      if (node.type === 3) { // text
        return true
      }
      return !!(node.pre || (
        !node.hasBindings && // no dynamic bindings
        !node.if && !node.for && // not v-if or v-for or v-else
        !isBuiltInTag(node.tag) && // not a built-in
        isPlatformReservedTag(node.tag) && // not a component
        !isDirectChildOfTemplateFor(node) &&
        Object.keys(node).every(isStaticKey)
      ))
    }
    

    在阅读完isStatic方法后,我们发现静态节点必须满足以下几种情况:

    1. 带插值(表达式)的文本节点,不是静态节点,例如:
    // 不是静态节点
    let html = `<div>{{msg}}</div>`
    
    1. 纯文本节点,是静态节点,例如:
    // 是静态节点
    let html = `<div>Hello, Vue.js</div>`
    
    1. 如果是普通元素节点,并且使用了v-pre指令,则是静态节点,例如:
    // 是静态节点
    let html = `<div v-pre>{{msg}}</div>`
    
    1. 如果是普通元素,在没有使用v-pre指令的情况下,还必须同时满足:没有动态绑定属性、没有使用v-if、没有使用v-for、不是内置组件slot/component、是平台保留标签、不是带有v-fortemplate标签的直接子节点、节点的所有属性的key都是静态key,例如:
    // 是静态节点
    let html = '<div class="box"></div>'
    

    标记静态节点

    在分析完isStatic方法后,我们来分析一下markStatic标记静态节点方法的实现原理,其代码如下:

    function markStatic (node: ASTNode) {
      node.static = isStatic(node)
      if (node.type === 1) {
        // do not make component slot content static. this avoids
        // 1. components not able to mutate slot nodes
        // 2. static slot content fails for hot-reloading
        if (
          !isPlatformReservedTag(node.tag) &&
          node.tag !== 'slot' &&
          node.attrsMap['inline-template'] == null
        ) {
          return
        }
        for (let i = 0, l = node.children.length; i < l; i++) {
          const child = node.children[i]
          markStatic(child)
          if (!child.static) {
            node.static = false
          }
        }
        if (node.ifConditions) {
          for (let i = 1, l = node.ifConditions.length; i < l; i++) {
            const block = node.ifConditions[i].block
            markStatic(block)
            if (!block.static) {
              node.static = false
            }
          }
        }
      }
    }
    

    代码分析:

    • type=2type=3的时候,代表它们分别是带表达式的插值文本和纯文本,这个时候使用isStatic方法的返回结果,直接标记static属性为true即可。
    • 对于type=1,它是普通元素节点的时候,判断逻辑稍微复杂一点。第一步首先需要使用for循环去遍历children子节点,然后在for循环中递归调用markStatic方法,以达到深度遍历并标记子节点的目的。在这个过程中,唯一一个值得注意的地方就是,在对children子节点标记完毕后,会根据子节点的static属性来设置父节点的static属性。只要有一个子节点的static属性不为true,那么父节点也一定不为true。第二步,如果当前节点有v-if/v-else-if/v-else等指令,由于这些节点并不会保存在children数组中,而是在node.ifConditions属性下面,因此我们需要遍历ifConditions数组,来递归标记子节点。同样的,在标记完子节点后,我们需要根据子节点的static来同步更新父节点的static
    const showMsg = false
    let html = `
      <div>
        <div v-if="showMsg">show</div>
        <div v-else="showMsg">not show</div>
      </div>
    `
    
    const node = {
      ifConditions: [
        { exp: 'showMsg', block: 'div的ast节点' },
        { exp: undefined, block: 'div的ast节点' }
      ]
    }
    

    标记静态根节点

    在介绍完标记静态节点后,我们接着要介绍标记静态根节点markStaticRoots方法,其代码如下:

    function markStaticRoots (node: ASTNode, isInFor: boolean) {
      if (node.type === 1) {
        if (node.static || node.once) {
          node.staticInFor = isInFor
        }
        // For a node to qualify as a static root, it should have children that
        // are not just static text. Otherwise the cost of hoisting out will
        // outweigh the benefits and it's better off to just always render it fresh.
        if (node.static && node.children.length && !(
          node.children.length === 1 &&
          node.children[0].type === 3
        )) {
          node.staticRoot = true
          return
        } else {
          node.staticRoot = false
        }
        if (node.children) {
          for (let i = 0, l = node.children.length; i < l; i++) {
            markStaticRoots(node.children[i], isInFor || !!node.for)
          }
        }
        if (node.ifConditions) {
          for (let i = 1, l = node.ifConditions.length; i < l; i++) {
            markStaticRoots(node.ifConditions[i].block, isInFor)
          }
        }
      }
    }
    

    我们可以看到,在markStaticRoots方法中它对于children子节点和带v-if/v-else-if/v-else等指令的处理过程是类似的,我们省略这部分内容重复的介绍。

    我们来看下面这段有意思的代码:

    // For a node to qualify as a static root, it should have children that
    // are not just static text. Otherwise the cost of hoisting out will
    // outweigh the benefits and it's better off to just always render it fresh.
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    

    我们从注释中也可以看出来,如果当前节点static属性为true了,要标记它为静态根节点的话,还必须满足它的子节点不能只有一个纯文本节点,因为这样做其优化成本要大于其收益。

    codegen代码生成

    optimize优化章节介绍完毕以后,我们来到了编译的最后一步:代码生成。codegen代码生成阶段的逻辑相对来说还是比较多的,在这一节我们尽可能的分析常见的使用场景,例如:对v-forv-if/v-else以及v-for等场景代码生成流程的分析。

    baseCompile方法中,我们可以看到在parseoptimize这两个主要流程之后,最后一步codegen的过程:

    import { generate } from './codegen/index'
    function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      const ast = parse(template.trim(), options)
      if (options.optimize !== false) {
        optimize(ast, options)
      }
      const code = generate(ast, options)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    }
    

    generate

    baseCompile方法中,codegen负责将优化好的AST树形结构转换成可执行代码,其中最重要的是code.rendergenerate方法就是用来负责处理这个过程的,它是从src/compiler/codegen/index.js文件中引入的,其代码如下:

    export function generate (
      ast: ASTElement | void,
      options: CompilerOptions
    ): CodegenResult {
      const state = new CodegenState(options)
      const code = ast ? genElement(ast, state) : '_c("div")'
      return {
        render: `with(this){return ${code}}`,
        staticRenderFns: state.staticRenderFns
      }
    }
    

    generate方法中它首先判断了ast,如果没有ast,则code赋值为_c("div"),如果有则调用genElement方法。在其返回对象的render属性中,它使用with把我们的代码进行了包裹。在compileToFunctions章节中,我们介绍过它会使用new Function的形式把字符串代码转换成真正的Function

    function createFunction (code, errors) {
      try {
        return new Function(code)
      } catch (err) {
        errors.push({ err, code })
        return noop
      }
    }
    
    // 转换前
    const render = 'with(this){return _c("div")}'
    
    // 转换后
    const func = function () {
      with (this) {
        return _c('div')
      }
    }
    

    还记得_c是什么吗?,_c是在initRender中定义的一个方法:

    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
    

    codegen代码生成阶段,我们会接触到各种类似命名的函数,它们都是辅助函数,我们可以在src/core/instance/render-helpers/index.js文件中找到:

    export function installRenderHelpers (target: any) {
      target._o = markOnce
      target._n = toNumber
      target._s = toString
      target._l = renderList
      target._t = renderSlot
      target._q = looseEqual
      target._i = looseIndexOf
      target._m = renderStatic
      target._f = resolveFilter
      target._k = checkKeyCodes
      target._b = bindObjectProps
      target._v = createTextVNode
      target._e = createEmptyVNode
      target._u = resolveScopedSlots
      target._g = bindObjectListeners
      target._d = bindDynamicKeys
      target._p = prependModifier
    }
    

    上面的辅助函数有很多,我们来重点说明几个最常用的:

    export function installRenderHelpers (target: any) {
      target._s = toString          // 转字符串
      target._l = renderList        // 处理v-for列表
      target._t = renderSlot        // 处理插槽
      target._m = renderStatic      // 处理静态节点
      target._f = resolveFilter     // 处理过滤器
      target._v = createTextVNode   // 创建文本VNode
      target._e = createEmptyVNode  // 创建空VNode
    }
    

    genElement

    generate方法中,它通过调用genElement来返回一个对象,我们来看一下这个方法的定义:

    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)
      } else if (el.for && !el.forProcessed) {
        return genFor(el, state)
      } else if (el.if && !el.ifProcessed) {
        return genIf(el, state)
      } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
        return genChildren(el, state) || 'void 0'
      } else if (el.tag === 'slot') {
        return genSlot(el, state)
      } else {
        // component or element
        let code
        if (el.component) {
          code = genComponent(el.component, el, state)
        } else {
          let data
          if (!el.plain || (el.pre && state.maybeComponent(el))) {
            data = genData(el, state)
          }
    
          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方法中清晰的看到,它会根据ast属性的不同来分别调用不同的代码生成函数。例如:genStatic用来处理生成静态节点的、genFor用来处理v-for循环的,genChildren用来处理子节点的以及genComponent用来处理组件的。

    下一步,我们的目标是对这些不同的代码生成函数来进行分析,同时由于codegen代码生成的逻辑相对来说还是比较多的,因此我们只介绍最常见的几种。

    genStatic

    在之前的optimize章节中,我们分析过是否为静态根节点的条件,假设我们有以下template

    const html = `<div><p>Hello</p></div>`
    

    根据之前的分析过程,我们知道div节点会被标记为staticRoot静态根节点。在第一次执行genElement方法的时候,因为staticRoottruestaticProcessedfalse,因此会调用genStatic方法。接下来,我们来看一下genStatic方法的代码:

    function genStatic (el: ASTElement, state: CodegenState): string {
      el.staticProcessed = true
      // ...省略pre相关
      state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
       // ...省略pre相关
      return `_m(${
        state.staticRenderFns.length - 1
      }${
        el.staticInFor ? ',true' : ''
      })`
    }
    

    genStatic方法的最开始,它首先对staticProcessed赋值为true,这样做是为了防止在递归调用genElement方法的过程中,重复调用genStatic。接着,它递归调用genElement来处理div的子节点p。在处理p节点的过程中,因为p节点不满足静态根节点的条件,因此它会走其他代码生成函数。我们这里为了方便,直接说明结果,至于结果如何来的,我们会在之后的小节中进行介绍:

    // p节点的文本内容
    const text = '_v(["Hello"])'
    
    // p节点
    const p = '_c("p", [_v(["Hello"])])'
    
    // div节点
    const div = '_c("div", [_c("p", [_v(["Hello"])])])'
    
    // state.staticRenderFns数组
    const state = {
      staticRenderFns: ['_c("div", [_c("p", [_v(["Hello"])])])']
    }
    

    我们最后来看genStatic的返回值,因为staticRenderFns数组只有一条数据并且staticInForfalse,因此函数返回值为:

    return '_m(0)'
    

    在分析完genStatic方法后,我们可以知道:静态根节点代码生成的render存放在staticRenderFns中,而不是render

    最后回到baseCompile方法,在我们的例子中,我们使用generate生成code后,baseCompile方法的返回值如下:

    return {
      ...,
      render: '_m(0)',
      staticRenderFns: ['_c("div", [_c("p", [_v(["Hello"])])])']
    }
    

    genIf

    genIf是用来处理v-if/v-else等指令的,在前面章节中我们介绍过如果存在v-if/v-else指令,那么其子节点是存放在ifConditions数组中的,以下面代码为例:

    const showMsg = true
    let html = `
      <div>
        <div v-if="showMsg">show</div>
        <div v-else>not show</div>
      </div>
    `
    const ast = {
      if: true,
      ifConditions: [
        { exp: 'showMsg', block: 'div元素的ast' },
        { exp: undefined, block: 'div元素的ast' }
      ]
    }
    

    在调用genElement方法的时候,因为ast对象的if属性为true,并且ifProcessed属性为false,所以会调用genIf方法。接下来,我们来看一下genIf方法的代码:

    export function genIf (
      el: any,
      state: CodegenState,
      altGen?: Function,
      altEmpty?: string
    ): string {
      el.ifProcessed = true // avoid recursion
      return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
    }
    function genIfConditions (
      conditions: ASTIfConditions,
      state: CodegenState,
      altGen?: Function,
      altEmpty?: string
    ): string {
      if (!conditions.length) {
        return altEmpty || '_e()'
      }
    
      const condition = conditions.shift()
      if (condition.exp) {
        return `(${condition.exp})?${
          genTernaryExp(condition.block)
        }:${
          genIfConditions(conditions, state, altGen, altEmpty)
        }`
      } else {
        return `${genTernaryExp(condition.block)}`
      }
    
      // v-if with v-once should generate code like (a)?_m(0):_m(1)
      function genTernaryExp (el) {
        return altGen
          ? altGen(el, state)
          : el.once
            ? genOnce(el, state)
            : genElement(el, state)
      }
    }
    

    genIf方法中,它首先把ifProcessed赋值为true,这样做的目的在之前genStatic小节也提到过了,主要是为了防止递归调用genElement时重复执行genIf方法。然后,它调用genIfConditions来处理ifConditions数组。

    genIfConditions中,它每次获取数组的第一个,然后判断其exp表达式,如果有则使用三目运算表达式来返回,同时会递归调用genIfConditions,如果没有则直接返回genTernaryExp方法的调用结果。

    我们结合之前我们提到的例子,来进行说明:

    // 第一次调用genIfConditions时
    const condition = { exp: 'showMsg', block: 'div元素的ast' }
    
    // if判断exp为真,调用genTernaryExp,再递归调用genElement,处理div的文本节点
    const txt = '_v("show")'
    const div = '_c("div", [_v("show")])'
    
    // 递归调用genIfConditions时
    const condition = { exp: undefined, block: 'div元素的ast' }
    
    // if判断exp为假,调用genTernaryExp,再递归调用genElement,处理div的文本节点
    const txt = '_v("not show")'
    const div = '_c("div", [_v("not show")])'
    
    // genIfConditions递归调用结束,genIf方法返回值
    return '(showMsg)?_c("div", [_v("show")]):_c("div", [_v("not show")])'
    

    再回到baseCompiler方法中,通过generate方法返回code后,最终的返回值如下:

    return {
      ...,
      render: 'with(this){return _c("div", [(showMsg)?_c("div", [_v("show")]):_c("div", [_v("not show")])])}',
      staticRenderFns: []
    }
    

    genFor

    genFor是用来处理v-for循环的,我们以下面代码为例来说明:

    const list = ['AAA', 'BBB', 'CCC']
    let html =  `
      <ul>
        <li v-for="(item, index) in list" :key="index">{{item}}</li>
      </ul>
    `
    

    在第一次调用genElement因为ul标签上没有任何指令或者属性,也不是一个组件,因此会调用genChildren递归调用genElement来处理子节点。第二次调用genElement时,这个时候处理li标签,因为li标签上存在v-for指令,它的ast对象的for属性为判断为真,并且forProcessed属性为false,因此会调用genFor方法。接下来,我们来看一下genFor方法的代码:

    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}` : ''
    
      // ...
    
      el.forProcessed = true // avoid recursion
      return `${altHelper || '_l'}((${exp}),` +
        `function(${alias}${iterator1}${iterator2}){` +
          `return ${(altGen || genElement)(el, state)}` +
        '})'
    }
    

    genFor方法的最开始,重新定义了几个属性,根据我们的例子这些属性值为:

    const exp = 'list'
    const alias = 'item'
    const iterator1 = ',index'
    const iterator2 = ''
    

    接着,它通过调用genElement来处理li标签及其的文本节点:

    const txt = '_v(_s(item))'
    const li = '_c("li",{key:index},[_v(_s(item))])'
    

    处理完毕以后,genFor方法的返回值如下:

    return '_l((list),function(item,index){return _c("li",{key:index},[_v(_s(item))])})'
    

    genChildren递归处理子节点完毕后,其值如下:

    const code = '_c("ul",l((list),function(item,index){return _c("li",{key:index},[_v(_s(item))])}),0)'
    

    再回到baseCompiler方法中,通过generate方法返回code后,最终的返回值如下:

    return {
      ...,
      render: 'with(this){return _c("ul",l((list),function(item,index){return _c("li",{key:index},[_v(_s(item))])}),0)',
      staticRenderFns: []
    }
    

    genChildren

    genChildren是用来处理子节点的,对于子节点来说通常有两种情况:v-for循环的子节点、正常的子节点。

    在调用genElement的时候,当节点不匹配前面提到的genStaticgenIfgenFor等等情况的时候,证明当前节点是一个普通节点,此时调用genChildren来处理它的子节点,当子节点处理完毕后只需要使用_c把子节点包裹起来就可以了。

    export function genElement (el: ASTElement, state: CodegenState): string {
      // ...
      const children = el.inlineTemplate ? null : genChildren(el, state, true);
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
      // ...
    }
    

    接下来,我们来看一下genChildren的代码:

    export function genChildren (
      el: ASTElement,
      state: CodegenState,
      checkSkip?: boolean,
      altGenElement?: Function,
      altGenNode?: Function
    ): string | void {
      const children = el.children
      if (children.length) {
        const el: any = children[0]
        // optimize single v-for
        if (children.length === 1 &&
          el.for &&
          el.tag !== 'template' &&
          el.tag !== 'slot'
        ) {
          const normalizationType = checkSkip
            ? state.maybeComponent(el) ? `,1` : `,0`
            : ``
          return `${(altGenElement || genElement)(el, state)}${normalizationType}`
        }
        const normalizationType = checkSkip
          ? getNormalizationType(children, state.maybeComponent)
          : 0
        const gen = altGenNode || genNode
        return `[${children.map(c => gen(c, state)).join(',')}]${
          normalizationType ? `,${normalizationType}` : ''
        }`
      }
    }
    

    v-for遍历子节点

    我们可以在genChildren中看到,它首先对v-for循环遍历的情况做了判断,为了更好的理解,我们使用在genFor小结的例子来举例说明:

    const list = ['AAA', 'BBB', 'CCC']
    let html = `
    <ul>
      <li v-for="item in list" :key="item">{{item}}</li>
    </ul>`
    

    当第一次调用genElement处理ul标签的时候,因为ul上没有任何属性或者指令,因此调用genChildren开始处理子节点。在if分支条件判断时,其值为真,会递归调用genElement来处理li标签。此时li标签因为有v-for,所以会使用genFor来处理,当genFor处理完毕后,其返回值如下:

    const code = '_l((list),function(item){return _c("li",{key:item},[_v(_s(item))])})'
    

    li标签处理完毕后,genChildren的返回值如下:

    return `_l((list),function(item){return _c('li',{key:item},[_v(_s(item))])}),0`
    

    此时ul标签调用genChildren处理子节点完毕,最后使用_cchildren包裹起来并赋值给code,其值如下:

    const code = `_c('ul',_l((list),function(item){return _c('li',{key:item},[_v(_s(item))])}),0)`
    

    普通子节点

    在介绍完v-for遍历子节点后,我们来看一种正常子节点的案例:

    const msg = 'hello'
    let html = `
    <div>
      <p>{{msg}}</p>
      <p>no data</p>
      文本节点
    </div>`
    

    因为div标签上没有任何属性和指令,因此它会调用genChildren来处理子节点,当执行到genChildren后,它不满足if分支的逻辑判断条件,因此会执行下面这段代码:

    export function genChildren (
      el: ASTElement,
      state: CodegenState,
      checkSkip?: boolean,
      altGenElement?: Function,
      altGenNode?: Function
    ): string | void {
      const children = el.children
      if (children.length) {
        const el: any = children[0]
        // ...省略v-for相关
        const normalizationType = checkSkip
          ? getNormalizationType(children, state.maybeComponent)
          : 0
        const gen = altGenNode || genNode
        return `[${children.map(c => gen(c, state)).join(',')}]${
          normalizationType ? `,${normalizationType}` : ''
        }`
      }
    }
    

    genChildren代码中,它会遍历children子节点数组,通过genNode按照asttype类型分情况调用genElementgenCommentgenText

    function genNode (node: ASTNode, state: CodegenState): string {
      if (node.type === 1) {
        return genElement(node, state)
      } else if (node.type === 3 && node.isComment) {
        return genComment(node)
      } else {
        return genText(node)
      }
    }
    export function genText (text: ASTText | ASTExpression): string {
      return `_v(${text.type === 2
        ? text.expression // no need for () because already wrapped in _s()
        : transformSpecialNewlines(JSON.stringify(text.text))
      })`
    }
    export function genComment (comment: ASTText): string {
      return `_e(${JSON.stringify(comment.text)})`
    }
    

    代码分析:

    • children第一次遍历时,它是一个p标签并且子节点是一个带变量的插值表达式,会递归调用genElement,在遍历完毕后,它的返回值如下:
    const p = `_c('p', [_v(_s(msg))])`
    
    • children第二次遍历时,它同样是一个p标签,但它是一个静态节点,其子节点是一个纯文本,会递归调用genElement,在遍历完毕后,它的返回值如下:
    const p = `_c('p', [_v('no data')])`
    
    • children第三次遍历时,它是一个纯文本,会调用genText来处理,在遍历完毕后,它的返回值如下:
    const text = `_v('文本节点')`
    
    • 最后,当children都遍历完毕后,会调用join(',')方法把数组元素用逗号连接成字符串,也就是说genChildren的返回值如下:
    return `[_c('p', [_v(_s(msg))]),_c('p', [_v('no data')]),_v('文本节点')]`
    

    genChildren处理子节点完毕以后,会使用_c把子节点包裹起来并赋值给code,其值如下:

    const code = `_c('div', [_c('p', [_v(_s(msg))]),_c('p', [_v('no data')]),_v('文本节点')])`
    

    genComponent

    export function genElement (el: ASTElement, state: CodegenState): string {
      // ...
      if (el.component) {
        code = genComponent(el.component, el, state)
      } else {
        // ...
      }
      return code
    }
    

    我们在genElement方法中可以看到,想要调用genComponent方法,ASTcomponent属性必须有值。我们先来看一下component属性什么时候被赋值,其实它是在parse编译的时候赋值的,在processComponent方法中有如下代码:

    function processComponent (el) {
      let binding
      if ((binding = getBindingAttr(el, 'is'))) {
        el.component = binding
      }
      if (getAndRemoveAttr(el, 'inline-template') != null) {
        el.inlineTemplate = true
      }
    }
    

    我们可以看到,它使用getBindingAttr获取了is属性,如果有值且判断为真则赋值给el.component。那么,让我们回想一下什么组件可以撰写is属性呢?答案是内置组件component,也就是说genComponent是用来处理component内置组件的。

    根据我们的分析,我们假设有如下案例:

    const name = 'App'
    let html = '<component :is="name" />'
    

    当调用genElement方法的时候,因为el.component值为name,条件判断为真调用genComponent。我们来看一下genComponent方法的实现代码,如下:

    function genComponent (
      componentName: string,
      el: ASTElement,
      state: CodegenState
    ): string {
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      return `_c(${componentName},${genData(el, state)}${
        children ? `,${children}` : ''
      })`
    }
    

    genComponent代码不是很多,因为component内置组件没有子节点,所以genChildren不返回任何内容。就我们的例子而言,它最后的返回值如下:

    return `_c(name,{tag:"component"}`
    

    你可能会很好奇,返回值中的tag:"component"是如何来的,它其实是通过genData方法返回的。在genData方法中,对于内置组件component处理的相关代码如下:

    export function genData (el: ASTElement, state: CodegenState): string {
      let data = '{'
      // ...
      if (el.component) {
        data += `tag:"${el.tag}",`
      }
      data = data.replace(/,$/, '') + '}'
      // ...
      return data
    }
    

    genData

    genData是用来处理节点上各种属性、指令、事件、作用域插槽以及ref等等,它的功能非常多,代码也多,但并不是很复杂,更多的是使用if分支来判断以上各种情况。

    因此,为了有针对性的学习我们并不会对genData的每一行代码进行说明,而是针对几个常用的case案例来分析。

    export 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},`
      }
      // 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)},`
      }
      data = data.replace(/,$/, '') + '}'
      // ...
      return data
    }
    

    假设,我们有如下template模板:

    let html = `
      <div>
        <p ref="pRef" key="p">123</p>
        <div class="box" style="{width:100px}" data-index="100"></div>
      </div>`
    

    代码分析:

    • 当处理p标签的时候,因为它有refkey两属性,因此其返回值和生成的code如下:
    const data = `{ref:"pRef",key:"p"}`
    const code = `_c("p", {ref:"pRef",key:"p"}, [_v("123")])`
    
    • 当处理div标签的时候,它有classstyle,对于这两个属性它们是使用state.dataGenFns来处理的。在CodegenState构造函数中,有这样一段代码:
    export class CodegenState {
      // ...
      constructor (options: CompilerOptions) {
        // ...
        this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
        // ...
      }
    }
    

    实例属性dataGenFns是通过所处不同的平台扩展而来的,对于Web浏览器端而言,css Modulestyle Module都定义了genData方法,它们在src/platforms/web/compiler/modules目录下的class.jsstyle.js

    // css module
    function genData (el: ASTElement): string {
      let data = ''
      if (el.staticStyle) {
        data += `staticStyle:${el.staticStyle},`
      }
      if (el.styleBinding) {
        data += `style:(${el.styleBinding}),`
      }
      return data
    }
    // style module
    function genData (el: ASTElement): string {
      let data = ''
      if (el.staticClass) {
        data += `staticClass:${el.staticClass},`
      }
      if (el.classBinding) {
        data += `class:${el.classBinding},`
      }
      return data
    }
    

    所以对于实例属性dataGenFns而言,它是一个数组,其中有两个元素,一个是cssgenData方法,另外一个是stylegenData方法。当for循环遍历state.dataGenFns的时候,会分别调用它们。在遍历完毕后,此时的data值如下:

    let data = `{staticClass:"box",staticStyle:{"{width":"100px}"},`
    
    • 最后,就是来处理data-index属性,它是通过genProps来处理的,这个方法的代码如下:
    function genProps (props: Array<ASTAttr>): string {
      let staticProps = ``
      let dynamicProps = ``
      for (let i = 0; i < props.length; i++) {
        const prop = props[i]
        const value = __WEEX__
          ? generateValue(prop.value)
          : transformSpecialNewlines(prop.value)
        if (prop.dynamic) {
          dynamicProps += `${prop.name},${value},`
        } else {
          staticProps += `"${prop.name}":${value},`
        }
      }
      staticProps = `{${staticProps.slice(0, -1)}}`
      if (dynamicProps) {
        return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
      } else {
        return staticProps
      }
    }
    

    genData执行到最后时,其data返回值以及div标签生成code的值如下:

    let data = `{staticClass:"box",staticStyle:{"{width":"100px}"},attrs:{"data-index":"100"}}`
    const code = `_c("div", {staticClass:"box",staticStyle:{"{width":"100px}"},attrs:{"data-index":"100"}})`
    

    小结:在分析完genData这一小节后,标志着codegen代码生成章节已经全部介绍完毕了,通过这一章节我们知道了把AST转换成code过程中的各种细节。虽然我们极力撰写很多案例来说明,但依旧很有多逻辑我们无法覆盖到,例如:插槽/作用域插槽,event事件以及v-model等等。对于这些内容,我们会安排在后续的扩展章节进行分析。

    如果觉得写得不错,请到GitHub给我一个Star

    上一篇:Vue2.0源码分析:编译原理(上)

    由于掘金文章字数限制,不得不拆分上、下两篇文章。


    起源地下载网 » Vue2.0源码分析:编译原理(下)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元