【Vue2.0源码学习】生命周期篇-模板编译阶段(template)

文章目录

    • 1. 前言
    • 2. 模板编译阶段分析
      • 2.1 两种$mount方法对比
      • 2.2 完整版的vm.$mount方法分析
    • 3. 总结

1. 前言

前几篇文章中我们介绍了生命周期的初始化阶段,我们知道,在初始化阶段各项工作做完之后调用了vm.$mount方法,该方法的调用标志着初始化阶段的结束和进入下一个阶段,从官方文档给出的生命周期流程图中可以看到,下一个阶段就进入了模板编译阶段,该阶段所做的主要工作是获取到用户传入的模板内容并将其编译成渲染函数。

在这里插入图片描述

模板编译阶段并不是存在于Vue的所有构建版本中,它只存在于完整版(即vue.js)中。在只包含运行时版本(即vue.runtime.js)中并不存在该阶段,这是因为当使用vue-loadervueify时,*.vue文件内部的模板会在构建时预编译成渲染函数,所以是不需要编译的,从而不存在模板编译阶段,由上一步的初始化阶段直接进入下一阶段的挂载阶段。

在这里,我们有必要介绍一下什么是完整版和只包含运行时版。

vue基于源码构建的有两个版本,一个是runtime only(一个只包含运行时的版本),另一个是runtime + compiler(一个同时包含编译器和运行时的完整版本)。而两个版本的区别仅在于后者包含了一个编译器。

  • 完整版本

    一个完整的Vue版本是包含编译器的,我们可以使用template选项进行模板编写。编译器会自动将template选项中的模板字符串编译成渲染函数的代码,源码中就是render函数。如果你需要在客户端编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就需要一个包含编译器的版本。 如下:

    // 需要编译器的版本
    new Vue({template: '<div>{{ hi }}</div>'
    })
    
  • 只包含运行时版本

    只包含运行时的版本拥有创建Vue实例、渲染并处理Virtual DOM等功能,基本上就是除去编译器外的完整代码。该版本的适用场景有两种:

    1.我们在选项中通过手写render函数去定义渲染过程,这个时候并不需要包含编译器的版本便可完整执行。

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

    2.借助vue-loader这样的编译工具进行编译,当我们利用webpack进行Vue的工程化开发时,常常会利用vue-loader*.vue文件进行编译,尽管我们也是利用template模板标签去书写代码,但是此时的Vue已经不需要利用编译器去负责模板的编译工作了,这个过程交给了插件去实现。

很明显,编译过程对性能会造成一定的损耗,并且由于加入了编译的流程代码,Vue代码的总体积也更加庞大(运行时版本相比完整版体积要小大约 30%)。因此在实际开发中,我们需要借助像webpackvue-loader这类工具进行编译,将Vue对模板的编译阶段合并到webpack的构建流程中,这样不仅减少了生产环境代码的体积,也大大提高了运行时的性能,一举两得。

为了完整的学习源码,本篇文章将会分析完整版中的模板编译阶段到底做了些什么。

2. 模板编译阶段分析

上文中说了,完整版和只包含运行时版之间的差异主要在于是否有模板编译阶段,而是否有模板编译阶段主要表现在vm.$mount方法的实现上。此时你可能会有疑问:照这么说,$mount方法也有两个版本?对的,你可以这么理解,但归根结底来说还是一种。我们分别来看一下。

2.1 两种$mount方法对比

只包含运行时版本的$mount代码如下:

Vue.prototype.$mount = function (el,hydrating) {el = el && inBrowser ? query(el) : undefined;return mountComponent(this, el, hydrating)
};

在该版本中的$mount方法内部获取到el选项对应的DOM元素后直接调用mountComponent函数进行挂载操作,关于该函数我们会在挂载阶段详细介绍。

而完整版本的$mount代码如下:

var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el,hydrating) {// 省略获取模板及编译代码return mount.call(this, el, hydrating)
}

注意,在完整版本的$mount定义之前,先将Vue原型上的$mount方法先缓存起来,记作变量mount。此时你可能会问了,这$mount方法还没定义呢,怎么先缓存起来了。

其实在源码中,是先定义只包含运行时版本的$mount方法,再定义完整版本的$mount方法,所以此时缓存的mount变量就是只包含运行时版本的$mount方法。

为什么要这么做呢?上文我们说了,完整版本和只包含运行时版本之间的差异主要在于是否有模板编译阶段,只包含运行时版本没有模板编译阶段,初始化阶段完成后直接进入挂载阶段,而完整版本是初始化阶段完成后进入模板编译阶段,然后再进入挂载阶段。也就是说,这两个版本最终都会进入挂载阶段。所以在完整版本的$mount方法中将模板编译完成后需要回头去调只包含运行时版本的$mount方法以进入挂载阶段。

这也就是在完整版本的$mount方法中先把只包含运行时版本的$mount方法缓存下来,记作变量mount,然后等模板编译完成,再执行mount方法(即只包含运行时版本的$mount方法)。

所以分析模板编译阶段其实就是分析完整版的vm.$mount方法的实现。

2.2 完整版的vm.$mount方法分析

完整版的vm.$mount方法定义位于源码的src/platforms/web/entry-runtime-with-compiler.js中,如下:

var mount = Vue.prototype.$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;// resolve template/el and convert to render functionif (!options.render) {var template = options.template;if (template) {if (typeof template === 'string') {if (template.charAt(0) === '#') {template = idToTemplate(template);/* istanbul ignore if */if (!template) {warn(("Template element not found or is empty: " + (options.template)),this);}}} else if (template.nodeType) {template = template.innerHTML;} else {{warn('invalid template option:' + template, this);}return this}} else if (el) {template = getOuterHTML(el);}if (template) {if (config.performance && mark) {mark('compile');}var ref = compileToFunctions(template, {outputSourceRange: "development" !== 'production',shouldDecodeNewlines: shouldDecodeNewlines,shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,delimiters: options.delimiters,comments: options.comments}, this);var render = ref.render;var staticRenderFns = ref.staticRenderFns;options.render = render;options.staticRenderFns = staticRenderFns;if (config.performance && mark) {mark('compile end');measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');}}}return mount.call(this, el, hydrating)
};

从代码中可以看到,该函数可大致分为三部分:

  • 根据传入的el参数获取DOM元素;
  • 在用户没有手写render函数的情况下获取传入的模板template
  • 将获取到的template编译成render函数;

接下来我们就逐一分析。

首先,根据传入的el参数获取DOM元素。如下:

el = el && query(el);function query (el) {if (typeof el === 'string') {var selected = document.querySelector(el);if (!selected) {warn('Cannot find element: ' + el);return document.createElement('div')}return selected} else {return el}
}

由于el参数可以是元素,也可以是字符串类型的元素选择器,所以调用query函数来获取到el对应的DOM元素。由于query函数比较简单,就是根据传入的el参数是否为字符串从而以不同方式获取到对应的DOM元素,这里就不逐行展开介绍了。

另外,这里还多了一个判断,就是判断获取到el对应的DOM元素如果是bodyhtml元素时,将会抛出警告。这是因为Vue会将模板中的内容替换el对应的DOM元素,如果是bodyhtml元素时,替换之后将会破坏整个DOM文档,所以不允许elbodyhtml。如下:

if (el === document.body || el === document.documentElement) {warn("Do not mount Vue to <html> or <body> - mount to normal elements instead.");return this
}

接着,在用户没有手写render函数的情况下获取传入的模板template;如下:

if (!options.render) {var template = options.template;if (template) {if (typeof template === 'string') {if (template.charAt(0) === '#') {template = idToTemplate(template);/* istanbul ignore if */if (!template) {warn(("Template element not found or is empty: " + (options.template)),this);}}} else if (template.nodeType) {template = template.innerHTML;} else {{warn('invalid template option:' + template, this);}return this}} else if (el) {template = getOuterHTML(el);}
}

首先获取用户传入的template选项赋给变量template,如果变量template存在,则接着判断如果template是字符串并且以##开头,则认为templateid选择符,则调用idToTemplate函数获取到选择符对应的DOM元素的innerHTML作为模板,如下:

if (template) {if (typeof template === 'string') {if (template.charAt(0) === '#') {template = idToTemplate(template);}}
}var idToTemplate = cached(function (id) {var el = query(id);return el && el.innerHTML
});

如果template不是字符串,那就判断它是不是一个DOM元素,如果是,则使用该DOM元素的innerHTML作为模板,如下:

if (template.nodeType) {template = template.innerHTML;
}

如果既不是字符串,也不是DOM元素,此时会抛出警告:提示用户template选项无效。如下:

else {{warn('invalid template option:' + template, this);}return this
}

如果变量template不存在,表明用户没有传入template选项,则根据传入的el参数调用getOuterHTML函数获取外部模板,如下:

if (el) {template = getOuterHTML(el);
}function getOuterHTML (el) {if (el.outerHTML) {return el.outerHTML} else {var container = document.createElement('div');container.appendChild(el.cloneNode(true));return container.innerHTML}
}

不管是从内部的template选项中获取模板,还是从外部获取模板,总之就是要获取到用户传入的模板内容,有了模板内容接下来才能将模板编译成渲染函数。

获取到模板之后,接下来要做的事就是将其编译成渲染函数,如下:

if (template) {var ref = compileToFunctions(template, {outputSourceRange: "development" !== 'production',shouldDecodeNewlines: shouldDecodeNewlines,shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,delimiters: options.delimiters,comments: options.comments}, this);var render = ref.render;var staticRenderFns = ref.staticRenderFns;options.render = render;options.staticRenderFns = staticRenderFns;
}

关于将模板编译成渲染函数的具体步骤在前面文章模板编译篇中已经做了详细介绍,在这里,我们仅做简单回顾。

把模板编译成渲染函数是在compileToFunctions函数中进行的,该函数接收待编译的模板字符串和编译选项作为参数,返回一个对象,对象里面的render属性即是编译好的渲染函数,最后将渲染函数设置到$options上。

3. 总结

本篇文章介绍了生命周期中的第二个阶段——模板编译阶段。

首先介绍了Vue源码构建的两种版本:完整版本和只包含运行时版本。并且我们知道了模板编译阶段只存在于完整版中,在只包含运行时版本中不存在该阶段,这是因为在只包含运行时版本中,当使用vue-loadervueify时,*.vue文件内部的模板会在构建时预编译成渲染函数,所以是不需要编译的,从而不存在模板编译阶段。

然后对比了两种版本$mount方法的区别。它们的区别在于在$mount方法中是否进行了模板编译。在只包含运行时版本的$mount方法中获取到DOM元素后直接进入挂载阶段,而在完整版本的$mount方法中是先将模板进行编译,然后回过头调只包含运行时版本的$mount方法进入挂载阶段。

最后,我们知道了分析模板编译阶段其实就是分析完整版的vm.$mount方法的实现,我们将完整版的vm.$mount方法源码进行了逐行分析。知道了在该阶段中所做的工作就是:从用户传入的el选项和template选项中获取到用户传入的内部或外部模板,然后将获取到的模板编译成渲染函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/87063.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode322. 零钱兑换

322. 零钱兑换 文章目录 [322. 零钱兑换](https://leetcode.cn/problems/coin-change/)一、题目二、题解方法一&#xff1a;完全背包二维数组方法二&#xff1a;一维数组 三、注意 一、题目 给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 a…

【跟小嘉学习区块链】二、Hyperledger Fabric 架构详解

系列文章目录 【跟小嘉学习区块链】一、区块链基础知识与关键技术解析 【跟小嘉学习区块链】一、区块链基础知识与关键技术解析 文章目录 系列文章目录[TOC](文章目录) 前言一、Hyperledger 社区1.1、Hyperledger(面向企业的分布式账本)1.2、Hyperledger社区组织结构 二、Hype…

java性能安全:OOM问题排查、Arthas分析高CPU问题、防止Dos攻击

一、OOM问题 分析流程&#xff1a; 第一步&#xff1a;进程分析&#xff0c;分析老年代回收次数和消耗时间 第二步&#xff1a;日志分析&#xff0c;找出OOM发生时间的日志来锁定执行方法&#xff0c;对应的机器ip 第三步&#xff1a;找到对应的ip机器查看&#xff0c;进一步分…

UML六大关系总结

UML六大关系有&#xff1a;继承、关系、聚合、组合、实现、依赖。分为通过图和代码总结这些关系。 1、继承 继承&#xff08;Inheritance&#xff09;&#xff1a;表示类之间的继承关系&#xff0c;子类继承父类的属性和方法&#xff0c;并可以添加自己的扩展。 继承&#x…

Java反序列化漏洞

我想时至今日&#xff0c;这个漏洞依然存在&#xff0c;据说都已经有人写出了webshell&#xff0c;很恐怖呀。 接下来分析下这个漏洞。 Java序列化和反序列化 具体实现细节可参考: Java序列化机制和原理。 一个简易的漏洞程序 在Java反序列化中&#xff0c;会调用被反序列化…

【Spring Boot】Spring Boot源码解读与原理剖析

这里写目录标题 前言精进Spring Boot首选读物“小册”变“大书”&#xff0c;彻底弄懂Spring Boot全方位配套资源&#xff0c;学不会来找我&#xff01;技术新赛道&#xff0c;2023领先抢跑 前言 承载着作者的厚望&#xff0c;掘金爆火小册同名读物《Spring Boot源码解读与原理…

Windows11 手把手教授开放端口

首先在控制面板点击“系统与安全”&#xff0c;找到防火墙 然后点击“windows defender”打开防火墙 点击左侧目录栏中“高级设置” 点击“入站规则”&#xff0c;再点击新建入站规则&#xff08;开放端口有开放入站端口与开放出站端口之分&#xff0c;这里讲入站端口的开放…

老胡的周刊(第109期)

老胡的信息周刊[1]&#xff0c;记录这周我看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。 &#x1f3af; 项目 lobe-chat[2] LobeChat 是一个开源的、可扩展…

Elasticsearch 入门 索引、分词器

term, match_phrase, match查询 参考 ElasticSearch match, match_phrase, term的区别 term是对输入不分词&#xff0c;进行全文索引查询。存储时是否启用分词器&#xff0c;会影响查询效果match_phase对输入分词&#xff0c;但要求查询时将每个term都搜到&#xff0c;且顺序…

Redis 缓存雪崩、缓存穿透、缓存击穿

Redis 是一种常用的内存缓存工具&#xff0c;但在某些情况下&#xff0c;它可能会遭受缓存雪崩、缓存穿透和缓存击穿等问题。下面是一些预防这些问题的建议&#xff1a; 1、缓存雪崩 缓存雪崩指的是在某个时间点上&#xff0c;大量的缓存数据同时失效或过期&#xff0c;导致大…

华为云云耀云服务器L实例评测|华为云上安装kafka

文章目录 华为云云耀云服务器L实例评测&#xff5c;华为云上安装kafka一、kafka介绍二、华为云主机准备三、kafka安装1. 安装什么版本java2. 安装zookeeper服务3. 使用systemctl 管理启动ZooKeeper服务4. 修改kafka配置5. 使用systemctl 管理启动kafka服务6. 创建一个测试 topi…

前端JavaScript中的 == 和 ===区别,以及他们的应用场景,快来看看吧,积累一点知识。

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 一、等于操作符 二、全等操作符 三、区别 小结 一、等于操作符 等于操作符用两个等于号&#xff08; &am…

优先队列(priority_queue)用法详解

c优先队列(priority_queue)用法详解_c 优先队列_吕白_的博客-CSDN博客 既然是队列那么先要包含头文件#include <queue>, 他和queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队 优先队列具有队列的所有特性&#xff0c;包括基本操作…

map的一些测试-string键的查找

主要区别在于声明map的时候多了一个less<> #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string> #include <map> #include <chrono> using namespace std; class spender { public:spender(string strfun) :strfun(strfun…

Java面试题十二天

一、Java面试题十二天 1.为什么在阿里巴巴Java开发手册中强制要求使用包装类型定义属性呢&#xff1f; 默认值问题&#xff1a;使用基本数据类型定义属性时&#xff0c;如果没有给属性赋初始值&#xff0c;会使用默认值&#xff08;如 int 的默认值为 0&#xff09;&#xff…

SAP 操作:怎么设定屏幕前台字段显示/编辑

文章目录 前言一、步骤设定方式 前言 SAP将字段放进群组&#xff0c;通过对群组进行控制。 一、步骤 后勤常规-物料主数据-字段选择 设定方式 点击后面绿色按钮2.

WPF 类库 使用handycontrol 配置

在学习wpf发现了一个非常好用的UI库 handycontrol 但是很多地方讲的都是WPF应用程序怎么用&#xff0c;很少有讲类库那么引用的问题&#xff0c;所以在这里自己总结一下&#xff0c;希望能帮助到大家&#xff1a; 1.添加 handycontrol 的引用&#xff1b;安装&#xff0c;我已…

前端面试题记录

vue2响应式原理 vue2主要是采用了数据劫持结合发布者-订阅者模式来实现数据的响应式&#xff0c;vue在初始化的时候&#xff0c;会遍历data中的数据&#xff0c;使用object.defineProperty为data中的每一个数据绑定setter和getter&#xff0c;当获取数据的时候会触发getter&am…

HZOI-256:国王游戏

题目描述 ​ 恰逢 H 国国庆,国王邀请 n位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数&#xff0c;国王自己也在左、右手上各写一个整数。然后&#xff0c;让这 n 位大臣排成一排&#xff0c;国王站在队伍的最前面。排好队后&#xff0c;所有的大臣…