美文网首页程序员
vue 编译过程,由templete编译成render函数

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

作者: 臣以君纲 | 来源:发表于2019-02-21 00:41 被阅读0次

    在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 编译过程,由templete编译成render函数

        本文链接:https://www.haomeiwen.com/subject/owaqyqtx.html