Vue
是一个渐进式 JavaScript
框架,提供了简单易用的模板语法,帮助开发者以声明式的方式构建用户界面。Vue
的模板编译原理是其核心之一,它将模板字符串编译成渲染函数,并在运行时高效地更新 DOM
。本文将深入探讨 Vue
模板编译的原理和过程编译的过程,也就是解析出 render
函数的过程,该过程分为四个阶段:
- 入口分析:寻找真正的编译入口
- 解析阶段:将模板字符串解析为抽象语法树(AST)
- 优化阶段:遍历AST,标记静态节点以便后续优化
- 生成阶段:将优化后的AST生成渲染函数(render function)
这篇文章我们只分享模板编译的入口分析
流程讲解
-
挂载实例
在整个
Vue
源码设计中,共有三处地方会执行挂载-
自动调用挂载方法
首先
Vue
在实例化的时候,当传入el
配置项,Vue
在初始化方法中会自动调用挂载实例方法// main.js new Vue({ el: '#app' }); // src\core\instance\init.js Vue.prototype._init = function (options) { ... if (vm.$options.el) { // 挂载实例 vm.$mount(vm.$options.el); } };
-
手动调用挂载方法
如果
Vue
实例在实例化时没有收到el
选项,则它处于“未挂载”状态或者我们也可以选择手动挂载,调用Vue
对外暴露的$mount
方法// main.js new Vue({ ... }).$mount('#app'); // 挂载实例
-
组件实例挂载
组件实例的创建的过程,当执行挂载逻辑时,依旧走的
$mount
方法,本章的重点是 Vue 实例化时候模板的编译。var componentVNodeHooks = { // 初始化钩子函数 (在组件的虚拟节点被创建时调用) init: function init (vnode, hydrating) { ... child.$mount(hydrating ? vnode.elm : undefined, hydrating); // 挂载组件实例 } }
总之不论是哪种方法,最终都会带着 el 属性走到 Vue 原型上的 $mount 方法
-
-
$mount 方法
我们紧接着看
$mount
方法,他的主要作用就是将传入的元素(el
)或模板(template
)编译为渲染函数(render
),下面我们详细展开$mount 的不同版本
-
**运行时版本 ( Runtime-Only )**在纯运行时版本中,Vue 依赖于预编译好的渲染函数(
render
),而不会进行模板编译// src\platforms\web\runtime\index.js Vue.prototype.$mount = function (el, hydrating) { // 挂载 ... return mountComponent(this, el, hydrating) };
-
**包含编译器的版本 (Runtime+Compiler)**在包含编译器的版本中,
Vue
需要处理从模板到渲染函数的编译过程。这需要对$mount
方法进行扩展// src\platforms\web\entry-runtime-with-compiler.js var mount = Vue.prototype.$mount; // 备份原始 $mount 方法 Vue.prototype.$mount = function (el, hydrating) { /* 模板编译 */ return mount.call(this, el, hydrating) };
为什么需要两次定义 $mount 方法 ?
- 模块化设计:
Vue
的设计是模块化的,基础的$mount
方法在运行时版本和包含编译器的版本中都存在,它们共享一个基础实现 - 扩展功能:运行时版本中的
$mount
方法假设已经有了渲染函数,因此直接进行挂载;包含编译器的版本需要在挂载之前进行模板编译,因此需要扩展基础的$mount
方法 - 分离关注点:基础的
$mount
方法专注于组件实例的挂载逻辑,扩展的$mount
方法处理模板编译的额外逻辑,从而在不同场景下提供合适的功能
获取需要编译的模板
在
Vue
的官方提供的生命周期图示,也描述了这一过程- 判断 render 选项
首先,判断如果选项中传入了
render
函数,则直接调用初始定义的$mount
方法去进行实例挂载 因为我们的初始目的就是为了将el
或template
转化为为render
函数,这就是为何直接传入函数的方式可以提高渲染效率,原因在于:- 避免了运行时的模板编译步骤
- 提供了更灵活和高效的渲染控制
- 减少了运行时的计算开销
- 判断 template 选项
然后,判断如果选项中传入了
template
选项,则获取对应的HTML
模板字符串 获取规则:- 如果该字符串以
#
开头,它将被用作querySelector
的选择器,并使用所选中元素的innerHTML
作为模板字符串 - 如果是
DOM
元素,直接使用元素的innerHTML
作为模板字符串
-
判断 el 选项
最后,判断如果选项中传入了
el
选项,则获取元素的innerHTML
作为模板字符串
// src\platforms\web\runtime\index.js Vue.prototype.$mount = function (el, hydrating) { // 挂载 ... return mountComponent(this, el, hydrating) }; ... // src\platforms\web\entry-runtime-with-compiler.js var mount = Vue.prototype.$mount; // 备份原始 $mount 方法 Vue.prototype.$mount = function (el, hydrating) { el = el && query(el); var options = this.$options; // 选项 // 1. 判断 render 选项 if (!options.render) { var template = options.template; // 2. 判断 template 选项 if (template) { // 如果是字符串, 通过 id获取元素并获取 innerHTML作为模板字符串 if (typeof template === 'string') { template = idToTemplate(template); // 如果是元素, 直接获取元素的 innerHTML作为模板字符串 } else if (template.nodeType) { template = template.innerHTML; } // 3. 判断 el 选项 } else if (el) { template = getOuterHTML(el); } // 模板编译 const { render, staticRenderFns } = compileToFunctions(template, { ... }, this); options.render = render; // 渲染函数 options.staticRenderFns = staticRenderFns; // 静态渲染函数数组 } return mount.call(this, el, hydrating) // 挂载 };
模板编译
当获取到需要编译的模板后,会调用
compileToFunctions
方法将模板编译成渲染函数和静态渲染函数 本文的重点是模板编译的入口分析,因此,接下来将继续分析compileToFunctions
函数compileToFunctions 方法
compileToFunctions
是Vue
中用于将模板字符串编译为渲染函数的关键方法。这个方法的实现涉及多个步骤,包括解析模板、优化生成的抽象语法树 (AST
),以及生成最终的渲染函数。下面我们逐步解析compileToFunctions
的实现过程compileToFunctions 方法
首先调用
createCompiler
方法,传入基础编译选项baseOptions
,创建一个编译器实例,然后compileToFunctions
方法是从createCompiler
方法调用结果的返回值中解构出来的// src\platforms\web\compiler\options.js // 编译器的基础选项 var baseOptions = { expectHTML: true, // 表示预期输入的模板是否为 HTML modules: modules$1, // 用于处理特定的功能或特性的模块数组 (class/style/v-model) directives: directives$1, // 对特殊指令的处理函数 (v-model/v-text/v-html) isPreTag: isPreTag, // 用于判断是否为 <pre> 标签 isUnaryTag: isUnaryTag, // 用于判断是否为自闭合标签 mustUseProp: mustUseProp, // 用于判断在给定的标签上绑定属性是否必须使用 prop 进行绑定 canBeLeftOpenTag: canBeLeftOpenTag, // 用于判断给定的标签是否可以不闭合 isReservedTag: isReservedTag, // 用于判断是否是平台保留标签 getTagNamespace: getTagNamespace, // 用于获取标签的命名空间 staticKeys: genStaticKeys(modules$1) // 用于生成静态键的列表 (优化渲染性能) }; ... // src\platforms\web\compiler\index.js var { compile, compileToFunctions } = createCompiler(baseOptions);
createCompiler 方法
紧接着看
createCompiler
方法的定义 我们发现createCompiler
方法又是从createCompilerCreator
方法调用结果的返回值中解构出来的,并且传入了baseCompile
函数作为参数,而该函数便是模板编译中最核心最重要的编译方法,它通过下列三个步骤完成了模板从字符串到渲染函数的转换过程- parse(解析):将模板字符串解析为
AST
- optimize(优化):标记
AST
中的静态节点,减少运行时需要处理的动态节点 - generate(生成):将优化后的
AST
转换为渲染函数代码
// src\compiler\index.js var createCompiler = createCompilerCreator(function baseCompile (template, options) { // 将模板字符串解析为 AST var ast = parse(template.trim(), options); // 对 AST 进行优化 optimize(ast, options); // 将 AST 转换为渲染函数代码 var code = generate(ast, options); return { ast: ast, render: code.render, staticRenderFns: code.staticRenderFns } });
createCompilerCreator 方法
然后继续看
createCompilerCreator
方法做了哪些事情 通过观察代码,我们知道了createCompilerCreator
方法是创建编译器的工厂函数 调用createCompilerCreator
方法并传入最核心的编译方法baseCompile
后返回createCompiler
函数,而该函数便是在获取compile
以及compileToFunctions
方法时候调用的那个创建编译器实例的方法// src\platforms\web\compiler\index.js var { compile, compileToFunctions } = createCompiler(baseOptions);
// src\compiler\create-compiler.js function createCompilerCreator (baseCompile) { return function createCompiler (baseOptions) { // 创建编译器实例 function compile (template, options) { // 编译模板 var compiled = baseCompile(template.trim(), finalOptions); return compiled } return { // 将模板字符串编译为渲染函数代码 compile: compile, // 将模板字符串编译为渲染函数 compileToFunctions: createCompileToFunctionFn(compile) } } }
createCompileToFunctionFn 方法
在
createCompiler
函数的调用结果中返回compileToFunctions
函数的时候,我们发现compileToFunctions
函数是createCompileToFunctionFn
函数调用的返回结果。因此,我们最后再去了解一下该函数// src\compiler\create-compiler.js return { compile: compile, compileToFunctions: createCompileToFunctionFn(compile) }
该函数主要在编译过程中,对错误发出提示信息以及对编译结果进行缓存 该函数的重点是调用了
compile
方法,然后compile
调用了最核心的编译方法baseCompile
// src\compiler\to-function.js function createCompileToFunctionFn (compile) { return function compileToFunctions (template, options, vm) { var compiled = compile(template, options); // 编译模板 ... return (cache[key] = res) // 返回并缓存编译结果 } }
-
总结
Vue
编译入口的逻辑之所以这么复杂,采用高阶函数和工厂函数的模式。是因为 Vue
需要在不同的平台下编译,接受不同的配置和选项,并生成适应不同需求的编译器实例,实现高度的灵活性、模块化、可扩展性和性能优化。同时通过缓存机制提高了运行时性能。通过这种方式,Vue
在保持核心功能强大和灵活的同时,提供了良好的开发体验和代码质量