深入解析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中常见陷阱小结
Apr 27 Javascript
简单的js表单验证函数
Oct 28 Javascript
Jquery创建层显示标题和内容且随鼠标移动而移动
Jan 26 Javascript
jQuery Trim去除字符串首尾空字符的实现方法说明
Feb 11 Javascript
jQuery中prevAll()方法用法实例
Jan 08 Javascript
ES6学习教程之Map的常用方法总结
Aug 03 Javascript
jQuery中过滤器的基本用法示例
Oct 11 jQuery
微信公众平台获取access_token的方法步骤
Mar 29 Javascript
单线程JavaScript实现异步过程详解
May 19 Javascript
利用js实现简易红绿灯
Oct 15 Javascript
手写Vue源码之数据劫持示例详解
Jan 04 Vue.js
Vue鼠标滚轮滚动切换路由效果的实现方法
Aug 04 Vue.js
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
PDO::prepare讲解
2019/01/29 PHP
身份证号码前六位所代表的省,市,区, 以及地区编码下载
2007/04/12 Javascript
jquery异步循环获取功能实现代码
2010/09/19 Javascript
javascript 隔行换色函数代码
2010/10/24 Javascript
JavaScript高级程序设计 读书笔记之十 本地对象Date日期
2012/02/27 Javascript
输入自动提示搜索提示功能的javascript:sugggestion.js
2013/09/02 Javascript
改变隐藏的input中value值的方法
2014/03/19 Javascript
javascript函数中参数传递问题示例探讨
2014/07/31 Javascript
jquery 根据name名获取元素的value值
2015/02/27 Javascript
arguments对象验证函数的参数是否合法
2015/06/26 Javascript
JS+CSS实现TreeMenu二级树形菜单完整实例
2015/09/18 Javascript
多种JQuery循环滚动文字图片效果代码
2020/06/23 Javascript
AngularJS基于ui-route实现深层路由的方法【路由嵌套】
2016/12/14 Javascript
jQuery插件FusionWidgets实现的Bulb图效果示例【附demo源码下载】
2017/03/23 jQuery
vue.js中npm安装教程图解
2018/04/10 Javascript
Vue实现搜索结果高亮显示关键字
2019/05/28 Javascript
深入解析Python中的WSGI接口
2015/05/11 Python
Python模糊查询本地文件夹去除文件后缀的实例(7行代码)
2017/11/09 Python
python 通过xml获取测试节点和属性的实例
2018/03/31 Python
python基础教程项目四之新闻聚合
2018/04/02 Python
详解Python3 中hasattr()、getattr()、setattr()、delattr()函数及示例代码数
2018/04/18 Python
HTML+CSS3模拟心的跳动实例代码
2017/09/05 HTML / CSS
HTML4和HTML5之间除了相似以外的10个主要不同
2012/12/13 HTML / CSS
HTML5之SVG 2D入门8—文档结构及相关元素总结
2013/01/30 HTML / CSS
html5 迷宫游戏(碰撞检测)实例一
2013/07/25 HTML / CSS
Lookfantastic阿联酋官网:英国知名美妆护肤购物网站
2020/05/26 全球购物
财务会计专业自荐书
2014/06/30 职场文书
企业法人代表证明书
2014/09/27 职场文书
社区党风廉政建设调研报告
2015/01/01 职场文书
单位计划生育责任书
2015/05/09 职场文书
学雷锋献爱心活动总结
2015/05/11 职场文书
无工作证明怎么写
2015/06/15 职场文书
初中运动会前导词
2015/07/20 职场文书
2016大学先进团支部事迹材料
2016/03/01 职场文书
2016年五一国际劳动节活动总结
2016/04/06 职场文书
六年级作文之关于梦
2019/10/22 职场文书