正文开始之前,我们先根据源码来了解一下Vue的两个版本。 一种是仅运行时版本,另一种是运行时加编译器版本。 这两个版本之间的主要区别在于前者的源代码包含编译器。
编译器有哪些,百度百科里的解释是
简单来说,编译器就是将“一种语言(通常是中级语言)”翻译成“另一种语言(通常是低级语言)”的程序。 现代编译器的主要工作流程:源代码→预处理器→编译器→目标代码→链接器→可执行文件。
简单来说,编译器是一个提供源代码到目标代码转换的工具。 进一步理解,vue的外部编译器实现了将.vue文件转换编译为可执行Javascript脚本的功能。
3.1.1运行时+编译器
一个完整的vue版本包含了编译器,我们可以使用template进行模板编译。 编译器会手动将模板编译成渲染函数。
// 需要编译器的版本 new Vue({ template: '{{ hi }}' })
3.1.2仅运行时
对于不包含编译器的仅运行时版本,需要传递编译后的渲染函数,如下所示:
// 不需要编译器 new Vue({ render (h) { return h('div', this.hi) } })
显然,编译过程对性能有一定的损失,但是由于增加了编译过程的代码,导致vue代码量变得越来越大,所以我们可以使用webpack的vue-loader工具来进行编译,将编译阶段与创建vue. 下来,这不仅优化了性能,还减小了体积。
3.2 挂载的基本思想
vue挂载的过程比较复杂。 我们通过流程图来明确基本的实现思路。
如果用一句话概括挂载过程,可以描述为挂载一个组件,使用渲染函数生成虚拟DOM,更新视图时,将虚拟DOM渲染成真实DOM。
具体过程是:首先判断挂载的DOM元素,并确保该元素不能是html、body、nodes。 判断option中是否有render属性(如果运行时没有编译,则初始化option时需要传入render渲染函数)。 当有渲染属性时,默认情况下我们使用仅运行时版本,从而跳过模板编译阶段并调用真正的挂载函数$mount。 另一方面,当我们传递一个模板时(即如果不使用内置编译器,我们将使用runtime+compile版本),Vue源码会首先进入编译阶段。 这一阶段的核心是两个步骤。 一是把模板解析成具体的句子树,也就是我们经常看到的AST。 二是根据给定的AST生成目标平台所需的代码。 在浏览器端就是上面提到的render函数。 完成模板编译后,也会进入$mount挂载阶段。 真正的挂载过程是在mountComponent方法中执行的。 该函数的核心是实例化一个渲染观察器。 观察者的具体内容将在另一章中讨论。 我们只需要知道渲染观察器的功能即可。 一种是在初始化时执行回弹函数,另一种是当检测到vm实例中的数据发生变化时执行回弹函数。 这个反弹函数就是updateComponent。 该技术会通过vm._render生成虚拟DOM,最后通过vm._update将虚拟DOM转换为真实DOM。
接下来我们就从代码角度出发来理解挂载的实现思路。 下面我们只提取mount的骨架代码描述。
// 内部真正实现挂载的方法 Vue.prototype.$mount = function (el, hydrating) { el = el && inBrowser ? query(el) : undefined; // 调用mountComponent方法挂载 return mountComponent(this, el, hydrating) }; // 缓存了原型上的 $mount 方法 var mount = Vue.prototype.$mount; // 重新定义$mount,为包含编译器和不包含编译器的版本提供不同封装,最终调用的是缓存原型上的$mount方法 Vue.prototype.$mount = function (el, hydrating) { // 获取挂载元素 el = el && query(el); // 挂载元素不能为跟节点 if (el === document.body || el === document.documentElement) { warn( "Do not mount Vue to or - mount to normal elements instead." ); return this } var options = this.$options; // 需要编译 or 不需要编译 if (!options.render) { // 使用内部编译器编译模板 } // 最终调用缓存的$mount方法 return mount.call(this, el, hydrating) } // mountComponent方法思路 function mountComponent(vm, el, hydrating) { // 定义updateComponent方法,在watch回调时调用。 updateComponent = function () { // render函数渲染成虚拟DOM, 虚拟DOM渲染成真实的DOM vm._update(vm._render(), hydrating); }; // 实例化渲染watcher new Watcher(vm, updateComponent, noop, {}) }
3.3 编译过程——模板编译成render函数
通过文章前半部分的学习,我们对Vue的挂载流程有了一个大概的了解。 接下来我们就从模板编译的过程开始。 在阅读源码时,我发现模板编译过程相当复杂。 在短时间内解释整个编译过程是不切实际的。 因此,本节其余部分仅简单介绍一下实现思路。
3.3.1模板的三种编写方式
模板编译有三种形式,分别是:
// 1. 熟悉的字符串模板 var vm = new Vue({ el: '#app', template: '模板字符串' }) // 2. 选择符匹配元素的 innerHTML模板var vm = new Vue({ el: '#app', template: '#test' }) // 3. dom元素匹配元素的innerHTML模板test1test
var vm = new Vue({ el: '#app', template: document.querySelector('#test') })test1test2
三种写法分别对应代码的三个不同分支。
var template = options.template; if (template) { // 针对字符串模板和选择符匹配模板 if (typeof template === 'string') { // 选择符匹配模板,以'#'为前缀的选择器 if (template.charAt(0) === '#') { // 获取匹配元素的innerHTML template = idToTemplate(template); /* istanbul ignore if */ if (!template) { warn( ("Template element not found or is empty: " + (options.template)), this ); } } // 针对dom元素匹配 } else if (template.nodeType) { // 获取匹配元素的innerHTML template = template.innerHTML; } else { // 其他类型则判定为非法传入 { warn('invalid template option:' + template, this); } return this } } else if (el) { // 如果没有传入template模板,则默认以el元素所属的根节点作为基础模板 template = getOuterHTML(el); }
X-Template 模板表单通常用于演示或具有非常大模板的非常大的应用程序。 官方不建议在其他情况下使用它,因为这会将模板与组件的其他定义分开。
3.3.2 流程图
vue源码中的编译过程代码比较复杂,涉及到很多函数处理逻辑。 实现过程中巧妙地采用偏函数方法,提取配置项处理和编译的核心逻辑。 为了理解这个设计思想,我画了一张逻辑图vue源码解析 编译,有助于理解。
3.3.3 逻辑分析
虽然有流程图,但是编译逻辑还是比较冗长,不好理解。 我们就跟随它的脚步,根据代码来分析一下各个环节的执行流程。
var ref = compileToFunctions(template, { outputSourceRange: "development" !== 'production', shouldDecodeNewlines: shouldDecodeNewlineandroids, shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this); // 将compileToFunction方法暴露给Vue作为静态方法存在 Vue.compile = compileToFunctions;
这是编译的入口,也是Vue暴露的编译方法。 compileToFunctions 需要传递三个参数:模板 template、编译配置选项和 Vue 实例。我们先简单看一下配置中的一些默认选项。
1.delimiters 该选项可以更改纯文本插入分隔符。 不传值时,Vue默认分隔符为{{}},用户可以通过该选项更改
2.comments 设置为true时,模板中的HTML注释将被保留并呈现。 默认行为是丢弃它们。
然后一步步找到compileToFunctions的症结所在
var createCompiler = createCompilerCreator(function baseCompile (template,options) { //把模板解析成抽象的语法树 var ast = parse(template.trim(), options); // 配置中有代码优化选项则会对Ast语法树进行优化 if (options.optimize !== false) { optimize(ast, options); } var code = generate(ast, options); return { ast: ast, render: code.render, staticRenderFns: code.staticRenderFns } });
createCompilerCreator的角色定位为编译器的创建者。 他传递一个基本编译器 baseCompile 作为参数。 baseCompile 是 Android 实际执行编译功能的地方。 他将模板template和基本配置选项作为参数传递。实现了两个功能
1、将模板解析成具体的句子树,简称AST,以及代码中对应的parse部分
2.可选:优化AST句子树并执行optimize方法
3.根据不同平台,生成AST句子树必要的代码,以及对应的generate函数
看一下createCompilerCreator的实现方法。
function createCompilerCreator (baseCompile) { return function createCompiler (baseoptions) { // 内部定义compile方法 function compile (template, options) { // 将剔除空格后的模板以及合并选项后的配置作为参数传递给baseCompile方法,其中finalOptions为baseOptions和用户options的合并 var compiled = baseCompile(template.trim(), finalOptions); { detectErrors(compiled.ast, warn); } compiled.errors = errors; compiled.tips = tips; return compiled } return { compile: compile, compileToFunctions: createCompileToFunctionFn(compile) } } }
createCompilerCreator 函数只有一个功能。 它借助部分函数缓存了baseCompile的基本编译方法,并返回编译器函数。 该函数内部定义了真正执行编译的compile方法,最终返回compile和compileToFunctons作为两个对象属性。 这也是compileToFunctions的来源。 内部编译的作用是将基本配置baseOptions和用户自定义配置选项进行合并(baseOptions是与外部平台相关的配置),最后返回合并配置后的baseCompile编译方法。
compileToFunctions 从 createCompileToFunctionFn 函数的返回值开始,该函数会将编译方法compile作为参数传递。
function createCompileToFuncphptionFn (compile) {
var cache = Object.create(null);
return function compileToFunctions (template,options,vm) {
options = extend({}, options);
// 缓存的作用:避免重复编译同个模板造成性能的浪费
if (cache[key]) {
return cache[key]
}
// 执行编译方法
var compiled = compile(template, options);
// turn code into functions
var res = {};
var fnGenErrors = [];
// 编译出的函数体字符串作为参数传递给createFunction,返回最终的render函数
res.render = createFunction(compiled.render, fnGenErrors);
// 渲染优化相关
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});
return (cache[key] = res)
}
}
最后我们找到了compileToFunctions varcompiled=compile(template,options);的真实执行过程,并通过creatFunction将编译后的函数体字符串转换为render函数并返回。
function createFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err: err, code: code }); return nojavascriptop } }
函数体字符串类似“with(this){return_m(0)}”,最终渲染函数为function(){with(this){return_m(0)}}
至此,Vue中关于编译流程的思路已经梳理清楚了。 之所以编译逻辑混乱,主要是因为Vue在不同的平台上有不同的编译流程vue源码解析 编译,每个编译流程的baseOptions选项都会不同。 同时,在同一个平台下,不希望每次编译时都传入相同的baseOptions参数,因此在createCompilerCreator初始化编译器时传入参数,并借助部分函数来缓存配置。 同时剥离了编译相关的合并配置,这是Vue在编译方面非常巧妙的设计。
总结
以上就是小编介绍的Vue源码实例挂载编译过程。 希望对您有所帮助。 如果您有任何疑问,请给我留言,编辑会及时回复您。 在此也感谢您对我们网站的支持!