前言
文中讲解代码为 Vue 组合式 API 的最后一个版本 2.6.14 ~ 为了更好的理解,省略了部分代码,留下核心逻辑进行讲解。如需完整代码解释,可在代码库拉取完整代码,每一行均有完整注释,并在不断地完善中,我也会不断补充所有 Vue 源码中涉及到的逻辑;文章中有需要纠正的地方,欢迎大家指出,我们共同打造一份详细易懂的源码解析 ~
github代码地址:欢迎star🌟🌟🌟
gitee代码地址:欢迎star🌟🌟🌟
如果大家想要了解代码中提到的函数,可以评论区留言,我会补充对该函数的讲解!
Vue
是一个渐进式 JavaScript
框架,提供了简单易用的模板语法,帮助开发者以声明式的方式构建用户界面。Vue
的模板编译原理是其核心之一,它将模板字符串编译成渲染函数,并在运行时高效地更新 DOM
。本文将深入探讨 Vue
模板编译的原理和过程编译的过程就是解析出 render
函数的过程,该过程分为四个阶段:
- 入口分析:寻找真正的编译入口
- 解析阶段:将模板字符串解析为抽象语法树(AST)
- 优化阶段:遍历AST,标记静态节点以便后续优化
- 生成阶段:将优化后的AST生成渲染函数(render function)
太沉重的篇幅不易于我们理解,这一章我们主要讲解模板编译的入口分析 ~
流程讲解
挂载实例
在整个 Vue
源码设计中,共有三处地方会执行挂载
自动调用挂载方法
首先 Vue
在实例化的时候,当传入 el
配置项,Vue
在初始化方法中会自动调用挂载实例方法
想了解 Vue 初始化流程的,可以看这篇文章了解:new Vue 的过程发生了什么
php
// 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
方法
php
// main.js
new Vue({
...
}).$mount('#app'); // 挂载实例
组件实例挂载
组件实例的创建的过程,当执行挂载逻辑时,依旧走的 $mount
方法
本章的重点是 Vue 实例化时候模板的编译,所以组件实例的挂载将放在其他文章中重点讲解
swift
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
),而不会进行模板编译
javascript
// src\platforms\web\runtime\index.js
Vue.prototype.$mount = function (el, hydrating) { // 挂载
...
return mountComponent(this, el, hydrating)
};
包含编译器的版本 (Runtime+Compiler)
在包含编译器的版本中,Vue
需要处理从模板到渲染函数的编译过程。这需要对 $mount
方法进行扩展
javascript
// 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
作为模板字符串
ini
// 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
方法调用结果的返回值中解构出来的
javascript
// 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
转换为渲染函数代码
scss
// 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
方法时候调用的那个创建编译器实例的方法
scss
// src\platforms\web\compiler\index.js
var { compile, compileToFunctions } = createCompiler(baseOptions);
php
// 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
函数调用的返回结果。因此,我们最后再去了解一下该函数
python
// src\compiler\create-compiler.js
return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
该函数主要在编译过程中,对错误发出提示信息以及对编译结果进行缓存
该函数的重点是调用了 compile
方法,然后 compile
调用了最核心的编译方法 baseCompile
php
// 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
在保持核心功能强大和灵活的同时,提供了良好的开发体验和代码质量