首页 > 互联资讯 > 技术交流  > 

Vue2模版编译流程详解

目录
  • 1、起步
    • 项目结构
    • App.vue
    • webpack.config.js
    • 打包构建
    • vue-loader 源码
    • template-loader
  • 2、模板编译流程
    • parseHTML 阶段
    • genElement 阶段
    • compilerToFunction 阶段
    • parseHTML 阶段
    • genElement 阶段
  • 3、总结

    为了更好理解 vue 的模板编译这里我整理了一份模板编译的整体流程,如下所示,下面将用源码解读的方式来找到模板编译中的几个核心步骤,进行详细说明:

    1、起步

    这里我使用 webpack 来打包 vue 文件,来分析 vue 在模板编译中的具体流程,如下所示,下面是搭建的项目结构和文件内容:

    项目结构

    ├─package-lock.json
    ├─package.json
    ├─src
    |  ├─App.vue
    |  └index.js
    ├─dist
    |  └main.js
    ├─config
    |   └webpack.config.js

    App.vue

    
    ​
    
    ​
    

    webpack.config.js

    const { VueLoaderPlugin } = require('vue-loader')
    ​
    module.exports = {
      mode: 'development',
      module: {
        rules: [
          {
            test: /.vue$/,
            loader: 'vue-loader'
          },
          // 它会应用到普通的 `.js` 文件
          // 以及 `.vue` 文件中的 `n' +
        'export default {n' +
        '  props: {},n' +
        '  data() {n' +
        '    return {n' +
        '      count: 0n' +
        '    }n' +
        '  }n' +
        '}n' +
        'n' +
        'n' +
        'n',
      filename: 'App.vue',
      template: {
        type: 'template',
        content: 'n
    n {{ count }}n
    n',   start: 10,   end: 53,   attrs: {} }, script: {   type: 'script',   content: 'n' +     'export default {n' +     ' props: {},n' +     ' data() {n' +     '   return {n' +     '     count: 0n' +     '   }n' +     ' }n' +     '}n',   start: 74,   end: 156,   attrs: {} }, .... }

    template-loader

    template-loader 的作用是将 import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&" 模块编译成 render 函数并导出,以下是编译产物:

    // 编译前
    
    {{ count }}
    ​ // 编译后 var render = function render() { var _vm = this,   _c = _vm._self._c return _c("div", { attrs: { id: "box" } }, [   _vm._v("n " + _vm._s(_vm.count) + "n"), ]) } var staticRenderFns = [] render._withStripped = true ​ export { render, staticRenderFns }

    template-loader 核心原理是通过 vue/compiler-sfc 将模板转换成为 render 函数,并返回 template 编译产物

    module.exports = function (source) {
      const loaderContext = this
        ...
      // 接收模板编译核心库
      const { compiler, templateCompiler } = resolveCompiler(ctx, loaderContext)
    ​
        ...
    ​
      // 开启编译
      const compiled = compiler.compileTemplate(finalOptions)
    ​
        ...
    ​
      // 编译后产出,code就是render函数
      const { code } = compiled
    ​
      // 导出template模块
      return code + `nexport { render, staticRenderFns }`
    }

    2、模板编译流程

    vue/compiler-sfc 是模板编译的核心库,在 vue2.7 版本中使用,而 vue2.7 以下的版本都是使用vue-template-compiler,本质两个包的功能是一样的,都可以将模板语法编译为 JavaScript,接下来我们来解析一下在模板编译过程中使用的方法:

    parseHTML 阶段

    可以将 vue 文件中的模板语法转义为 AST,为后续创建 dom 结构做预处理

    export function parseHTML(html, options: HTMLParserOptions) {
      // 存储解析后的标签
      const stack: any[] = []
      const expectHTML = options.expectHTML
      const isUnaryTag = options.isUnaryTag || no
      const canBeLeftOpenTag = options.canBeLeftOpenTag || no
      let index = 0
      let last, lastTag
      // 循环 html 字符串结构
      while (html) {
        // 记录当前最新html
        last = html
        if (!lastTag || !isPlainTextElement(lastTag)) {
          // 获取以 < 为开始的位置
          let textEnd = html.indexOf('<')
          if (textEnd === 0) {
            // 解析注释
            if (comment.test(html)) {
              const commentEnd = html.indexOf('-->')
              if (commentEnd >= 0) {
                if (options.shouldKeepComment && options.comment) {
                  options.comment(
                    html.substring(4, commentEnd),
                    index,
                    index + commentEnd + 3
                  )
                }
                advance(commentEnd + 3)
                continue
              }
            }
            // 解析条件注释
            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
            }
            // 解析截取结束标签
            const endTagMatch = html.match(endTag)
            if (endTagMatch) {
              const curIndex = index
              advance(endTagMatch[0].length)
              parseEndTag(endTagMatch[1], curIndex, index)
              continue
            }
            // 解析截取开始标签
            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(
              '([sS]*?)(]*>)',
              'i'
            ))
          const rest = html.replace(reStackedTag, function (all, text, endTag) {
            endTagLength = endTag.length
            if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
              text = text
                .replace(//g, '$1') // #7298
                .replace(//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)
          break
        }
      }
      // 清空闭合标签
      parseEndTag()
      // 截取标签,前后推进位置
      function advance(n) {
        index += n
        html = html.substring(n)
      }
      // 解析开始标签
      function parseStartTag() {
        const start = html.match(startTagOpen)
        if (start) {
          const match: any = {
            tagName: start[1],
            attrs: [],
            start: index
          }
          advance(start[0].length)
          let end, attr
          while (
            !(end = html.match(startTagClose)) &&
            (attr = html.match(dynamicArgAttribute) || 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
          }
        }
      }
      // 匹配处理开始标签
      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: ASTAttr[] = 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 (__DEV__ && 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)
        }
      }
      // 解析结束标签
      function parseEndTag(tagName?: any, start?: any, end?: any) {
        let pos, lowerCasedTagName
        if (start == null) start = index
        if (end == null) end = index
        // Find the closest opened tag of the same type
        if (tagName) {
          lowerCasedTagName = tagName.toLowerCase()
          for (pos = stack.length - 1; pos >= 0; pos--) {
            if (stack[pos].lowerCasedTag === lowerCasedTagName) {
              break
            }
          }
        } else {
          // If no tag name is provided, clean shop
          pos = 0
        }
        if (pos >= 0) {
          // Close all the open elements, up the stack
          for (let i = stack.length - 1; i >= pos; i--) {
            if (__DEV__ && (i > pos || !tagName) && options.warn) {
              options.warn(`tag <${stack[i].tag}> has no matching end tag.`, {
                start: stack[i].start,
                end: stack[i].end
              })
            }
            if (options.end) {
              options.end(stack[i].tag, start, end)
            }
          }
          // Remove the open elements from the stack
          stack.length = pos
          lastTag = pos && stack[pos - 1].tag
        } else if (lowerCasedTagName === 'br') {
          if (options.start) {
            options.start(tagName, [], true, start, end)
          }
        } else if (lowerCasedTagName === 'p') {
          if (options.start) {
            options.start(tagName, [], false, start, end)
          }
          if (options.end) {
            options.end(tagName, start, end)
          }
        }
      }
    }

    genElement 阶段

    genElement 会将 AST 预发转义为字符串代码,后续可将其包装成 render 函数的返回值

    // 将AST预发转义成render函数字符串
    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) {
          // 处理v-once指令
        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 {
        // 处理组件和dom元素
           ...
        return code
      }
    }

    通过genElement函数包装处理后,将vue 模板的 template 标签部分转换为 render 函数,如下所示:

    const compiled = compiler.compileTemplate({
      source: 'n' +
        '
    n' + ' {{ count }}n' + ' n' + '
    n' }); const { code } = compiled; // 编译后 var render = function render() { var _vm = this, _c = _vm._self._c return _c("div", { attrs: { id: "box" } }, [ _vm._v("n " + _vm._s(_vm.count) + "n "), _c("button", { on: { add: _vm.handleAdd } }, [_vm._v("+")]), ]) } var staticRenderFns = [] render._withStripped = true

    compilerToFunction 阶段

    genElement 阶段编译的字符串产物,通过 new Function将 code 转为函数

    export function createCompileToFunctionFn(compile: Function): Function {
      const cache = Object.create(null)
      return function compileToFunctions(
        template: string,
        options?: CompilerOptions,
        vm?: Component
      ): CompiledFunctionResult {
        ...
        // 编译
        const compiled = compile(template, options)
        // 将genElement阶段的产物转化为function
        function createFunction(code, errors) {
          try {
            return new Function(code)
          } catch (err: any) {
            errors.push({ err, code })
            return noop
          }
        }
        const res: any = {}
        const fnGenErrors: any[] = []
        // 将code转化为function
        res.render = createFunction(compiled.render, fnGenErrors)
        res.staticRenderFns = compiled.staticRenderFns.map(code => {
          return createFunction(code, fnGenErrors)
        })
        ...
      }
    }

    为了方便理解,使用断点调试,来看一下 compileTemplate 都经历了哪些操作:

    首先会判断是否需要预处理,如果需要预处理,则会对 template 模板进行预处理并返回处理结果,此处跳过预处理,直接进入 actuallCompile 函数

    这里可以看到本身内部还有一层编译函数对 template 进行编译,这才是最核心的编译方法,而这个 compile 方法来源于 createCompilerCreator

    createCompilerCreator 返回了两层函数,最终返回值则是 compile 和 compileToFunction,这两个是将 template 转为 render 函数的关键,可以看到 template 会被解析成 AST 树,最后通过 generate 方法转义成函数 code,接下来我们看一下parse函数中是如何将 template 转为 AST 的。

    继续向下 debug 后,会走到 parseHTML 函数,这个函数是模板编译中用来解析 HTML 结构的核心方法,通过回调 + 递归最终遍历整个 HTML 结构并将其转化为 AST 树。

    parseHTML 阶段

    使用 parseHTML 解析成的 AST 创建 render 函数和 Vdom

    genElement 阶段

    将 AST 结构解析成为虚拟 dom 树

    最终编译输出为 render 函数,得到最终打包构建的产物。

    3、总结

    到此我们应该了解了 vue 是如何打包构建将模板编译为渲染函数的,有了渲染函数后,只需要将渲染函数的 this 指向组件实例,即可和组件的响应式数据绑定。vue 的每一个组件都会对应一个渲染 Watcher ,他的本质作用是把响应式数据作为依赖收集,当响应式数据发生变化时,会触发 setter 执行响应式依赖通知渲染 Watcher 重新执行 render 函数做到页面数据的更新。

    以上就是Vue2模版编译流程详解的详细内容,更多关于Vue2模版编译的资料请关注讯客其它相关文章!

    Vue2模版编译流程详解由讯客互联技术交流栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“Vue2模版编译流程详解