深入解析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 相关文章推荐
用 Javascript 验证表单(form)中多选框(checkbox)值
Sep 08 Javascript
JSON 编辑器实现代码
Dec 06 Javascript
js 绑定带参数的事件以及手动触发事件
Apr 27 Javascript
JS判断数组中是否有重复值得三种实用方法
Aug 16 Javascript
js实现简单鼠标跟随效果的方法
Apr 10 Javascript
解决AjaxFileupload 上传时会出现连接重置的问题
Jul 07 Javascript
Javascript别踩白块儿(钢琴块儿)小游戏实现代码
Jul 20 Javascript
jQuery实现checkbox即点即改批量删除及中间遇到的坑
Nov 11 jQuery
开发Vue树形组件的示例代码
Dec 21 Javascript
js实现前面自动补全位数的方法
Oct 10 Javascript
vue实现购物车案例
May 30 Javascript
JavaScript的function函数详细介绍
Nov 20 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
Windows下XDebug 手工配置与使用说明
2010/07/11 PHP
CodeIgniter扩展核心类实例详解
2016/01/20 PHP
Yii数据读取与跳转参数传递用法实例分析
2016/07/12 PHP
Javascript 继承机制实例
2009/08/12 Javascript
jquery插件实现鼠标经过图片右侧显示大图的效果(类似淘宝)
2013/02/04 Javascript
js综合应用实例简单的表格统计
2013/09/03 Javascript
javascript实现禁止复制网页内容
2014/12/16 Javascript
浅谈Web页面向后台提交数据的方式和选择
2016/09/23 Javascript
详解js界面跳转与值传递
2016/11/22 Javascript
浅谈angularjs $http提交数据探索
2017/01/20 Javascript
JavaScript中从setTimeout与setInterval到AJAX异步
2017/02/13 Javascript
Bootstrap Table使用整理(三)
2017/06/09 Javascript
基于js文件加载优化(详解)
2018/01/03 Javascript
VUE 实现滚动监听 导航栏置顶的方法
2018/09/11 Javascript
ng-repeat指令在迭代对象时的去重方法
2018/10/02 Javascript
JavaScript算法学习之冒泡排序和选择排序
2019/11/02 Javascript
js生成1到100的随机数最简单的实现方法
2020/02/07 Javascript
js中!和!!的区别与用法
2020/05/09 Javascript
[01:01:18]VP vs NIP 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/17 DOTA
python使用7z解压apk包的方法
2015/04/18 Python
解读Django框架中的低层次缓存API
2015/07/24 Python
通过实例浅析Python对比C语言的编程思想差异
2015/08/30 Python
5个很好的Python面试题问题答案及分析
2018/01/19 Python
Python实现基于PIL和tesseract的验证码识别功能示例
2018/07/11 Python
英国第一蛋白粉品牌:Myprotein
2016/09/14 全球购物
Agoda中文官网:安可达(低价预订全球酒店)
2021/01/18 全球购物
土木工程专业自荐信
2013/10/04 职场文书
销售会计工作职责
2013/12/02 职场文书
秋季运动会表扬稿
2014/01/16 职场文书
单位介绍信范文
2014/01/18 职场文书
董事长助理岗位职责
2014/02/18 职场文书
请假条怎么写
2014/04/10 职场文书
任命书范本大全
2014/06/06 职场文书
就业推荐表自我评价范文
2015/03/02 职场文书
安装pytorch时报sslerror错误的解决方案
2021/05/17 Python
Python中基础数据类型 set集合知识点总结
2021/08/02 Python