深入解析Vue源码实例挂载与编译流程实现思路详解


Posted in Javascript onMay 05, 2019

在正文开始之前,先了解vue基于源码构建的两个版本,一个是 runtime only ,另一个是 runtime加compiler 的版本,两个版本的主要区别在于后者的源码包括了一个编译器。

什么是编译器,百度百科上面的解释是

简单讲,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)。

通俗点讲,编译器是一个提供了将源代码转化为目标代码的工具。更进一步理解,vue内置的编译器实现了将 .vue 文件转换编译为可执行javascript脚本的功能。

3.1.1 Runtime + Compiler

一个完整的vue版本是包含编译器的,我们可以使用 template 进行模板编写。编译器会自动将模板编译成 render 函数。

// 需要编译器的版本
new Vue({
 template: '<div>{{ hi }}</div>'
})

3.1.2 Runtime Only

而对于一个不包含编译器的 runtime-only 版本,需要传递一个编译好的 render 函数,如下所示:

// 不需要编译器
new Vue({
 render (h) {
 return h('div', this.hi)
 }
})

很明显,编译过程对性能有一定的损耗,并且由于加入了编译过程的代码,vue代码体积也更加庞大,所以我们可以借助webpack的vue-loader工具进行编译,将编译阶段从vue的构建中剥离出来,这样既优化了性能,也缩小了体积。

3.2 挂载的基本思路

vue挂载的流程是比较复杂的,我们通过流程图理清基本的实现思路。

深入解析Vue源码实例挂载与编译流程实现思路详解

如果用一句话概括挂载的过程,可以描述为挂载组件,将渲染函数生成虚拟DOM,更新视图时,将虚拟DOM渲染成为真正的DOM。

详细的过程是:首先确定挂载的DOM元素,且必须保证该元素不能为 html,body 这类跟节点。判断选项中是否有 render 这个属性(如果不在运行时编译,则在选项初始化时需要传递 render 渲染函数)。当有 render 这个属性时,默认我们使用的是 runtime-only 的版本,从而跳过模板编译阶段,调用真正的挂载函数 $mount 。另一方面,当我们传递是 template 模板时(即在不使用外置编译器的情况下,我们将使用 runtime+compile 的版本),Vue源码将首先进入编译阶段。该阶段的核心是两步,一个是把模板解析成抽象的语法树,也就是我们常听到的 AST ,第二个是根据给定的AST生成目标平台所需的代码,在浏览器端是前面提到的 render 函数。完成模板编译后,同样会进入 $mount 挂载阶段。真正的挂载过程,执行的是 mountComponent 方法,该函数的核心是实例化一个渲染 watcher ,具体 watcher 的内容,另外放章节讨论。我们只要知道渲染 watcher 的作用,一个是初始化的时候会执行回调函数,另一个是当 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 <html> or <body> - 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 template的三种写法

template模板的编写有三种方式,分别是:

// 1. 熟悉的字符串模板
var vm = new Vue({
 el: '#app',
 template: '<div>模板字符串</div>'
})
// 2. 选择符匹配元素的 innerHTML模板
<div id="app">
 <div>test1</div>
 <script type="x-template" id="test">
 <p>test</p>
 </script>
</div>
var vm = new Vue({
 el: '#app',
 template: '#test'
})
// 3. dom元素匹配元素的innerHTML模板
<div id="app">
 <div>test1</div>
 <span id="test"><div class="test2">test2</div></span>
</div>
var vm = new Vue({
 el: '#app',
 template: document.querySelector('#test')
})

三种写法对应代码的三个不同分支。

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模板的方式一般用于模板特别大的 demo 或极小型的应用,官方不建议在其他情形下使用,因为这会将模板和组件的其它定义分离开。

3.3.2 流程图解

vue源码中编译流程代码比较绕,涉及的函数处理逻辑比较多,实现流程中巧妙的运用了偏函数的技巧将配置项处理和编译核心逻辑抽取出来,为了理解这个设计思路,我画了一个逻辑图帮助理解。

深入解析Vue源码实例挂载与编译流程实现思路详解

3.3.3 逻辑解析

即便有流程图,编译逻辑理解起来依然比较晦涩,接下来,结合代码分析每个环节的执行过程。

var ref = compileToFunctions(template, {
 outputSourceRange: "development" !== 'production',
 shouldDecodeNewlines: shouldDecodeNewlines,
 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 是真正执行编译功能的地方,他传递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 的来源。而内部 compile 的作用,是为了将基础的配置 baseOptions 和用户自定义的配置 options 进行合并,( baseOptions 是跟外部平台相关的配置),最终返回合并配置后的 baseCompile 编译方法。

compileToFunctions 来源于 createCompileToFunctionFn 函数的返回值,该函数会将编译的方法 compile 作为参数传入。

function createCompileToFunctionFn (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 真正的执行过程 var compiled = compile(template, options); ,并将编译后的函数体字符串通过 creatFunction 转化为 render 函数返回。

function createFunction (code, errors) {
 try {
 return new Function(code)
 } catch (err) {
 errors.push({ err: err, code: code });
 return noop
 }
}

其中函数体字符串类似于 "with(this){return _m(0)}" ,最终的render渲染函数为 function(){with(this){return _m(0)}}

至此,Vue中关于编译过程的思路也梳理清楚了,编译逻辑之所以绕,主要是因为Vue在不同平台有不同的编译过程,而每个编译过程的 baseOptions 选项会有所不同,同时在同一个平台下又不希望每次编译时传入相同的 baseOptions 参数,因此在 createCompilerCreator 初始化编译器时便传入参数,并利用偏函数将配置进行缓存。同时剥离出编译相关的合并配置,这些都是Vue在编译这块非常巧妙的设计。

总结

以上所述是小编给大家介绍的Vue源码实例挂载与编译流程,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
[IE&amp;FireFox兼容]JS对select操作
Jan 07 Javascript
JavaScript 事件记录使用说明
Oct 20 Javascript
如何获取JQUERY AJAX返回的JSON结果集实现代码
Dec 10 Javascript
获得Javascript对象属性个数的示例代码
Nov 21 Javascript
JS控制输入框内字符串长度
May 21 Javascript
jquery checkbox 勾选的bug问题解决方案与分析
Nov 13 Javascript
jquery实现的用户注册表单提示操作效果代码分享
Aug 28 Javascript
JavaScript输入框字数实时统计更新
Jun 17 Javascript
ExtJs整合Echarts的示例代码
Feb 27 Javascript
基于AngularJs select绑定数字类型的问题
Oct 08 Javascript
javascript自定义加载loading效果
Sep 15 Javascript
基于ajax实现上传图片代码示例解析
Dec 03 Javascript
Vue 中如何正确引入第三方模块的方法步骤
May 05 #Javascript
详解vue的双向绑定原理及实现
May 05 #Javascript
Vue+Express实现登录注销功能的实例代码
May 05 #Javascript
Vue 递归多级菜单的实例代码
May 05 #Javascript
微信小程序自定义组件传值 页面和组件相互传数据操作示例
May 05 #Javascript
详解Vue调用手机相机和相册以及上传
May 05 #Javascript
原生JS实现动态添加新元素、删除元素方法
May 05 #Javascript
You might like
PHP 模拟$_PUT实现代码
2010/03/15 PHP
php快速url重写更新版[需php 5.30以上]
2010/04/25 PHP
浅析php插件 Simple HTML DOM 用DOM方式处理HTML
2013/07/01 PHP
PHP响应post请求上传文件的方法
2015/12/17 PHP
phalcon model在插入或更新时会自动验证非空字段的解决办法
2016/12/29 PHP
php实现基于pdo的事务处理方法示例
2017/07/21 PHP
PHP实现的基于单向链表解决约瑟夫环问题示例
2017/09/30 PHP
PHP基于PDO扩展操作mysql数据库示例
2018/12/24 PHP
TP5框架实现的数据库备份功能示例
2020/04/05 PHP
让JavaScript 轻松支持函数重载 (Part 1 - 设计)
2009/08/04 Javascript
5个javascript的数字格式化函数分享
2011/12/07 Javascript
javascript返回顶部效果(自写代码)
2013/01/06 Javascript
jquery和雅虎的yql服务实现天气预报服务示例
2014/02/08 Javascript
深入分析js的冒泡事件
2014/12/05 Javascript
jQuery中first()方法用法实例
2015/01/06 Javascript
浅谈几种常用的JS类定义方法
2016/06/08 Javascript
JS 调试中常见的报错问题解决方法
2017/05/20 Javascript
简要讲解Python编程中线程的创建与锁的使用
2016/02/28 Python
python入门基础之用户输入与模块初认识
2016/11/14 Python
PyQt5图形界面播放音乐的实例
2019/06/17 Python
Python利用matplotlib做图中图及次坐标轴的实例
2019/07/08 Python
Python图像处理之图片文字识别功能(OCR)
2019/07/30 Python
Python Django实现layui风格+django分页功能的例子
2019/08/29 Python
Python猜数字算法题详解
2020/03/01 Python
Python如何实现自带HTTP文件传输服务
2020/07/08 Python
PyCharm最新激活码(2020/10/27全网最新)
2020/10/27 Python
css3 flex布局 justify-content:space-between 最后一行左对齐
2020/01/02 HTML / CSS
HTML5中外部浏览器唤起微信分享功能的代码
2020/09/15 HTML / CSS
优衣库澳大利亚官网:UNIQLO澳大利亚
2017/01/18 全球购物
三星英国官网:Samsung英国
2018/09/25 全球购物
Vrbo西班牙:预订您的度假公寓(公寓、乡村房屋…)
2020/04/27 全球购物
土木工程专业大学毕业生求职信
2013/10/13 职场文书
学校安全管理责任书
2014/07/23 职场文书
干部竞争上岗演讲稿
2014/09/11 职场文书
2015年关爱留守儿童工作总结
2015/05/22 职场文书
酒店工程部的岗位职责汇总大全
2019/10/23 职场文书