深入解析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 相关文章推荐
载入进度条 效果
Jul 08 Javascript
input 高级限制级用法
Mar 26 Javascript
javascript 文本框水印/占位符(watermark/placeholder)实现方法
Jan 15 Javascript
js对文章内容进行分页示例代码
Mar 05 Javascript
浅谈Javascript中Object与Function对象
Sep 26 Javascript
BootStrap 超链接变按钮的实现方法
Sep 25 Javascript
AngulerJS学习之按需动态加载文件
Feb 13 Javascript
AngularJS表单提交实例详解
Feb 18 Javascript
JS实现课堂随机点名和顺序点名
Mar 09 Javascript
如何在 Vue.js 中使用第三方js库
Apr 25 Javascript
AngularJS动态生成select下拉框的方法实例
Nov 17 Javascript
关于uniApp editor微信滑动问题
Jan 15 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
关于Appserv无法打开localhost问题的解决方法
2009/10/16 PHP
php更新mysql后获取影响的行数发生异常解决方法
2013/03/28 PHP
一个简单安全的PHP验证码类 附调用方法
2016/06/24 PHP
PHP入门教程之面向对象基本概念实例分析
2016/09/11 PHP
jQuery学习笔记 操作jQuery对象 文档处理
2012/09/19 Javascript
浏览器加载、渲染和解析过程黑箱简析
2012/11/29 Javascript
Javascript Boolean、Nnumber、String 强制类型转换的区别详细介绍
2012/12/13 Javascript
javascript 使用 NodeList需要注意的问题
2013/03/04 Javascript
iframe子父页面调用js函数示例
2013/11/07 Javascript
使用jquery.upload.js实现异步上传示例代码
2014/07/29 Javascript
BootStrap使用file-input插件上传图片的方法
2016/09/05 Javascript
通过学习bootstrop导航条学会修改bootstrop颜色基调
2017/06/11 Javascript
vue通过watch对input做字数限定的方法
2017/07/13 Javascript
js判断传入时间和当前时间大小实例(超简单)
2018/01/11 Javascript
详解Angular6学习笔记之主从组件
2018/09/05 Javascript
记一次webapck4 配置文件无效的解决历程
2018/09/19 Javascript
JS控制下拉列表左右选择实例代码
2020/05/08 Javascript
Python中除法使用的注意事项
2014/08/21 Python
python中pycurl库的用法实例
2014/09/30 Python
python删除特定文件的方法
2015/07/30 Python
Python如何读取MySQL数据库表数据
2017/03/11 Python
Python文件监听工具pyinotify与watchdog实例
2018/10/15 Python
python实现浪漫的烟花秀
2019/01/30 Python
使用pyqt 实现重复打开多个相同界面
2019/12/13 Python
python加密解密库cryptography使用openSSL生成的密匙加密解密
2020/02/11 Python
使用Python实现NBA球员数据查询小程序功能
2020/11/09 Python
HTML5验证以及日期显示的实现详解
2013/07/05 HTML / CSS
自动化专业毕业生自荐信
2013/11/01 职场文书
生产内勤岗位职责
2013/12/07 职场文书
执行力心得体会
2013/12/31 职场文书
团代会主持词
2014/04/02 职场文书
小学家长评语大全
2014/04/16 职场文书
门市房租房协议书
2014/12/04 职场文书
谁动了我的奶酪读书笔记
2015/06/30 职场文书
如何制定销售人员薪酬制度?
2019/07/09 职场文书
告别网页搜索!教你用python实现一款属于自己的翻译词典软件
2021/06/03 Python