vue源码解析 编译-深入解析Vue源码实例挂载编译流程实现思路解读

正文开始之前,我们先根据源码来了解一下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模板
test1

test

var vm = new Vue({ el: '#app', template: '#test' }) // 3. dom元素匹配元素的innerHTML模板
test1
test2
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 模板表单通常用于演示或具有非常大模板的非常大的应用程序。 官方不建议在其他情况下使用它,因为这会将模板与组件的其他定义分开。

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源码实例挂载编译过程。 希望对您有所帮助。 如果您有任何疑问,请给我留言,编辑会及时回复您。 在此也感谢您对我们网站的支持!

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 源码编译 vue源码解析 编译-深入解析Vue源码实例挂载编译流程实现思路解读 https://www.wkzy.net/game/194150.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务