记录--手写vm.$mount方法
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

一、概述
在我们开发中,经常要用到Vue.extend创建出Vue的子类来构造函数,通过new 得到子类的实例,然后通过$mount挂载到节点,如代码:
<div id="mount-point"></div>
<!-- 创建构造器 -->
var Profile = Vue.extend({
template:'<p>{{firstName}} {{lastName}} aka{{alias}}</p>',
data:function(){
return{
firstName:'Walter',
lastName:'White',
alias:'Heisenberg'
}
}
})
<!-- 创建Profile实例,并挂载到一个元素上 -->
new Profile().$mount('#mount-point');
$mount方法是怎么实现的,篇文章就来讲一下
二、使用方式
vm.$mount( [elementOrSelector] )
(1)参数
{ Element | string } [elementOrSelector]
(2)返回值
vm,即实例本身。
(3)用法
1、如果Vue.js实例在实例化时没有收到el选项,则它处于“未挂载”状态,没有关联的DOM元素。 2、可以使用vm.$mount手动挂载一个未挂载的实例。 3、如果没有提供elementOrSelector参数,模板将被渲染为文档之外的元素,并且必须使用原生DOM的API把它插入文档中。 4、这个方法返回实例自身,因而可以链式调用其他实例方法。
(4)例子
var MyComponent = Vue.extend({
template:'<div>Hello!</div>',
})
<!-- 创建并挂载到#app(会替换#app) -->
new MyComponent().$mount('#app');
<!-- 创建并挂载到#app(会替换#app) -->
new MyComponent().$mount({el:'#app'});
<!-- 创建并挂载到#app(会替换#app) -->
var component = new MyComponent().$mount();
document.getElementById('app').appendChild(component.$el);
1、在不同的构建版本中,vm.$mount的表现都不一样。其差异主要体现在完整版(vue.js)和只包含运行时版本(vue.runtime.js)之间。
2、完整版和只包含运行时版本之间的差异在于是否有编译器,而是否有编译器的差异主要在于vm.$mount方法的表现形式。
3、在只包含运行时的构建版本中,vm.mount的作用会稍有不同,它首先会检查template或el选项所提供的模板是否已经转换成渲染函数(render函数)。如果没有,则立即进入编译过程,将模板编译成渲染函数,完成之后再进入挂载与渲染的流程中。
4、只包含运行时版本的vm.$mount没有编译步骤,它会默认实例上已经存在渲染函数,如果不存在,则会设置一个。并且,这个渲染函数在执行时会返回一个空节点的VNode,以保证执行时不会因为函数不存在而报错。同时如果是开发环境下运行,Vue.js会触发警告,提示我们当前使用的是只包含运行时的版本,会让我们提供渲染函数,或者去使用完整的构建版本。
5、从原理的角度来讲,完整版和只包含运行时版本之间是包含关系,完整版包含只包含运行时版本。
三、完整版vm.$mount的实现原理
(1)实现代码
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
<!-- 做些什么 -->
return mount.call(this,el);
}
1、将Vue原型上的$mount方法保存在mount中,以便后续使用。
2、然后Vue原型上的$mount方法被一个新的方法覆盖了。新方法中会调用原始的方法,这种做法通常被称为函数劫持。(看源码的同学可能发现了,vue多处用了函数劫持的做法,例如:对数组实现监听的时候...)
3、通过函数劫持,可以在原始功能上新增一些其他功能。上面代码中,vm.$mount的原始方法就是mount的核心功能,而在完整版中需要将编译功能新增到核心功能上去。
(2)由于el参数支持元素类型或者字符串类型的选择器,所以第一步是通过el获取DOM元素。
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
return mount.call(this,el);
}
使用query获取DOM元素
function query(el){
if(typeof el === 'string'){
const selected = document.querySelector(el);
if(!selected){
return document.createElement('div');
}
return selected;
}else{
return el;
}
}
1、如果el是字符串,则使用doucment.querySelector获取DOM元素,如果获取不到,则创建一个空的div元素。
2、如果el不是字符串,那么认为它是元素类型,直接返回el(如果执行vm.$mount方法时没有传递el参数,则返回undefined)
(3)编译器
1、首先判断Vue.js实例中是否存在渲染函数,只有不存在时,才会将模板编译成渲染函数。
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
const options = this.$options;
if(!options.render){
<!-- 将模板编译成渲染函数并赋值给options.render -->
}
return mount.call(this,el);
}
2、在实例化Vue.js时,会有一个初始化流程,其中会向Vue.js实例上新增一些方法,这里的this.$options就是其中之一,它可以访问到实例化Vue.js时用户设置的一些参数,例如tempalte和render。
3、如果在实例化Vue.js时给出了render选项,那么template其实是无效的,因为不会进入模板编译的流程,而是直接使用render选项中提供的渲染函数。
4、Vue.js在官方文档的template选项中也给出了相应的提示。如果没有render选项,那么需要获取模板并将模板编译成渲染函数(render函数)赋值给render选项。
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
const options = this.$options;
if(!options.render){
<!-- 新增获取模板相关逻辑 -->
let template = options.template;
if(template){ }else if(el){
template = getOuterHTML(el);
}
}
return mount.call(this,el);
}
5、从选项中取出template选项,也就是取出用户实例化Vue.js时设置的模板。如果没有取到,说明用户没有设置tempalte选项。那么使用getOuterHTML方法从用户提供的el选项中获取模板。
function getOuterHTML(el){
if(el.outerHTML){
return el.outerHTML;
}else{
const container = document.createElement('div');
container.appendChild(el.cloneNode(true));
return container.innerHTML;
}
}
6、getOuterHTML方法会返回参数中提供的DOM元素的HTML字符串。
7、整体逻辑
如果用户没有通过template选项设置模板,那么会从el选项中获取HTML字符串当作模板。如果用户提供了template选项,那么需要对它进一步解析,因为这个选项支持很多种使用方式。template选项可以直接设置成字符串模板,也可以设置为以#开头的选择符,还可以设置成DOM元素。
8、从不同的格式中将模板解析出来
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
const options = this.$options;
if(!options.render){
<!-- 新增获取模板相关逻辑 -->
let template = options.template;
if(template){
if(typeof tempalte === 'string'){
if(tempalte.charAt(0) === "#"){
template = idToTemplate(tempalte);
}
}else if(tempalte.nodeType){
template = template.innerHTML;
}else{
if(process.env.NODE_ENV !== 'production'){
warn('invalid template option:'+tempalte,this);
}
return this;
}
}else if(el){
template = getOuterHTML(el);
}
}
return mount.call(this,el);
}
9、如果tempalte是字符串并且以#开头,则它将被用作选择符。通过选择符获取DOM元素后,会使用innerHTML作为模板。
10、使用idToTemplate方法从选择符中获取模板。idToTemplate使用选择符获取DOM元素之后,将它的innerHTML作为模板。
function idToTemplate(id){
const el = query(id);
return el && el.innerHTML;
}
11、如果template是字符串,但不是以#开头,就说明template是用户设置的模板,不需要进行任何处理,直接使用即可。
12、如果template选项的类型不是字符串,则判断它是否是一个DOM元素,如果是,则使用DOM元素的innerHTML作为模板。如果不是,只需要判断它是否具备nodeType属性即可。
13、如果tempalte选项既不是字符串,也不是DOM元素,那么Vue.js会触发警告,提示用户template选项是无效的。
14、获取模板之后,下一步是将模板编译成渲染函数,通过执行compileToFunctions函数可以将模板编译成渲染函数并设置到this.options上。
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
el = el && query(el);
const options = this.$options;
if(!options.render){
<!-- 新增获取模板相关逻辑 -->
let template = options.template;
if(template){
if(typeof tempalte === 'string'){
if(tempalte.charAt(0) === "#"){
template = idToTemplate(tempalte);
}
}else if(tempalte.nodeType){
template = template.innerHTML;
}else{
if(process.env.NODE_ENV !== 'production'){
warn('invalid template option:'+tempalte,this);
}
return this;
}
}else if(el){
template = getOuterHTML(el);
}
<!-- 新增编译相关逻辑 -->
if(tempalte){
const { render } = compileToFunctions(
template,
{...},
this
)
options.render = render;
}
}
return mount.call(this,el);
}
15、将模板编译成代码字符串并将代码字符串转换成渲染函数的过程是在compileToFunctions函数中完成的,其内部实现如下
function compileToFunctions(template,options,vm){
options = extend({},options);
<!-- 检查缓存 -->
const key = options.delimiters
? String(options.delimiters)+tempalte
:template;
if(cache[key]){
return cache[key];
}
<!-- 编译 -->
const compiled = compile(template,options);
<!-- 将代码字符串转换为函数 -->
const res = {};
res.render = createFunction(compiled.render);
return (cache[key] = res)
}
function createFunction(code){
return new Function(code);
}
1)首先,将options属性混合到空对象中,其目的是让options称为可选参数。
2)检查缓存中是否已经存在编译后的模板。如果模板已经被编译,就会直接返回缓存中的结果,不会重复编译,保证不做无用功来提升性能。
3)调用compile函数来编译模板,将模板编译成代码字符串并存储在compiled中的render属性中。
4)调用createFunction函数将代码字符串转换成函数。其实现原理箱单简单,使用new Function(code)就可以完成。
5)在代码字符串被new Function(code)转换成函数之后,当调用函数时,代码字符串会被执行。例如
const code = 'console.log("Hello Berwin")';
const render = new Function(code);
render();//Hello Berwin
6)最后,将渲染函数返回给调用方。
16、当通过compileToFunctions函数得到渲染函数之后,将渲染函数设置到this.$options上。
四、只包含运行时版本的vm.$mount的实现原理
(1)只包含运行时版本的vm.
mount方法的核心功能。实现如下
Vue.prototype.$mount = function(el){
el = el && inBrower ? query(el) : undefined;
return mountComponent(this,el);
}
1、$mount方法将ID转换为DOM元素后,使用mountComponent函数将Vue.js实例挂载到DOM元素上。
2、将实例挂载到DOM元素上指的是将模板渲染到指定的DOM元素中,而且是持续性的,以后当数据(状态)发生变化时,依然可以渲染到指定的DOM元素中。
3、实现这个功能需要开启watcher。
watcher将持续观察模板中用到的所有数据(状态),当这些数据(状态)被修改时它将得到通知,从而进行渲染操作。这个过程回持续到实例被销毁。
export function mountComponent(vm,el){
if(!vm.$options.render){
vm.$options.render = createEmptyVNode;
if(process.env.NODE_ENV !== 'production'){
<!-- 在开发环境发出警告 -->
}
}
}
4、mountComponent方法会判断实例上是否存在渲染函数。如果不存在,则设置一个默认的渲染函数createEmptyVNode,该渲染函数执行后,会返回一个注释类型的VNode节点。
5、事实上,如果在mountComponent方法中发现实例上没有渲染函数,则会将el参数指定页面中的元素节点替换成一个注释节点,并且在开发环境下在浏览器的控制台中给出警告。
(2)Vue.js实例在不同的阶段会触发不同的生命周期钩子,在挂载实例之前会触发beforeMount钩子函数。
export function mountComponent(vm,el){
if(!vm.$options.render){
vm.$options.render = createEmptyVNode;
if(process.env.NODE_ENV !== 'production'){
<!-- 在开发环境发出警告 -->
}
callHook(vm,'beforeMount')
}
}
1、钩子函数触发后,将执行真正的挂载操作。挂载操作与渲染类似,不同的是渲染指的是渲染一次,而挂载指的是持续性渲染。挂载之后,每当状态发生变化时,都会进行渲染操作。
(3)mountComponent具体实现
export function mountComponent(vm,el){
if(!vm.$options.render){
vm.$options.render = createEmptyVNode;
if(process.env.NODE_ENV !== 'production'){
<!-- 在开发环境发出警告 -->
}
<!-- 触发生命周期钩子 -->
callHook(vm,'beforeMount');
<!-- 挂载 -->
vm._watcher = new Watcher(vm,()=>{
vm._update(vm._render())
},noop);
<!-- 触发生命周期钩子 -->
callHook(vm,'mounted');
return vm;
}
}
1、vm._update作用:调用虚拟DOM中的patch方法来执行节点的比对与渲染操作。
2、vm._render作用:执行渲染函数,得到一份新的VNode节点树。
3、vm._update(vm._render())作用:先调用渲染函数得到一份最新的VNode节点树,然后通过vm._update方法对最新的VNode和上一次渲染用到的旧VNode进行对比并更新DOM节点。简单来说,就是执行了渲染操作。
(4)挂载是持续性的,而持续性的关键就在于new Watcher这行代码。
1、Watcher的第二个参数支持函数,并且当它是函数时,会同时观察函数中所读取的所有Vue.js实例上的响应式数据。
2、当watcher执行函数时,函数中所读取的数据都将会触发getter去全局找到watcher并将其收集到函数的依赖列表中。即,函数中读取的所有数据都将被watcher观察。这些数据中的任何一个发生变化时,watcher都将得到通知。
3、当数据发生变化时,watcher会一次又一次地执行函数进入渲染流程,如此反复,这个过程会持续到实例被销毁。
4、挂载完毕后,会触发mounted钩子函数。
如果不懂watcher,其实可以去掉看,就简单很多
export function mountComponent(vm,el){
if(!vm.$options.render){
vm.$options.render = createEmptyVNode;
if(process.env.NODE_ENV !== 'production'){
<!-- 在开发环境发出警告 -->
}
<!-- 触发生命周期钩子 -->
callHook(vm,'beforeMount');
<!-- 挂载 -->
vm._update(vm._render())
<!-- 触发生命周期钩子 -->
callHook(vm,'mounted');
return vm;
}
}
这样,是不是很容易理解了。整个mountComponent,一句关键代码:vm._update(vm._render()),表示通过执行vm._render()得到VNode,再把VNode传入vm._update() ,vm._update()得功能是 将传入的VNode 变成 真实Dom渲染到页面。
简单地总结一下:
$mount()的思路就是, 判断 用户传入的option有没有render函数,
1.有的话就走运行时版本,
2.没有的话就自动生成render函数,然后在执行运行时版本(其实这就是编译时版本,比运行时版本多了异步生成render函数的步骤)。
执行运行时版本的时候,
- 通过render()获得Vnode
- 把Vnode传入_update() 实现渲染
本文转载于:
https://juejin.cn/post/6844904181438889991
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--手写vm.$mount方法的更多相关文章
- POJ 3984 迷宫问题【BFS/路径记录/手写队列】
迷宫问题 Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 31428 Accepted: 18000 Description 定义 ...
- 手写call,apply方法实现
call Function.prototype.myCall = function(){ var object = arguments[0]; var arr = []; for(var i = 1; ...
- 手写redux方法以及数组reduce方法
reduce能做什么? 1)求和 2)计算价格 3)合并数据 4)redux的compose方法 这篇文章主要内容是什么? 1)介绍reduce的主要作用 2)手写实现reduce方法 0)了解red ...
- JavaScript手写new方法
1.看一下正常使用的new方法 function father(name){ this.name=name; this.sayname=function(){ console.log(this.nam ...
- JavaScript数组方法总结及手写
目录 手写数组衍生方法 1.检测是否为数组 2.类数组转化为数组 3.数组扁平化 4.数组去重 5.数组使用Math.max 手写数组内置方法 1. Array.prototype.filter 2. ...
- 如果选择构建ui界面方式,手写代码,xib和StoryBoard间的博弈
代码手写UI这种方法经常被学院派的极客或者依赖多人合作的大型项目大规模使用. 大型多人合作项目使用代码构建UI,主要是看中纯代码在版本管理时的优势,检查追踪改动以及进行代码合并相对容易一些. 另外,代 ...
- 【Xamarin挖墙脚系列:代码手写UI,xib和StoryBoard间的博弈,以及Interface Builder的一些小技巧(转)】
正愁如何选择构建项目中的视图呢,现在官方推荐画板 Storybord...但是好像 xib貌似更胜一筹.以前的老棒子总喜欢装吊,用代码写....用代码堆一个HTML页面不知道你们尝试过没有.等页面做出 ...
- 关于代码手写UI,xib和StoryBoard
代码手写UI 这种方法经常被学院派的极客或者依赖多人合作的大型项目大规模使用.Geek们喜欢用代码构建UI,是因为代码是键盘敲出来的,这样可以做到不开IB,手不离开键盘就完成工作,可以专注于编码环境, ...
- uni-app通过canvas实现手写签名
分享一个uni-app实现手写签名的方法 具体代码如下: <template> <view > <view class="title">请在下面 ...
- (手写识别) Zinnia库及其实现方法研究
Zinnia库及其实现方法研究 (转) zinnia是一个开源的手写识别库.采用C++实现.具有手写识别,学习以及文字模型数据制作转换等功能. 项目地址 [http://zinnia.sourcefo ...
随机推荐
- JS leetcode 杨辉三角 超详细题解分析
壹 ❀ 引 刷leetcode的第四天,原题出处为杨辉三角,题目描述如下: 给定一个非负整数 numRows,生成杨辉三角的前 numRows 行. 在杨辉三角中,每个数是它左上方和右上方的数的和. ...
- 【分布式】load balance 02-consistent hash algorithm 一致性哈希算法原理详解
负载均衡系列专题 01-负载均衡基础知识 02-一致性 hash 原理 03-一致性哈希算法 java 实现 04-负载均衡算法 java 实现 概念 一致哈希是一种特殊的哈希算法. 在使用一致哈希算 ...
- JavaScript选择器
Js选择器 JS选择器常用的有getElementById().getElementsByClassName().getElementsByName().getElementsByTagName(). ...
- leetcode - 中序遍历
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 . 示例 1: 输入:root = [1,null,2,3] 输出:[1,3,2] 示例 2: 输入:root = [] 输出:[] 示例 ...
- 02-Redis系列之-架构和高级API的使用
通用部分 通用命令 # 1-keys # 打印出所有key keys * # 打印出所有以n开头的key keys n* # 打印出所有以nam开头,第四个字母是a到z的范围 keys nam[a-z ...
- 【Azure 应用服务】如何关掉App Service/Function App的FTP部署, 使之变成FTPS
问题描述 如何关掉App Service/Function App的FTP部署, 使之变成FTPS方式呢? 问题解答 在应用服务/函数应用的配置下选择右边的常规设置,然后修改FTP状态为"仅 ...
- 手把手教你用 NebulaGraph AI 全家桶跑图算法
前段时间 NebulaGraph 3.5.0 发布,@whitewum 吴老师建议我把前段时间 NebulaGraph 社区里开启的新项目 ng_ai 公开给大家. 所以,就有了这个系列文章,本文是该 ...
- C++基本知识梳理
一.命名空间 概念:命名空间是新定义的一个作用域,里面可以放函数,变量,定义类等,主要用来防止命名冲突. 实现:namespace关键字 命名空间名字{ 命名空间成员 } 注意点: 1.命名空间可以嵌 ...
- Mysql基础目录
尚硅谷Mysql课程笔记 课程链接: https://www.bilibili.com/video/BV1iq4y1u7vj?p=1 第01章_数据库概述 第02章_MySQL环境搭建 第03章_基本 ...
- 案例8:将"picK"的大小写互换
最终输出结果为PICk. 需要先计算两个字母之间的间隔,比如a和A之间的间隔为多少. 然后在将大写字母转换为小写字母,加上间隔的值: 将小写字母转换为大写字母,减去间隔的值. 示例代码如下: #def ...