vue 编译过程,由templete编译成render函数

在vue中html代码可以用templete来写,这也是我们常用的方式,也可以直接写render函数,而在最后vue源码执行过程中,都是执行的render函数,templete转化为render函数这一过程在实际开发中一般由webpack完成,而如果是以vue带编译版本进行开发,则会有把templete转化为render这一过程,这一过程比较耗时,所以实际开发过程中我们都是使用的不带编译版本,编译过程由打包工具完成,
下面我们通过vue js的源码来看一下templete转化为render函数的整体过程,
我们先回到之前的执行挂载到dom时执行的$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
  if (!options.render) {
    let template = options.template
    if (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 = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

可以看到如果dom节点不是定义的render函数而是定义的templete模板的话,就会执行compileToFunctions函数,并传入一些参数,我们进入compileToFunctions函数

export const createCompiler = createCompilerCreator(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
  }
})

经过一系列的函数转化后,最后其实执行的就是这个baseCompile方法,对templete模板进行转化,通过parse函数进行抽象语法树的转换,对抽象语法树ast进行配置合并,最后把抽象语法树转化为render函数,整体流程就是这三个环节,下面我们一个一个来分析,,
首先是parse,我们进入parse

warn = options.warn || baseWarn

  platformIsPreTag = options.isPreTag || no
  platformMustUseProp = options.mustUseProp || no
  platformGetTagNamespace = options.getTagNamespace || no
  const isReservedTag = options.isReservedTag || no
  maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)

  transforms = pluckModuleFunction(options.modules, 'transformNode')
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

  delimiters = options.delimiters

  const stack = []
  const preserveWhitespace = options.preserveWhitespace !== false
  const whitespaceOption = options.whitespace
  let root
  let currentParent
  let inVPre = false
  let inPre = false
  let warned = false

可以看到有定义一个栈用来匹配标签的对应关系,当遇到对应关系时使用栈作为数据结构非常合适,比如括号合法性这种,以后遇到这种对应合法性类的算法大家可以首先想到栈这个数据结构
有定义一个root对象,这就是ast的根节点,parse中还有定义许多其他的辅助函数,比较多没有都列出来,我们再看下start函数的定义,在之后处理完开始标签后会用这个函数进行处理

start (tag, attrs, unary, start) {
      // check namespace.
      // inherit parent ns if there is one
      const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

      // handle IE svg bug
      /* istanbul ignore if */
      if (isIE && ns === 'svg') {
        attrs = guardIESVGBug(attrs)
      }

      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      if (ns) {
        element.ns = ns
      }

      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        element.start = start
        element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
          cumulated[attr.name] = attr
          return cumulated
        }, {})
      }

      if (isForbiddenTag(element) && !isServerRendering()) {
        element.forbidden = true
        process.env.NODE_ENV !== 'production' && warn(
          'Templates should only be responsible for mapping the state to the ' +
          'UI. Avoid placing tags with side-effects in your templates, such as ' +
          `<${tag}>` + ', as they will not be parsed.',
          { start: element.start }
        )
      }

      // apply pre-transforms
      for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
      }

      if (!inVPre) {
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        processFor(element)
        processIf(element)
        processOnce(element)
      }

      if (!root) {
        root = element
        if (process.env.NODE_ENV !== 'production') {
          checkRootConstraints(root)
        }
      }

      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        closeElement(element)
      }
    },

首先构造ast,这里通过currentparent完成父子关系的嵌套,以后的每个子标签的开始处理都会调用这个start函数,从而能够构造出父子关系,之后还有对v-if v-for的属性的额外处理,内容都很多,我们知道这个流程就好了
有对不合法标签的处理如script style等,对预定义标签的处理,
parse在初始化完成后主要执行了parseHtml,我们来看代码

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeLetters}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i

在定义parseHtml之前定义了一些正则表达式,这写正则就是用来匹配templete字符串中的标签,属性等的,例如<p class="text">123</p>
这个templete字符串在解析过程中 开始标签 <p通过startTagOpen正则匹配,class属性通过attribute正则匹配,结束标签</p>通过endTag匹配,我们知道了怎么匹配,接下来进入匹配流程

while (html) {
    last = html
    // Make sure we're not in a plaintext content element like script/style
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
        // Comment:
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')

          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            advance(commentEnd + 3)
            continue
          }
        }

        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf(']>')

          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue
          }
        }

        // Doctype:
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }

        // End tag:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }

        // Start tag:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
          continue
        }
      }

      let text, rest, next
      if (textEnd >= 0) {
        rest = html.slice(textEnd)
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
          // < in plain text, be forgiving and treat it as text
          next = rest.indexOf('<', 1)
          if (next < 0) break
          textEnd += next
          rest = html.slice(textEnd)
        }
        text = html.substring(0, textEnd)
      }

      if (textEnd < 0) {
        text = html
      }

      if (text) {
        advance(text.length)
      }

      if (options.chars && text) {
        options.chars(text, index - text.length, index)
      }
    } else {
      let endTagLength = 0
      const stackedTag = lastTag.toLowerCase()
      const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
      const rest = html.replace(reStackedTag, function (all, text, endTag) {
        endTagLength = endTag.length
        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
          text = text
            .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
            .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
        }
        if (shouldIgnoreFirstNewline(stackedTag, text)) {
          text = text.slice(1)
        }
        if (options.chars) {
          options.chars(text)
        }
        return ''
      })
      index += html.length - rest.length
      html = rest
      parseEndTag(stackedTag, index - endTagLength, index)
    }

    if (html === last) {
      options.chars && options.chars(html)
      if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
        options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })
      }
      break
    }
  }

while循环每次以匹配<开始,当匹配到开始标签时,就会进入到parseStartTag()的处理,我们来看下代码,

function parseStartTag () {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
        start: index
      }
      advance(start[0].length)
      let end, attr
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        attr.start = index
        advance(attr[0].length)
        attr.end = index
        match.attrs.push(attr)
      }
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }

可以看到,从开始标签<起,先是向前移动tag的长度的数量,advance就是移动位置的函数,

function advance (n) {
    index += n
    html = html.substring(n)
  }

然后通过一个while循环进行属性处理的部分,组装成一个数组,赋值给match对象的attrs属性,一直到匹配到>结束标签才结束循环,最后返回一个match对象,包含标签名属性数组和开始位置,之后会调用handleStartTag方法,对返回的数组进行处理,我们进入handleStartTag方法

 function handleStartTag (match) {
    const tagName = match.tagName
    const unarySlash = match.unarySlash

    if (expectHTML) {
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        parseEndTag(lastTag)
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName)
      }
    }

    const unary = isUnaryTag(tagName) || !!unarySlash

    const l = match.attrs.length
    const attrs = new Array(l)
    for (let i = 0; i < l; i++) {
      const args = match.attrs[i]
      const value = args[3] || args[4] || args[5] || ''
      const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
        ? options.shouldDecodeNewlinesForHref
        : options.shouldDecodeNewlines
      attrs[i] = {
        name: args[1],
        value: decodeAttr(value, shouldDecodeNewlines)
      }
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        attrs[i].start = args.start + args[0].match(/^\s*/).length
        attrs[i].end = args.end
      }
    }

    if (!unary) {
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
      lastTag = tagName
    }

    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

最后会执行options.start 也就是之前分析的start中的处理,在那里完成父子关系的构建和一些特殊属性的处理,
开始标签处理结束后返回到parseHtml的while循环,继续匹配下一个开始标签<,如果直接匹配到结束标签,对结束标签进行处理,如果未直接处理到结束标签也没直接处理到开始标签,说明是一个文本节点,就会执行文本节点的处理,
当所有标签都被处理完成也就是templete字符串已经advance到最后,就会把parse中定义的root根节点返回,此抽象语法树构造完成。
最后构建出的结果是这样

{type: 1, tag: "h1", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …}
attrsList: []
attrsMap: {class: "top"}
children: Array(2)
0: {type: 3, text: "123", start: 16, end: 19, static: true}
1: {type: 1, tag: "p", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …}
length: 2
__proto__: Array(0)
end: 34
parent: undefined
plain: false
rawAttrsMap: {class: {…}}
start: 0
static: true
staticClass: ""top""
staticInFor: false
staticProcessed: true
staticRoot: true
tag: "h1"
type: 1
__proto__: Object

原templete代码为

'<h1 class="top">123<p>222</p></h1>'

当然这只是parse的过程,鉴于篇幅原因,
之后还有optimize和generate我们下节在讲

推荐阅读更多精彩内容

  • 前几天想学学Vue中怎么编写可复用的组件,提到要对Vue的render函数有所了解。可仔细一想,对于Vue的ren...
    kangaroo_v阅读 29,106评论 4 74
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 3,437评论 0 30
  • # 传智播客vue 学习## 1. 什么是 Vue.js* Vue 开发手机 APP 需要借助于 Weex* Vu...
    再见天才阅读 2,562评论 0 6
  • 如果可以,我也愿意做一个船员 终年守在我的孤岛上 每天被从东方涌来的金色波浪叫醒 那里没有信号,于是我只能写信 托...
    孤轴阅读 39评论 0 0
  • 我不停地走着 沿着水泥路,一直走着 夜晚的寒风 刮着我的脸生疼生疼 吹乱了我的头发,吹散了我的衣裳 干嘛总是这样,...
    李莉NI阅读 19评论 0 0