文章目录

  • 前言
  • 一、Vue的模板语法
  • 二、Vue的$mount
  • 三、compiler主要文件
  • 四、compile过程
  • 1、parse
  • 2、optimize
  • 3、generate
  • 总结



前言

在vue中,我们一直使用的是template模板,而不是真正的html,所以我们才能在template模板中使用各种指令v-if,{{}}表达式等。但最终template需要经过编译过程,转化为真正的html语言,才能渲染成我们的页面~


一、Vue的模板语法

Vue官方介绍:

1、Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js的模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。
2、 在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM操作次数减到最少。
3、如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量,你也可以不用模板,直接写渲染 (render) 函数,使用可选的 JSX语法。

二、Vue的$mount

在new Vue()时,Vue.prototype._init()方法,最终会调用vm.$mount(vm.$options.el)进行DOM挂载。Vue.prototype.$mount 在多个文件中都有定义。

vue3 编译转es5 vue如何编译template 模板_vue

  • Vue是一个跨平台的MVVM 框架,这里支持webweex,platforms目录 是Vue.js 的入口,2个目录代表了2个主要入口,分别运行在不同平台。
  • 【platforms/web/rutime/index.js】中定义了基础的Vue.prototype.$mount方法,但是不带模板编译功能。在【platforms/web/entry-runtime-with-compiler.js】中重新定义了Vue.prototype.$mount方法加入了模板编译功能(template生成render函数的过程)。

web/entry-runtime-with-compiler.js

import { compileToFunctions } from './compiler/index'
//....
const mount = Vue.prototype.$mount
// 重写了$mount方法,添加了模板编译功能
Vue.prototype.$mount = function (
  el?: string | Element,  // 挂载的元素,可以是字符串,也可以是 DOM 对象
  hydrating?: boolean
): Component {
  el = el && query(el) //查找元素
  //不能挂载到body,html元素上, 因为挂载点是会被组件模板自身替换点, 显然body/html不能被替换
  if (el === document.body || el === document.documentElement) { 
    return this
  }
  
  const options = this.$options
  // 检查options是否有render,无则需要编译生成render函数
  if (!options.render) { // 无render方法,把el或template进行编译,生成render方法。
    let template = options.template
    if (template) {  // 无render,有template,用template内容compile解析
      if (typeof template === 'string') { // template的类型是字符串
        if (template.charAt(0) === '#') { // template是ID
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        template = template.innerHTML   // template的类型是元素节点,则使用该元素的 innerHTML 作为模板
      } else { //template既不是字符串又不是元素节点
        return this
      }
    } else if (el) { // 无render,无template,那么使用el元素的outerHTML作为模板内容
      template = getOuterHTML(el)
    }
    if (template) {
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      // !!获取转换后的render函数与staticRenderFns,并挂在$options上
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render  // 编译生成的render函数挂载到options
      options.staticRenderFns = staticRenderFns

      // 非produceiton环境时统计编译器性能, config是全局配置对象
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  
  // 调用上面公共的mount方法(web/runtime/index.js),options若一开始有render方法,直接到这,不需要编译过程。若一开始无,需要经过编译,生成render,并挂载到options中。
  return mount.call(this, el, hydrating)
}
  • 【platforms/web/entry-runtime-with-compiler.js】中的$mount方法,就是加了一个render方法的判断。判断new Vue(options)中options是否传入了render。
  • 若传入了render,则直接调用【platforms/web/runtime/index.js】中定义的不带编译功能的$mount基础方法往下走。
  • 若未传入render,对传入的template参数进行一系列的判断,最终调用compileToFunctions方法,进行编译,生成render,再赋给options: options.render = render; options.staticRenderFns = staticRenderFns;
  • compileToFunctions传入的第一个参数就是模板字符串template,第二个参数是配置选项options。
  • compileToFunctions最后追溯到是在【src/compiler】目录中定义的,compiler目录里面都是编译相关内容。
// plaltforms/web/comipler/index.js
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)

三、compiler主要文件

vue3 编译转es5 vue如何编译template 模板_模板编译_02

四、compile过程

compileToFunctions追根溯源

import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
  • compileToFunctions(template,options,vm)在【src/platform/web/compiler/index.js】目录中定义,这个函数写的过程很绕,但最终是返回了ast、render、staticRenderFns。
  • createCompiler在【src/compiler/index.js】定义:
import { createCompilerCreator } from './create-compiler'

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1、模板解析
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
  // 2、优化
    optimize(ast, options) 
  }
  // 3、代码生成
  const code = generate(ast, options) 
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
  • baseCompile函数中才包含了主要的编译过程,分为3个主要步骤:parse、optimize、generate。

vue3 编译转es5 vue如何编译template 模板_Vue源码_03

1、parse

  • vue就是将模板代码映射为AST数据结构,进行语法解析。parse过程正是用正则等方式解析 template 模板中的标签、元素、文本、注释等数据,形成AST(抽象语法树)。
  • ASTNode三种类型(在【flow/compile.js】中有定义):declare type ASTNode = ASTElement | ASTText | ASTExpression

元素-ASTElement:

declare type ASTElement = {
  type: 1;
  tag: string;
  attrsList: Array<ASTAttr>;
  attrsMap: { [key: string]: any };
  rawAttrsMap: { [key: string]: ASTAttr };
  parent: ASTElement | void;
  children: Array<ASTNode>;
  //...
};

表达式-ASTExpression:{{}}

declare type ASTExpression = {
  type: 2;
  expression: string;
  text: string;
  tokens: Array<string | Object>;
  static?: boolean;
  ssrOptimizability?: number;
  start?: number;
  end?: number;
  has$Slot?: boolean
};

文本-ASTText:

declare type ASTText = {
  type: 3;
  text: string;
  static?: boolean;
  isComment?: boolean;
  ssrOptimizability?: number;
  start?: number;
  end?: number;
  has$Slot?: boolean
};

2、optimize

  • 遍历AST,找出静态节点并打标记;在进行patch的过程中,DOM-Diff算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。
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(root) 递归标记每个节点是否为静态节点: node.static = isStatic(node),也是为 markStaticRoots 服务的,先把每个节点都处理之后,更方便找静态根节点。
  • markStaticRoots(root, false) 标记区域静态根节点:node.staticRoot = true

3、generate

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  //核心部分,生成render表达式字符串主体
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`, //最外层用with(this)包裹
    staticRenderFns: state.staticRenderFns //被标记为 staticRoot 节点的 VNode 就会单独生成 staticRenderFns
  }
}
  • 入参为之前2个步骤得到的AST,出参为render表达式、staticRenderFns函数
  • genElement(ast, state) 为生成render表达式字符串的核心函数。
  • 如官网介绍,下面render的实例:
<div>
      <header>
         <h1>I'm a template!</h1>
       </header>
       <p v-if="message">{{ message }}</p>
       <p v-else>No message.</p>
</div>

render:

function anonymous(
) {
  with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])}
}

staticRenderFns:

_m(0): function anonymous(
) {
  with(this){return _c('header',[_c('h1',[_v("I'm a template!")])])}
}

render code的大致结构:

_c(
  // 1、标签
  'div',
  //2、数据对象 
  {
   attr:{...} 
   ...
  },
  //3、子节点数组,循环其模型
  [
    _c(...)
  ]
)
  • _c 是在initState-initRender中定义,其实是createElement方法。
  • with(this) 中this指向的是proxy data对象。若code中使用了变量message,会查找this.message 至此,得到了render函数字符串,compile过程结束~

总结

$options中无render函数的vue实例经过compileToFunctions(template,options,vm)编译完,得到了render,staticRenderFns函数,并挂载到$options中。继续下面的生成vnode流程。