Vue 2.6 源码剖析-模板编译

模板编译介绍

模板编译的主要目的是将模板(template)转换为渲染函数(render)。

模板编译的作用

  • Vue 2.x 使用 VNode 描述视图以及各种交互,用户自己编写 VNode (直接写render,调用h函数)比较复杂

  • 用户只需要编写类似 HTML 的代码 - Vue.js 模板,通过编译器将模板转换为范围 VNode 的render函数

  • .vue文件(SFC)会被webpack在构建的过程中转换成render函数

    • 内部通过 vue-loader 实现

体验模板编译的结果

<div id="app">
  <h1>Vue<span>模板编译过程</span></h1>
  <p>{{ msg }}</p>
  <comp @myclick="handler"></comp>
</div>
<script src="../../dist/vue.js"></script>
<script>
  Vue.component('comp', {
    template: '<div>I am a comp</div>'
  })
  const vm = new Vue({
    el: "#app",
    data: {
      msg: 'Hello compiler'
    },
    methods: {
      handler() {
        console.log('test')
      }
    }
  });
  console.log(vm.$options.render)
</script>

找到模板

入口文件 entry-runtime-with-compiler.js 中会先判断用户是否定义了render(当前没有)。

然后判断是否定义了 template 选项(当前没有)。

然后判断是否定义了 el 选项(当前有)。

然后获取 el 的 outerHTML 作为模板(template)。

然后通过 compileToFunctions把 template 转换为 render 函数。

编译生成的render函数

格式化后的打印结果:

(function () {
  // with:在代码块中使用成员时可以省略this
  with (this) {
    return _c(
      "div", // tag
      { attrs: { id: "app" } }, // data
      [
        _m(0),
        _v(" "),
        _c("p", [_v(_s(msg))]),
        _v(" "),
        _c("comp", { on: { myclick: handler } }),
      ], // children
      1 // children处理方式
    );
  }
});
  • _m用于渲染静态内容,在处理模板的过程中,会对静态的内容做优化的处理。
    • 当前处理的h1标签
  • _v 创建空白的文本节点
    • 当前处理的p标签前后的换行
  • _c 创建vnode
    • p标签只有文本内容,所以传两个参数,第二个参数是文本内容(会被包裹成数组形式的children)
    • comp组件有事件没有内容,所以传两个参数,第二个参数是数据属性(data)
  • _s 把用户输入的数据转换成字符串
    • 它对几种特殊情况做了判断处理,不是JS原生的toString方法
    • 如果是纯文本,不会调用_s

编译生成的下划线开头的函数的位置

  • _c createElement 函数

    • src\core\instance\render.js
    // 对编译生成的 render 进行渲染的方法
    // _c是在 template 选项转换成的 render 函数中调用
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
    
  • _m/_v/_s

    • src\core\instance\render.js -> src\core\instance\render-helpers\index.js
    // 这里的方法都和渲染相关,将来在编译的时候使用
    // 在把模板编译成render函数时,render函数会调用这些方法
    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
    }
    

Vue Template Explorer

Vue Template Explorer(Vue 2.x) 是一个在线工具。

可以把HTML模板转换成render函数。

可以通过它来学习render函数。

tempalte模板:

<div id="app">
  {{ msg }}
  
  
  换行
  
  
  空    格
</div>

转换结果:

function render() {
  with (this) {
    return _c(
      "div",
      {
        attrs: {
          id: "app",
        },
      },
      [_v("\n  " + _s(msg) + "\n  \n  \n  换行\n  \n  \n  空    格\n")]
    );
  }
}

Vue 2 的render会原封不动的保留模板中的空格和换行(\n)

  • 尽管对展示来说没有任何意义,只会占用内存
  • 开发时可以把这些无意义的空格和换行去掉,从而提高性能

Vue 3 Template Explorer 的 render 已经移除了空白,不用考虑这个问题,Vue 3 转换结果:

import {
  toDisplayString as _toDisplayString,
  createVNode as _createVNode,
  openBlock as _openBlock,
  createBlock as _createBlock,
} from "vue";

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock(
      "div",
      { id: "app" },
      _toDisplayString(_ctx.msg) + " 换行 空 格 ",
      1 /* TEXT */
    )
  );
}

// Check the console for the AST

模板编译的入口

入口文件 entry-runtime-with-compiler.js 中通过compileToFunctions把模板编译成render,并返回。

compileToFunctions 是由 createCompiler 生成,传入了和web平台相关的选项。

// src\platforms\web\compiler\index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
// src\platforms\web\compiler\options.js
export const baseOptions: CompilerOptions = {
  expectHTML: true, // 期望的是HTML的内容
  modules, // 模块
  directives, // 指令
  isPreTag, // 是否是pre标签
  isUnaryTag, // 是否是自闭合标签
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag, // 是否是HTML的保留标签
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}

模块:处理 类样式、行内样式 以及 处理和v-if一起使用的v-model

// src\platforms\web\compiler\modules\index.js
import klass from './class'
import style from './style'
import model from './model'

export default [
  klass,
  style,
  model
]

指令:处理 v-model、v-text、v-html 指令

// src\platforms\web\compiler\directives\index.js
import model from './model'
import text from './text'
import html from './html'

export default {
  model,
  text,
  html
}

createCompiler

createCompiler在 src\compiler\index.js 中定义,是和平台无关的代码。

它又是通过一个函数(createCompilerCreator)返回,传入了一个核心的函数(baseCompile)。

baseCompile 接收两个参数:

  1. template 模板
  2. options 合并后的选项

baseCompile 内部做了3件事情:

  1. 把模板编译成AST(抽象语法树)
  2. 优化抽象语法树
  3. 把抽象语法树转换成字符串形式的JS代码

稍后再来看这个方法

createCompilerCreator

createCompilerCreator 中返回了 createCompiler 函数。

createCompiler 函数中定义了 compile 函数。

compile 接收两个参数:

  1. template 模板
  2. options 用户传入的选项

createCompilerCreator 内部会将 和平台相关的选项(baseOptions) 与 用户传入的选项 进行合并。

然后调用 baseCompile 并传递合并后的选项。

这是通过函数返回一个函数的目的:

  • 把两种选项都准备好。
  • 最后调用 baseCompile 去编译模板。

createCompiler 最终返回并创建了 compileToFunctions 函数。

compileToFunctions 就是模板编译的入口,也是通过一个函数(createCompileToFunctionFn)创建的。

稍后来看。

总结过程

  1. 完整版的入口中调用了 compileToFunctions 把模板编译成render函数

  2. compileToFunctions(template, {} ,this) 是由 createCompiler 生成的

  3. createCompiler(baseOptions) 是由 createCompilerCreator 生成的

    1. 接收和平台相关的选项参数 baseOptions

    2. 定义了 compile 函数

      1. compile(template, options)
        1. 接收两个参数
          1. template 模板
          2. options 用户传入的选项
        2. 定义:
          1. 内部首先会把平台相关的选项(baseOptions) 和 用户传入的选项(options ) 合并:finalOptions
          2. 然后调用 baseCompile 把 finalOptions 传入
          3. 最终返回这个 baseCompile 返回的对象(compiled)
    3. 最终返回 compile 和 compileToFunctions 函数

      1. compileToFunctions 是整个模板编译的入口,它是由 createCompileToFunctionFn 生成
        1. createCompileToFunctionFn(compile)
          1. 接收 createCompiler 定义的 compile 函数作为参数
          2. 内部定义了 compileToFunctions 函数并返回。
  4. createCompilerCreator(function baseCompile(){})

    1. 接收一个 baseCompile 函数作为参数
      1. baseCompile(template, finalOptions)
        1. 接收两个参数
          1. template 模板
          2. finalOptions 合并之后的选项
        2. 它是模板编译的核心函数,内部主要做了3件事情:
          1. parse:把模板解析成AST(抽象语法树)
          2. optimize:优化抽象语法树
          3. generate:把优化后的抽象语法树转换成字符串形式的JS代码
        3. 最终返回一个对象(compiled)
    2. 最后返回 createCompiler

模板编译的过程

compileToFunctions 入口函数

src\compiler\create-compiler.js 中 createCompilerCreator 返回的 createCompiler 最终返回了 compileToFunctions。

compileToFunctions 是通过 createCompileToFunctionFn(compile) 生成的。

所以 compileToFunctions 是在 src\compiler\to-function.js 的 createCompileToFunctionFn 中定义并返回的。

compileToFunctions 的核心就是:

  1. 在缓存中找编译结果,如果有就直接返回
  2. 没有的话,开始编译,并且把编译的字符串形式的JS代码转化成函数形式
  3. 最后缓存并且返回
// src\compiler\to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
  // 创建一个不带原型的对象
  // 目的是通过闭包缓存编译之后的结果
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 克隆 options(Vue中的options选项)
    // 目的是防止污染 Vue 中的 options
    options = extend({}, options)
    // 获取 warn 函数:开发环境中用于在控制台发送警告
    const warn = options.warn || baseWarn
    delete options.warn

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }

    // check cache
    // 1. 判断缓存中是否有编译的结果(读取缓存中的 CompiledFunctionResult 对象)
    // 如果有,直接把编译的结果返回,不需要编译
    // key 是模板
    // options.delimiters
    //   只有完整版的Vue才有,只有编译的时候才会使用到
    //   它的作用是改变插值表达式的符号(详细查看官方文档)
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile
    // 2. 调用 compile 把模板编译成编译对象{render, staticRenderFns, errors, tips}
    // render 存储的是字符串形式的js代码
    // errors 和 tips 是辅助性属性,在编译模板过程中收集遇到的错误和信息,在这里把这些信息打印出来
    const compiled = compile(template, options)

    // check compilation errors/tips
    // 打印错误和信息
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        if (options.outputSourceRange) {
          compiled.errors.forEach(e => {
            warn(
              `Error compiling template:\n\n${e.msg}\n\n` +
              generateCodeFrame(template, e.start, e.end),
              vm
            )
          })
        } else {
          warn(
            `Error compiling template:\n\n${template}\n\n` +
            compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
            vm
          )
        }
      }
      if (compiled.tips && compiled.tips.length) {
        if (options.outputSourceRange) {
          compiled.tips.forEach(e => tip(e.msg, vm))
        } else {
          compiled.tips.forEach(msg => tip(msg, vm))
        }
      }
    }

    // turn code into functions
    const res = {}
    const fnGenErrors = []

    // 3. 调用 createFunction 把字符串形式的js代码转换成函数
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    // 打印错误和信息
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }

    // 4. 把编译的结果缓存并返回
    return (cache[key] = res)
  }
}

createFunction
// src\compiler\to-function.js
function createFunction (code, errors) {
  try {
    // 通过 new Function 把字符串转换成函数
    return new Function(code)
  } catch (err) {
    // 如果失败还会收集错误信息,并返回一个空函数
    errors.push({ err, code })
    return noop
  }
}

compile

它的核心就是合并选项(baseOptions 和 options),调用 baseCompile 进行编译,最后记录错误和信息,返回编译好的对象。

// src\compiler\create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
  // baseOptions: 和平台相关的选项
  return function createCompiler (baseOptions: CompilerOptions) {
    /**
     * 把模板编译成字符串形式的JS代码
     * @param {*} template 模板
     * @param {*} options 用户传入的选项(调用compileToFunctions时传入的)
     */
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      // 以 baseOptions 为原型创建 finalOptions
      // finalOptions 的作用是用来合并 baseOptions 和 options
      const finalOptions = Object.create(baseOptions)
      // errors 和 tips 用于存储编译过程中出现的错误和信息
      const errors = []
      const tips = []

      // 定义warn函数:用于把消息放入对应的数组中
      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }

      if (options) {
        // 如果 options 存在,就合并 baseOptions 和 options
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\s*/)[0].length

          warn = (msg, range, tip) => {
            const data: WarningMessage = { msg }
            if (range) {
              if (range.start != null) {
                data.start = range.start + leadingSpaceLength
              }
              if (range.end != null) {
                data.end = range.end + leadingSpaceLength
              }
            }
            (tip ? tips : errors).push(data)
          }
        }
        // merge custom modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      finalOptions.warn = warn

      // 调用 baseCompile 返回对象 {render, staticRenderFns}
      //   render 存储的是字符串形式的js代码
      // baseCompile 是模板编译的核心函数
      // baseCompile 内部还会把编译遇到的错误和信息记录下来
      //  调用 finalOptions.warn 收集 errors 和 tips
      const compiled = baseCompile(template.trim(), finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        detectErrors(compiled.ast, warn)
      }
      // 将 errors 和 tips 记录到 compiled 对应的属性
      compiled.errors = errors
      compiled.tips = tips
      // 返回 compiled 对象
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

baseCompile - AST

在 compile 中合并完选项,开始调用 baseCompile 编译模板。

baseCompile 是模板编译的核心函数。

内部代码非常清晰,把不同功能的代码,拆分到不同的函数中进行处理,让代码的结构更清晰。

内部就做了3件事情:

  1. parse:把模板解析成AST(抽象语法树)
  2. optimize:优化抽象语法树
  3. generate:把优化后的抽象语法树转换成字符串形式的JS代码
// src\compiler\index.js
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1. 调用 parse 函数把模板字符串转换成抽象语法树 AST
  // 抽象语法树(AST):用来以树形的方式描述代码结构
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 2. 调用 optimize 优化抽象语法树
    optimize(ast, options)
  }
  // 3. 调用 generate 把抽象语法树转换成字符串形式的JS代码
  const code = generate(ast, options)

  // 最终返回一个对象
  return {
    ast,
    // 渲染函数(这里是字符串形式的render,不是最终调用的render,最终还要通过 createFunction 转换成函数的形式)
    render: code.render,
    // 静态渲染函数,生成静态 VNode 树
    staticRenderFns: code.staticRenderFns
  }
})

AST 抽象语法树

什么是抽象语法树
  • AST:Abstract Syntax Tree
  • 使用对象的形式描述树形的代码结构
    • 对象中记录父子节点,形成树的结构
  • 此处的抽象语法树是用来描述树形结构的 HTML 字符串
    • 先把HTML转化成字符串,然后记录标签的必要属性,以及解析Vue中的一些指令并记录到 AST
为什么使用抽象语法树
  • 模板字符串转换成 AST 后,可以通过 AST 对模板做优化处理
    • 标记模板中的静态内容
    • 静态内容:内容是纯文本的标签
  • 标记模板中的静态内容,在 patch 的时候直接跳过静态内容
  • 在 patch 的过程中静态内容不需要对比和重新渲染,从而优化性能

babel 对代码进行降级处理的时候,也是会把代码转化成 AST ,再把 AST 转化成降级之后的 JS 代码。

查看 AST 的工具

astexplorer 可以查看各种解析器生成的 AST。

可以选择语言(Vue) 和 解析器。

  • @vue/compiler-core Vue 3 中的解析器
  • vue-template-compiler Vue 2 中的解析器

在这里插入图片描述

属性介绍:

  • type 记录节点的类型
    • 1 - 标签
    • 3 - 文本
  • tag 标签名
  • attrsList、attrsMap、rawAttrsMap 记录标签中的属性
  • children 记录子节点
  • parent 记录父节点(Vue中会生成,这里没显示)
    • AST通过记录父子节点形成树的形式
  • static 标签当前节点是静态的

parse 生成AST的过程

parse的作用是把模板字符串转换成AST对象。

这个过程比较复杂,Vue内部还借鉴了一个开源的库去解析HTML。

深入研究parse的过程所花费的收获和时间不成正比,所以这里只关注整体的执行流程。

parse 接收两个参数:

  • template 模板字符串
  • options 合并后的选项
// src\compiler\parser\index.js
/*!
 * HTML Parser By John Resig (ejohn.org)
 * Modified by Juriy "kangax" Zaytsev
 * Original code by Erik Arvidsson (MPL-1.1 OR Apache-2.0 OR GPL-2.0-or-later)
 * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
 */
// 这段注释介绍,parseHTML 借鉴了开源库 simplehtmlparser

// import ... 

// 定义了一些匹配模板字符串中内容的正则表达式

// 匹配标签中的属性,包括指令
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
// 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// 匹配文档声明
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
// 匹配注释
const comment = /^<!\--/
// 匹配条件注释
const conditionalComment = /^<!\[/

// ...

/**
 * Convert HTML string to AST.
 */
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // 1. 解析 options
  // ...

  // 定义了一些变量和函数
  // ...

  // 2. 调用 parseHTML 对模板解析
  // 接收两个参数:
  // template 模板字符串
  // 一个包含选项中成员的对象 和 4个方法
  //   这4个方法是解析过程中的回调函数
  parseHTML(template, {
    // 选项中的成员
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    // 解析过程中的回调函数

    // 解析到开始标签时调用
    start (tag, attrs, unary, start, end) {
	    // ...

      // createASTElement 创建 AST 对象
      let element: ASTElement = createASTElement(tag, attrs, currentParent)

      // 给 AST 的各种属性赋值
	    //...
      

      if (!inVPre) {
        // processPre 处理 v-pre 指令
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        // 处理结构化的指令
        // v-for
        processFor(element)
        // v-if
        processIf(element)
        // v-once
        processOnce(element)
      }
			// ...
    },

    // 解析到结束标签时调用
    end (tag, start, end) {
      // ...
    },

    // 解析到文本内容时调用
    chars (text: string, start: number, end: number) {
      // ...
    },

    // 解析到注释标签时调用
    comment (text: string, start, end) {
      // ...
    }
  })

  // 最后返回root,root内存储的就是解析好的AST对象
  return root
}

advance
// src\compiler\parser\html-parser.js
function advance (n) {
  // 记录当前的位置
  index += n
  // 截取剩余的内容
  html = html.substring(n)
}
handleStartTag
// src\compiler\parser\html-parser.js
// 解析标签和属性,并调用 start 方法
function handleStartTag (match) {
  //... 解析标签和属性

  if (options.start) {
    // 当对标签处理完毕之后
    // 最终调用了 options.start 方法
    // 并传递解析好的标签名、属性、是否是一元(unary)标签(自闭合标签)、起止位置
    // start 内部调用 createASTElement 创建 AST 对象
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}
createASTElement
// src\compiler\parser\index.js
export function createASTElement (
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
): ASTElement {
  // 返回一个 AST 对象
  return {
    type: 1,
    tag,
    // 标签的属性数组
    attrsList: attrs,
    // makeAttrsMap 把 attrs 转换成对象的形式
    // 方便后续使用
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []
  }
}

processPre
// src\compiler\parser\index.js
function processPre (el) {
  // getAndRemoveAttr 获取 v-pre 指令,然后从 AST 中移除对应的属性
  if (getAndRemoveAttr(el, 'v-pre') != null) {
    // 如果有v-pre,通过 pre 属性记录下来
    el.pre = true
  }
}
getAndRemoveAttr
// src\compiler\helpers.js
export function getAndRemoveAttr (
  el: ASTElement,
  name: string,
  removeFromMap?: boolean
): ?string {
  let val
  // 获取标签上的属性
  if ((val = el.attrsMap[name]) != null) {
    const list = el.attrsList
    for (let i = 0, l = list.length; i < l; i++) {
      if (list[i].name === name) {
        list.splice(i, 1)
        break
      }
    }
  }
  if (removeFromMap) {
    // 移除标签上的属性
    delete el.attrsMap[name]
  }
  // 返回属性的值
  return val
}
processIf
// src\compiler\parser\index.js
// 处理 v-if 指令
function processIf (el) {
  // 获取 v-if 指令的值(表达式),并移除 v-if 属性
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    // 如果有值(表达式),存储到 if 属性上
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    // 否则处理 v-else 和 v-else-if
    // 都是相似的处理过程,都是记录指令相关的数据
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}
addIfCondition
// src\compiler\parser\index.js
// 把 v-if 中的表达式和对应的 AST 对象,存储到 ifConditions 数组中
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}
总结

parse 函数处理的过程中,会依次遍历 HTML 模板字符串,把 HTML 模板字符串转换成 AST 对象(一个普通的对象)。

HTML 中的属性和指令都会记录在 AST 对象的相应属性上。

optimize 优化 AST

// src\compiler\optimizer.js
/**
 * Goal of the optimizer: walk the generated template AST tree
 * and detect sub-trees that are purely static, i.e. parts of
 * the DOM that never needs to change.
 * 优化器的目的:遍历编译模板生成的AST并检测纯静态的子树,即DOM中不需要更改的部分
 *
 * Once we detect these sub-trees, we can:
 * 一旦检测到这些子树,我们可以
 *
 * 1. Hoist them into constants, so that we no longer need to
 *    create fresh nodes for them on each re-render;
 * 1. 将它们提升为常量,这样我们就不再需要在每次重新渲染时为它们创建新的节点
 * 2. Completely skip them in the patching process.
 * 2. 在 patch 的过程中完全跳过它们
 */
// 优化的目的是为了标记 AST 中的静态节点
// 静态节点:对应的DOM子树永远不会发生变化(如纯文本的标签)
export function optimize (root: ?ASTElement, options: CompilerOptions) {
  // 判断是否传递了 AST 对象
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  // 标记 root 中的所有静态节点
  markStatic(root)
  // second pass: mark static roots.
  // 标记 root 中的静态根节点
  markStaticRoots(root, false)
}
markStatic

标记静态节点

function markStatic (node: ASTNode) {
  // 判断当前 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
    // 处理之前进行一些判断:
    // 判断元素标签是否是保留标签(判断当前是否是组件)
    // 如果是组件,不去把组件中的插槽内容标记成静态节点,避免:
    // 1. 组件无法改变插槽节点
    // 2. 静态插槽内容无法进行热重新加载
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    // 遍历 AST 对象的所有子节点
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      // 递归调用 markStatic 标记静态
      markStatic(child)
      if (!child.static) {
        // 如果有一个 child 不是 static,当前 node 就不是 staic
        node.static = false
      }
    }
    // 处理条件渲染中的 AST 对象,处理类似遍历 children
    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
        }
      }
    }
  }
}
isStatic

判断 astNode 是否是静态节点

function isStatic (node: ASTNode): boolean {
  // 判断 AST 节点的类型
  // 2:表达式(如插值表达式)
  // 它的内容会发生变化,所以不是静态节点,直接返回false
  if (node.type === 2) { // expression
    return false
  }
  // 3:静态的文本内容,返回true
  if (node.type === 3) { // text
    return true
  }
  // 最后判断下面条件都满足就表示是一个静态节点
  return !!(node.pre || ( // 如果是 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) && // 不是v-for中的直接子节点
    Object.keys(node).every(isStaticKey)
  ))
}
markStaticRoots

标记静态根节点。

静态根节点:

  • 标签中包含子标签,并且没有动态内容(都是纯文本内容)。
  • 如果标签中只包含纯文本内容,Vue中不会对它作优化(不会标记为静态根节点)。
    • 因为这样优化的成本大于收益
function markStaticRoots (node: ASTNode, isInFor: boolean) {
  // 判断 AST 描述的是否是 元素
  if (node.type === 1) {
    // 判断该节点是否是 静态的 或者 只渲染一次
    if (node.static || node.once) {
      // 标记该节点在 for 循环中是否是静态的
      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.
    // 如果一个元素只有文本子节点,那这个元素不是静态根节点
    // Vue 认为这种优化会带来负面的影响(优化成本大于收益)
    // 例如这个div就不算静态根节点:<div>纯文本</div>
    
    // 如果一个节点是静态的
    // 并且不是“只有一个文本节点”
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      // 设置为静态根节点
      node.staticRoot = true
      return
    } else {
      // 否则不是
      node.staticRoot = false
    }

    // 下面同markStatic类似
    // 遍历子节点和条件渲染中的AST对象,递归调用 markStaticRoots
    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)
      }
    }
  }
}

generate 把优化好的 AST 对象转化成 JS 代码

generate 接收两个参数:

  • ast - 优化好的 AST 对象
  • options - 合并好的选项

最终返回一个对象:

  • render - 使用 with 包裹 AST 对象转化成的 JS 代码
  • staticRenderFns - 存储静态根节点生成的字符串形式的代码
// src\compiler\codegen\index.js
export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  // 创建 CodegenState 对象:
  // 代码生成过程中使用到的状态对象
  const state = new CodegenState(options)
  // 如果ast存在,调用 genElement 开始生成代码
  // 否则 生成一个创建div的代码
  const code = ast ? genElement(ast, state) : '_c("div")'
  // 最后返回一个对象
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
CodegenState
export class CodegenState {
  options: CompilerOptions;
  warn: Function;
  transforms: Array<TransformFunction>;
  dataGenFns: Array<DataGenFunction>;
  directives: { [key: string]: DirectiveFunction };
  maybeComponent: (el: ASTElement) => boolean;
  onceId: number;
  staticRenderFns: Array<string>;
  pre: boolean;

  constructor (options: CompilerOptions) {
    // CodegenState 存储了一些和代码生成相关的属性和方法
    this.options = options
    this.warn = options.warn || baseWarn
    this.transforms = pluckModuleFunction(options.modules, 'transformCode')
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    this.directives = extend(extend({}, baseDirectives), options.directives)
    const isReservedTag = options.isReservedTag || no
    this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
    this.onceId = 0
    // 重点关注 staticRenderFns 和 pre
    // staticRenderFns 用来存储静态根节点生成的字符串形式的代码
    // 一个模板中可能有多个静态根节点,所以它是数组类型
    this.staticRenderFns = []
    // pre 记录当前处理的节点是否使用 v-pre 标记的
    this.pre = false
  }
}
render
genElement

generate 中最核心的就是 genElement,它是最终把 AST 转化成 代码的位置。

export function genElement (el: ASTElement, state: CodegenState): string {
  // 判断是否有父节点
  if (el.parent) {
    // 记录pre,根据自身的pre和父节点的pre取值
    // v-pre 标记的标签及子标签都是静态节点
    el.pre = el.pre || el.parent.pre
  }

  // 如果当前是静态根节点,且没有被处理过(staticProcessed=false)
  // genElement会被递归调用,这个判断用于防止重复处理这个节点
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)

  // 下面处理 once for if 指令,把它们转化成render函数中相应的代码
  } 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)

  // 如果是template标签并且不是slot和pre(也就是它不是静态的)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    // 生成它内部的子节点以及对应的代码
    // 如果没有子节点返回'void 0',也就是undefined
    return genChildren(el, state) || 'void 0'

  // 处理slot标签
  } 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))) {
        // 把AST对象中的相应属性,转换成 createElement 所需要的 data 对象的字符串形式
        data = genData(el, state)
      }

      // 处理子节点
      // 把el中的子节点,转化成 createElement中需要的数组形式
      // 也就是第三个参数 children
      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
  }
}
genData

genData 最终拼接了一个普通对象的字符串形式

根据 el 中的属性,拼接 createElement 中使用的相应的 data

最后返回 data。

genChildren

把子节点数组中的每一个AST对象,通过调用 genNode(当前使用) 生成对应的代码形式。

把生成的代码数组,通过逗号拼接成字符串,包裹成一个字符串形式的数组。

最后拼接 createElement 的第四个参数(如何去拍平数组)并返回。

export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  // 先判断 AST 对象是否有子节点
  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}`
    }

    // 这里是这个函数核心的处理过程

    // 首先获取如何去处理数组,也就是 createElement中的第四个参数(数组是否需要被拍平)
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    // map遍历数组中的每一个元素
    // 使用gen函数对每一个元素进行处理并返回
    // 然后用逗号把元素处理返回的数组拼接成一个字符串,包裹到一个数组中
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}
genNode
function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    // 当前 AST 节点是标签
    // 继续调用 genElement 处理当前的子节点
    return genElement(node, state)
  } else if (node.type === 3 && node.isComment) {
    // 如果是注释节点,生成注释节点对应的代码
    return genComment(node)
  } else {
    // 处理文本节点
    return genText(node)
  }
}
genComment
export function genComment (comment: ASTText): string {
  // _e:createEmptyVNode
  // JSON.stringify 用于给字符串加上引号 hello -> "hello"
  return `_e(${JSON.stringify(comment.text)})`
}
genText
export function genText (text: ASTText | ASTExpression): string {
  // _v:createTextVNode
  // type=2 表达式,直接返回该表达式(已经使用_s转化成了字符串)
  // transformSpecialNewlines 用于把代码中特殊的换行(unicode形式的)进行修正,防止意外情况
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}
staticRenderFns 生成静态根节点

staticRenderFns 存储的生成的静态根节点的渲染函数。

staticRenderFns 数组是在 genElement 中的 genStatic 方法中添加元素的。

staticRenderFns 定义为数组的原因:

  • 一个模板中可能会有多个静态节点(子节点都是静态节点的静态根节点)
  • staticRenderFns 先把每一个静态子树对应的代码进行存储
genStatic
// src\compiler\codegen\index.js
// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
  // 首先标记当前节点已经被处理过(防止重复处理)
  el.staticProcessed = true
  // Some elements (templates) need to behave differently inside of a v-pre
  // node.  All pre nodes are static roots, so we can use this as a location to
  // wrap a state change and reset it upon exiting the pre node.
  // 把 state.pre 暂存到一个变量中
  const originalPreState = state.pre
  // 获取 AST 对象的pre属性,并赋值给 state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  // 这里给 staticRenderFns 添加元素
  // 把静态根节点,转化成生成vnode的对应JS代码
  // staticProcessed 标记为 true
  // genElement 会直接进入到 component or element 处理过程中

  // staticRenderFns 定义为数组的原因:
  // 一个模板中可能会有多个静态节点(子节点都是静态节点的静态根节点)
  // 这里先把每一个静态子树对应的代码进行存储
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)

  // 当处理完当前节点后,再把原始状态中的 state.pre 还原
  state.pre = originalPreState

  // 最后返回当前节点对应的代码
  // _m:renderStatic 渲染静态内容
  // 传入的是当前节点在 staticRenderFns 中对应的索引
  // _m 会获取 staticRenderFns 中存储的对应的代码
  // 这里返回的是字符串类型的函数,但最终会被转化为函数调用
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}
_m函数:renderStatic

_m 函数在 installRenderHelpers 函数中被定义为 renderStatic 函数。

// src\core\instance\render-helpers\render-static.js
/**
 * Runtime helper for rendering static trees.
 */
export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  const cached = this._staticTrees || (this._staticTrees = [])
  // 从缓存中获取生成的静态根节点对应的代码
  let tree = cached[index]
  // if has already-rendered static tree and not inside v-for,
  // we can reuse the same tree.
  if (tree && !isInFor) {
    return tree
  }
  // otherwise, render a fresh tree.
  // 如果没有,就从 staticRenderFns 获取对应的函数并调用
  // 此时就生成了 vnode 节点,并把结果缓存
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )
  // 调用 markStatic 把当前返回的节点标记为静态的
  markStatic(tree, `__static__${index}`, false)
  return tree
}

markStatic
// src\core\instance\render-helpers\render-static.js
function markStatic (
  tree: VNode | Array<VNode>,
  key: string,
  isOnce: boolean
) {
  // 如果vnode是一个数组,递归调用markStaticNode
  if (Array.isArray(tree)) {
    for (let i = 0; i < tree.length; i++) {
      if (tree[i] && typeof tree[i] !== 'string') {
        markStaticNode(tree[i], `${key}_${i}`, isOnce)
      }
    }
  } else {
    // 否者直接调用 markStaticNode,把vnode设置为静态的
    markStaticNode(tree, key, isOnce)
  }
}

function markStaticNode (node, key, isOnce) {
  // 设置 isStatic 为true,表示是静态的
  // patch函数会判断 isStatic 为 true,不再对比差异,直接返回
  // 因为静态节点不会发生变化,不需要被处理,这是对静态节点的优化。
  // 如果静态节点已经被渲染到文档,那它不需要重新被渲染
  node.isStatic = true
  // 记录 key 和 isOnce
  node.key = key
  node.isOnce = isOnce
}

把字符串转化成函数的过程

baseCompile 方法只是返回了 AST 转化的 JS 字符串。

baseCompile 在 createCompilerCreator 中被调用。

createCompilerCreator 中定义的 createCompiler 最后返回了一个包含 compile 和 compileToFunctions(模板编译的入口函数) 的对象。

compileToFunctions 是 createCompileToFunctionFn 生成的,并接收了 compile 函数作为参数,并在内部定义。

compile 函数定义的内部调用了 baseCompile ,最终返回的是baseCompile 返回的结果(compiled)。

所以 createCompileToFunctionFn 内部使用了 baseCompile 返回的结果。

createCompileToFunctionFn

createCompileToFunctionFn 定义并返回的 compileToFunctions。

compileToFunctions 内部通过调用 createFunction 把 JS 字符串转化成函数。

接着遍历 staticRenderFns,把静态根节点中对应的每一个JS字符串转化为函数。

return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
  // compile
  // 2. 调用 compile 把模板编译成编译对象{render, staticRenderFns, errors, tips}
  // render 存储的是字符串形式的js代码
  // errors 和 tips 是辅助性属性,在编译模板过程中收集遇到的错误和信息,在这里把这些信息打印出来
  const compiled = compile(template, options)

  // ...

  // 3. 调用 createFunction 把字符串形式的js代码转换成函数
  res.render = createFunction(compiled.render, fnGenErrors)
  res.staticRenderFns = compiled.staticRenderFns.map(code => {
    return createFunction(code, fnGenErrors)
  })
  
  // ...

  // 4. 把编译的结果缓存并返回
  return (cache[key] = res)
}

至此模板编译的过程就讲完了。

模板编译过程-调试

通过查看源码了解:

  • 模板编译是把模板字符串首先转换成 AST 对象
  • 然后优化 AST 对象
    • 优化的过程就是标记静态根节点
  • 然后把优化好的 AST 对象转换成字符串形式的JS代码
  • 最终把字符串形式的JS代码,通过 new Function 转换成匿名函数
  • 这个匿名函数,就是最后生成的 render 函数。

模板编译最终就是把模板转换成 render 函数

<div id="app">
  <h1>Vue<span>模板编译过程</span></h1>
  <p>{{ msg }}</p>
  <div>该div不是静态根节点</div>
</div>
<script src="../../dist/vue.js"></script>
<script>
  Vue.component('comp', {
    template: '<div>I am a comp</div>'
  })
  const vm = new Vue({
    el: "#app",
    data: {
      msg: 'Hello compiler'
    }
  });
</script>

断点位置:

  • src\platforms\web\entry-runtime-with-compiler.js 入口文件调用 compileToFunctions 的位置。
    • 这是模板编译的入口函数
  • src\compiler\create-compiler.js 调用 baseCompile 的位置
    • 把模板字符串转换成 AST 、优化、生成 JS 代码的位置
  • src\compiler\to-function.js 调用 createFunction 的位置
    • 把JS代码转化成函数的位置

查看生成的AST对象,优化后查看结果:

  • h1 标签的 static 和 staticRoot 都为 true ,表示是静态节点,且是静态根节点
  • duv 标签的 static 为true,staticRoot为false,表示它是静态节点,但不是静态根节点

查看 generator 生成的 JS 代码(render,staticRenderFns)

  • 查看 h1 标签,它的 staticProcessed 属性为 true,被标记为处理完毕。

模板编译过程-总结

  1. compileToFunctions(template, options, vm) 是模板编译的入口函数
    1. 内部先从缓存中加载编译好的 render 函数
    2. 缓存中没有,调用 compile 开始编译
  2. compile(template, options)
    1. 首先合并选项
      1. 这是compile的核心
    2. 然后调用 baseCompile 编译模板
      1. 把模板和合并好的选项传递进去
  3. baseCompile(template.tirm(), finalOptions)
    1. 先完成模板编译核心的三件事情
      1. parse 把 template 字符串转换成 AST 对象
        1. 把 template 转换成 AST
      2. optimize 优化 AST,标记 AST 中的静态根节点
        1. 检测到静态根节点,设置为静态,不需要再每次重新渲染的时候重新生成节点(重绘)
        2. patch 阶段跳过静态根节点
      3. generator 把优化过后的 AST 转换成字符串形式 JS 代码
  4. compile 执行完毕,再次回到入口函数 compileToFunctions
    1. 继续把上一步中生成的字符串形式 JS 代码转换成函数
      1. 通过 createFunction,内部使用 new Function
    2. 当render 和 staticRenderFns 初始化完毕,挂载到 Vue 实例的 options 对应的属性中。

通过查看源码了解:

  • Vue 模板编译的过程中会标记静态根节点,对静态根节点进行优化处理。
    • 重新渲染的时候不需要处理静态根节点。因为它的内容不会改变。
  • 另外在模板中不要写过多的无意义的空白和换行
    • 否则生成的AST对象会保留这些空白和换行
    • 它们都会被存储到内存中。
    • 这些空白和换行对浏览器渲染来说是没有任何意义的。
    • 代码规范中也有响应的约定。

知识点小记

  • 模板编译的入口函数 compileToFunctions() 中的 parse 函数的作用是把模板解析成 AST 对象
  • AST 对象称为抽象语法树,通过 AST 抽象语法树来描述 DOM 的树形结构,目的是基于 AST 优化生成的代码
  • 模板编译的入口函数 compileToFunctions() 中的 optimize 函数的作用是标记 AST 中的静态根节点
  • 静态根节点是标签中除了文本内容以外,还需要包含其它标签
  • 静态根节点不会被重新渲染,patch 的过程中会跳过静态根节点
  • 模板和插值表达式在编译的过程中都会被转换成对应的代码形式,不会出现在 render 函数中
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值