Vue2总结(八)Template模版编译

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Vue2总结(八)Template模版编译

目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 30****分钟**

前言

Vue.Js 是采用虚拟DOM的前端框架之一,其中每一个vue对象都有 template,它类似于html又不完全相同,究竟Vue.js是如何将data中的数据渲染到真实的宿主环境中的?又是如何通过“响应式”修改数据的?template是如何被编译成真实环境中可用的HTML的?AST语法树又是什么? 本篇文章将从AST语法树的基础上,详细讲解从 template 到 DOM 的全过程。

Vue2总结(八)Template模版编译

$mount

在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式。然后,经过 generate(将AST语法树转化成 render function 字符串的过程)得到render函数,返回VNode。VNode是Vue的虚拟DOM节点,里面包含标签名、子节点、文本等信息。

首先看一下 mount 的代码:


/*把原本不带编译的$mount方法保存下来,在最后会调用。*/
const mount = Vue.prototype.$mount
/*挂载组件,带模板编译*/
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to html or body - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  /*处理模板templete,编译成render函数,render不存在的时候才会编译template,否则优先使用render*/
  if (!options.render) {
    let template = options.template
    /*template存在的时候取template,不存在的时候取el的outerHTML*/
    if (template) {
      /*当template是字符串的时候*/
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        /*当template为DOM节点的时候*/
        template = template.innerHTML
      } else {
        /*报错*/
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      /*获取element的outerHTML*/
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      /*将template编译成render函数,这里会有render以及staticRenderFns两个返回,这是vue的编译时优化,static静态不需要在VNode更新时进行patch,优化性能*/
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  /*Github:https://github.com/answershuto*/
  /*调用const mount = Vue.prototype.$mount保存下来的不带编译的mount*/
  return mount.call(this, el, hydrating)
}

通过 mount 代码我们可以看到,在mount的过程中,如果 render 函数不存在(render函数存在会优先使用render)会将template进行compileToFunctions 得到 render 以及 staticRenderFns。比如说手写组件时加入了template 的情况都会在运行时进行编译。而 render function 在运行后会返回 VNode 节点,供页面的渲染以及在 update 的时候 patch。接下来我们来看一下 template 是如何编译的。

AST语法树

首先,我们需要提前了解AST,因为template会被编译成AST,那么AST是什么?

在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。AST会经过 generate 得到 render 函数,render 的返回值是 VNode,VNode是Vue的虚拟DOM节点(关于虚拟DOM将在后续解读),具体定义代码如下:


export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?ArrayVNode;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  /*Github:https://github.com/answershuto*/

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?ArrayVNode,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*当前节点的标签名*/
    this.tag = tag
    /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
    this.data = data
    /*当前节点的子节点,是一个数组*/
    this.children = children
    /*当前节点的文本*/
    this.text = text
    /*当前虚拟节点对应的真实dom节点*/
    this.elm = elm
    /*当前节点的名字空间*/
    this.ns = undefined
    /*编译作用域*/
    this.context = context
    /*函数化组件作用域*/
    this.functionalContext = undefined
    /*节点的key属性,被当作节点的标志,用以优化*/
    this.key = data && data.key
    /*组件的option选项*/
    this.componentOptions = componentOptions
    /*当前节点对应的组件的实例*/
    this.componentInstance = undefined
    /*当前节点的父节点*/
    this.parent = undefined
    /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
    this.raw = false
    /*静态节点标志*/
    this.isStatic = false
    /*是否作为跟节点插入*/
    this.isRootInsert = true
    /*是否为注释节点*/
    this.isComment = false
    /*是否为克隆节点*/
    this.isCloned = false
    /*是否有v-once指令*/
    this.isOnce = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

CreateCompiler


  /*编译,将模板template编译成AST、render函数以及staticRenderFns函数*/
  function compile (
    template: string,
    options?: CompilerOptions
  ): CompiledResult {
    const finalOptions = Object.create(baseOptions)
    const errors = []
    const tips = []
    finalOptions.warn = (msg, tip) = {
      (tip ? tips : errors).push(msg)
    }

    /*做下面这些merge的目的因为不同平台可以提供自己本身平台的一个baseOptions,内部封装了平台自己的实现,然后把共同的部分抽离开来放在这层compiler中,所以在这里需要merge一下*/
    if (options) {
      // merge custom modules
      /*合并modules*/
      if (options.modules) {
        finalOptions.modules = (baseOptions.modules || []).concat(options.modules)
      }
      // merge custom directives
      if (options.directives) {
        /*合并directives*/
        finalOptions.directives = extend(
          Object.create(baseOptions.directives),
          options.directives
        )
      }
      // copy other options
      for (const key in options) {
        /*合并其余的options,modules与directives已经在上面做了特殊处理了*/
        if (key !== 'modules' && key !== 'directives') {
          finalOptions[key] = options[key]
        }
      }
    }

    /*基础模板编译,得到编译结果*/
    const compiled = baseCompile(template, finalOptions)
    if (process.env.NODE_ENV !== 'production') {
      errors.push.apply(errors, detectErrors(compiled.ast))
    }
    compiled.errors = errors
    compiled.tips = tips
    return compiled
  }

compile主要做了两件事,一件是合并option(前面说的将平台自有的option与传入的option进行合并),另一件是baseCompile,进行模板template的编译。

baseCompile


function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  /*parse解析得到AST*/
  const ast = parse(template.trim(), options)
  /*
    将AST进行优化
    优化的目标:生成模板AST,检测不需要进行DOM改变的静态子树。
    一旦检测到这些静态树,我们就能做以下这些事情:
    1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。
    2.在patch的过程中直接跳过。
 */
  optimize(ast, options)
  /*根据AST生成所需的code(内部包含render与staticRenderFns)*/
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

baseCompile首先会将模板template进行parse得到一个AST,再通过optimize做一些优化,最后通过generate得到render以及staticRenderFns。

parse


// 1.parse 解析模板字符串生成ast,源码中有2千多行

// const ast = parse(template.trim(), options)
// 对模板做解析,生成ast,对源代码抽象语法结构的树状表现形式
// 利用大量的正则表达式对字符串解析。
// tag:ul
// attrsMap:装属性
// events:装事件 'click':{'value':''}
// 除了一些自身属性,还维护了他的父子关系。parent指向父节点,children指向子节点
// html-parse函数解析模板
// 循环解析template,用正则进行各种匹配,对不同的情况分别处理,直到整个template被解析完毕,在匹配过程中利用advance函数不断的前进整个模板字符串,直到字符串末尾
// 匹配过程中主要使用了正则表达式来进行操作
// attr的匹配表达式
// 标签的开始符,标签的结束符
// 注释节点,文档类型节点

parse会用正则等方式解析template模板中的指令、class、style等数据,parse函数实现了这个转换的步骤,通过各种正则适配将 html 解析成AST。

optimize

optimize,翻译成中文是优化,而optimize完成的正是优化的过程,optimize通过对节点添加标志,从而在页面重新刷新进行vnode比对时,跳过静态节点,提高性能。


// 2.optimize(ast, options)

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,root即为通过parse阶段生成的ast抽象语法树。


// 2.1 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
        }
      }
    }
  }
}

//2.1.1 首先调用isStatic
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)
  ))
}

可以看到,如果node 是表达式,返回false,如果是文本节点,返回true,如果有v-for click等的属性也返回false,然后回到markStatic,可以看到会对node的children子节点递归调用markStaic,如果子节点不是static,就会把当前节点也置为非static,这就是markStatic的主要过程,下面我们进入markstaticRoot。


// 2.2 
function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
  
    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)
      }
    }
  }
}

optimize的主要作用是标记static静态节点,这是Vue在编译过程中的一处优化,后面当update更新界面时,会有一个patch的过程,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能。

generate

编译的最后一个步骤是generate,这里是真正的生成render函数,最后返回的是一个字符串,可由new Function或eval进行执行。


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
  }
}

主要执行了genElement方法,我们进入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
  }
}

可以看到,这时会根据不同的节点属性执行不同的方法,例如如果只是普通的节点会或返回一个code字符串,内容为’_c(xx’,_c这些方法是什么呢,其实这些方法都定义在辅助函数中。


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
}

因为情况非常多,我们就拿 v-for 来举例说明一下执行过程,实例 templete为。


// 举个栗子
ulli v-for="item of [1,2,3]"{{item}}/li/ul

v-for源码


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)}` +
    '})'
}

可以看到首先对templete类型的key进行了判断,如果没有,会进行警告,最后返回了一个_l作为函数名的执行函数,参数为定义的v-for值,函数内容为另一个genElement,最后整个templete返回的render函数内容为:


"with(this){return _c('ul',_l(([1,2,3]),function(item){return _c('li',[_v(_s(item))])}),0)}"

generate就是将AST转化成render funtion字符串的过程,得到结果是render的字符串以及staticRenderFns字符串。最后render函数在执行时就会把_c和_l等替换成真正的函数,从而返回一个vnode,再继续完成patch,插入真实dom树完成渲染。

总结

到现在为止,templete 编译过程已经完成。我们的template模板已经被转化成了我们所需的AST、render function字符串以及staticRenderFns字符串。下面借用网友的demo(源地址:http://www.jianshu.com/p/63ac3b9ede5d):


// 举个栗子
div class="main" :class="bindClass"
    div{{text}}/div
    divhello world/div
    div v-for="(item, index) in arr"
        p{{item.name}}/p
        p{{item.value}}/p
        p{{index}}/p
        p---/p
    /div
    div v-if="text"
        {{text}}
    /div
    div v-else/div
/div

转化后得到AST,如下图:

Vue2总结(八)Template模版编译

我们可以看到最外层的div是这颗AST的根节点,节点上有许多数据代表这个节点的形态,比如static表示是否是静态节点,staticClass表示静态class属性(非bind:class)。children代表该节点的子节点,可以看到children是一个长度为4的数组,里面包含的是该节点下的四个div子节点。children里面的节点与父节点的结构类似,层层往下形成一棵AST。

再来看看由AST得到的render函数:


with(this){
    return _c(  'div',
                {
                    /*static class*/
                    staticClass:"main",
                    /*bind class*/
                    class:bindClass
                },
                [
                    _c( 'div', [_v(_s(text))]),
                    _c('div',[_v("hello world")]),
                    /*这是一个v-for循环*/
                    _l(
                        (arr),
                        function(item,index){
                            return _c(  'div',
                                        [_c('p',[_v(_s(item.name))]),
                                        _c('p',[_v(_s(item.value))]),
                                        _c('p',[_v(_s(index))]),
                                        _c('p',[_v("---")])]
                                    )
                        }
                    ),
                    /*这是v-if*/
                    (text)?_c('div',[_v(_s(text))]):_c('div',[_v("no text")])],
                    2
            )
}

看了render function字符串,发现有大量的_c,_v,_s,_q,这些函数究竟是什么?直接翻源码看看。


  /* @flow */
  /*
    内部处理render的函数
    这些函数会暴露在Vue原型上以减小渲染函数大小
  */
  /*处理v-once的渲染函数*/
  Vue.prototype._o = markOnce
  /*将字符串转化为数字,如果转换失败会返回原字符串*/
  Vue.prototype._n = toNumber
  /*将val转化成字符串*/
  Vue.prototype._s = toString
  /*处理v-for列表渲染*/
  Vue.prototype._l = renderList
  /*处理slot的渲染*/
  Vue.prototype._t = renderSlot
  /*检测两个变量是否相等*/
  Vue.prototype._q = looseEqual
  /*检测arr数组中是否包含与val变量相等的项*/
  Vue.prototype._i = looseIndexOf
  /*处理static树的渲染*/
  Vue.prototype._m = renderStatic
  /*处理filters*/
  Vue.prototype._f = resolveFilter
  /*从config配置中检查eventKeyCode是否存在*/
  Vue.prototype._k = checkKeyCodes
  /*合并v-bind指令到VNode中*/
  Vue.prototype._b = bindObjectProps
  /*创建一个文本节点*/
  Vue.prototype._v = createTextVNode
  /*创建一个空VNode节点*/
  Vue.prototype._e = createEmptyVNode
  /*处理ScopedSlots*/
  Vue.prototype._u = resolveScopedSlots
}

通过源码展示的函数,render函数最后会返回一个VNode节点,在_update的时候,经过patch与之前的VNode节点进行比较,得出差异后将这些差异渲染到真实的DOM上,这里和React的DIFF算法类似。

补充

前面总结了这么多,无非是从源码角度了解到Template转换成DOM的实现,下面将手写一套函数来模拟实现 htmlParse 解析器的功能,首先要分析一下需要对哪些元素进行解析:

  • **1.元素类型**:也就是标签类型,所有用这样的标签。
  • **2.变量text:**现在我们实现一个的变量转换,它其实是一个节点。
  • **3.普通文本:**普通文本包括普通文字和空格、换行。
  • 实现思路:

    匹配单个字符 - 匹配标签 - 匹配属性 **- **匹配文本 - 匹配结束标签

    实现步骤:

  • 1.首先匹配到 字符;
  • 2.先正则匹配从 到后面的字符,若是开始标签便记录,同时用正则记录attrs;
  • 3.匹配完开始的 字符之后继续匹配
  • 4.又匹配到一个 ,继续执行上述步骤;
  • 5.发现是开始标签,再次记录,同时用正则记录attrs;
  • 6.如匹配到结束标签 ,则记录最后一个开始标签的结束,并继续执行上述步骤;
  • 7.再次匹配到结束标签,则继续记录开始标签的结束。
  • 8.循环往复,匹配、记录、结束 字符,最终完成。
  • 
    // 模拟 template
    // 转化HTML至AST对象
      function parse(template){
        var currentParent; //当前父节点
        var root; //最终生成的AST对象
        var stack = []; //插入栈
        var startStack = []; //开始标签栈
        var endStack = [];  //结束标签栈
        //console.log(template);
        parseHTML(template,{
          start:function start(targetName,attrs,unary,start,end,type,text){//标签名 ,attrs,是否结束标签,文本开始位置,文本结束位置,type,文本,
            var element = {   //我们想要的对象
              tag:targetName,
              attrsList:attrs,
              parent:currentParent,  //需要记录父对象吧
              type:type,
              children:[]
            }
            if(!root){ //根节点哈
              root = element;
            }
            if(currentParent && !unary){ //有父节点并且不是结束标签?
              currentParent.children.push(element);  //插入到父节点去
              element.parent = currentParent;  //记录父节点
            }
            if (!unary) {  //不是结束标签?
                if(type == 1){
                   currentParent = element;//不是结束标签,当前父节点就要切换到现在匹配到的这个开始标签哈,后面再匹配到
                   startStack.push(element);  //推入开始标签栈
                }
                 stack.push(element);  //推入总栈
             }else{
               endStack.push(element);  //推入结束标签栈
               currentParent = startStack[endStack.length-1].parent;   //结束啦吧当前父节点切到上一个开始标签,这能理解吧,当前这个已经结束啦
             }
             //console.log(stack,"currentstack")
          },
          end:function end(){
    
          },
          chars:function chars(){
    
          }
        });
        console.log(root,"root");
        return root;
      };
    
      // Regular Expressions for parsing tags and attributes
      var singleAttrIdentifier = /([^s"'/=]+)/;
      var singleAttrAssign = /(?:=)/;
      var singleAttrValues = [
        // attr value double quotes
        /"([^"]*)"+/.source,
        // attr value, single quotes
        /'([^']*)'+/.source,
        // attr value, no quotes
        /([^s"'=`]+)/.source
      ];
      var attribute = new RegExp(
        '^\s*' + singleAttrIdentifier.source +
        '(?:\s*(' + singleAttrAssign.source + ')' +
        '\s*(?:' + singleAttrValues.join('|') + '))?'
      );
      // could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
      // but for Vue templates we can enforce a simple charset
      var ncname = '[a-zA-Z_][\w\-\.]*';
      var qnameCapture = '((?:' + ncname + '\:)?' + ncname + ')';
      var startTagOpen = new RegExp('^' + qnameCapture);
      var startTagClose = /^s*(/?)/;
      var endTag = new RegExp('^\/' + qnameCapture + '[^]*');
      var doctype = /^!DOCTYPE [^]+/i;
      var comment = /^!--/;
      var conditionalComment = /^![/;
    
    //偷懒哈  上面的正则是我在vue上拿下来的,这个后期可以研究,下面的话简单的写两个用用,和vue原版的是有一些差别的
    
        //{{变量}}
    
      var varText = new RegExp('{{' + ncname + '}}');
      //空格与换行符
      var space = /^s/;
      var checline = /^[rn]/;
        /**
          type 1普通标签
          type 2代码
          type 3普通文本
        */
      function parseHTML(html,options){
        var stack = []; //内部也要有一个栈
        var index = 0;  //记录的是html当前找到那个索引啦
        var last; //用来比对,当这些条件都走完后,如果last==html 说明匹配不到啦,结束while循环
        var isUnaryTag = false;
    
        while(html){
          last = html;
          var textEnd = html.indexOf('');
          if(textEnd === 0){ //这一步如果第一个字符是那么就只有两种情况,1开始标签  2结束标签
            //结束标签
            var endTagMatch = html.match(endTag); //匹配
            if(endTagMatch){
              console.log(endTagMatch,"endTagMatch");
              isUnaryTag = true;
              var start = index;
              advance(endTagMatch[0].length); //匹配完要删除匹配到的,并且更新index,给下一次匹配做工作
              options.start(null,null,isUnaryTag,start,index,1);
              continue;
            }
            //初始标签
            var startMatch = parseStartTag();
            if(startMatch){
              parseStartHandler(startMatch);//封装处理下
              console.log(stack,"startMatch");
              continue;
            }
          }
    
          if(html === last){
            console.log(html,"html");
           break;
          }
        }
        function advance (n) {
          index += n;
          html = html.substring(n);
        }
        //处理起始标签 主要的作用是生成一个match 包含初始的attr标签
        function parseStartTag(){
          var start = html.match(startTagOpen);
          if(start){
            var match = {
               tagName: start[1],       // 标签名(div)
               attrs: [],               // 属性
               start: index             // 游标索引(初始为0)
           };
           advance(start[0].length);
           var end, attr;
           while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {//在endClose之前寻找attribute
               advance(attr[0].length);
               match.attrs.push(attr);
           }
           if (end) {
               advance(end[0].length);      // 标记结束位置
               match.end = index;      //这里的index 是在 parseHTML就定义 在advance里面相加
               return match         // 返回匹配对象 起始位置 结束位置 tagName attrs
           }
    
          }
        }
        //对match进行二次处理,生成对象推入栈
        function parseStartHandler(match){
          var _attrs = new Array(match.attrs.length);
          for(var i=0,len=_attrs.length;ilen;i++){  //这儿就是找attrs的代码哈
            var args = match.attrs[i];
            var value = args[3] || args[4] || args[5] || '';
            _attrs[i] = {
              name:args[1],
              value:value
            }
          }
          stack.push({tag: match.tagName,type:1, lowerCasedTag: match.tagName.toLowerCase(), attrs: _attrs}); //推栈
          options.start(match.tagName, _attrs,false, match.start, match.end,1);  //匹配开始标签结束啦。
        }
    
      }
    

    总结

    vue中的templete最后都会编译成render函数来执行,编译的过程主要由parse,optimize,generate组成。最后手写一个 htmlParse 解析器作为总结。本文的实例代码地址:https://github.com/GitLuoSiyu/vue-learn

    最后

    今天的 Vue2总结(八)AST语法树 就分享到这里,我的公众号没有留言功能哈,有问题大家心里默念,我能感受到,谢谢 ~ 

    原文始发于微信公众号(程序员思语):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> Vue2总结(八)Template模版编译


     上一篇
    Vue2总结(九)虚拟DOM Vue2总结(九)虚拟DOM
    目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 65*
    2021-04-05
    下一篇 
    Vue2总结(七)Vue事件机制 Vue2总结(七)Vue事件机制
    目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 30*
    2021-04-05