记录--手写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 ...
随机推荐
- 长沙IT技术圈百万年薪大佬?是否存在?
引子 不知不觉,IT技术圈开始流传起"百万年薪"的故事,有人问我,长沙有百万大佬么?其实我也不知道. 一 背景 长沙自古以来就是文风鼎盛之地,在今天也同样如此. 目前长沙有211. ...
- 基于HTTP2/3的流式请求/响应如何实现?
我想很多人已经体验过GRPC提供的三种流式消息交换(Client Stream.Server Stream和Duplex Stream)模式,在.NET Core上构建的GRPC应用本质上是采用HTT ...
- bootstrap响应式网页布局
bootstrap可以说是没有布局和审美概念的后端狗的福利.使用它的栅格系统可以轻松的对网页进行布局,而且可以做到响应式的布局.看起来很酷,接下来我就会总结自己在开发博客系统中用到的知识点及实战案例给 ...
- Java并发编程实例--1.创建和运行一个线程
从这一篇开始写Java并发编程实例,内容都翻译整理自书籍:<Java 7 Concurrency Cookbook> 谈到线程,无法逃避的一个问题就是: 并发(concurrency)和并 ...
- Singularity容器
"""参考文档 https://apptainer.org/user-docs/master/build_a_container.html ""&qu ...
- Google Chrome 开启多下载下载,提高文件下载速度
在地址栏输入: chrome://flags/#enable-parallel-downloading Parallel downloading改为Enabled后重启浏览器即可打开多线程下载 (多线 ...
- 【Python OO其二】设计模式之工厂模式(举例说明)
工厂模式 工厂模式中的"工厂"实际上就是把类看成制造某种模板的工具(工厂),由这个类生成的实例除了本身自有的属性外,还可以通过指定的方式产出具有不同属性的同一类实例 比如:有一个面 ...
- 名校 AI 课程|斯坦福 CS25:Transformers United 专题讲座
自 2017 年提出后,Transformer 名声大噪,不仅颠覆了自然语言处理(NLP)领域,而且在计算机视觉(CV).强化学习(RL).生成对抗网络(GANs).语音甚至是生物学等领域也大显锋芒, ...
- magic book
magic book.md body { font-family: var(--vscode-markdown-font-family, -apple-system, BlinkMacSystemFo ...
- Hugging Face 表情包来啦!
小编有一个朋友,微信聊基本不回复文字,内容和情绪都化身成表情包直接回复,并且一气呵成.自带上下文衔接.你身边有这样的朋友吗? 作为梦想成为第一家以表情符号上市的公司,以及在社交平台发文 emoji 不 ...