简介
再讲挂载原理之前呢,先介绍下vue的四种不同的构建版本
可以看出,排除环境问题之外,构建版本有两种,一种完整版
,一种运行时版本
,那么两种有什么区别的?简单的说,完整版
包含编译器
,运行时版本
不包含编译器
,需要特别强调的是,运行时版本
比完整版
的体积小30%左右,另外vue-cli
默认是运行时的版本
,当然版本可以根据具体情况自由选择,那么vue-cli3
更改版本如下所示:
// vue.config.js
module.exports = {
configureWebpack: {
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
},
}
那么什么时候需要编译器呢?如下代码所示
// 如果传入一个字符串给template选项,这时候就需要编译器
new Vue({
template:<div>hi</div>
})
// 不需要编译器
new Vue({
render: c => c (xx)
})
OK,科普完毕,下面就进入咱们今天的主题vue挂载方式及原理
。
vue挂载方式
相信大家都知道,VUE主要挂载有两种方式,一种是el
选项,一种是$mount
,但实际上,无论我们在实例化VUE时是否设置了el
选项,想让VUE实例具有关联的DOM元素,只有vm.$mount
方法这一种途径,如源码所示:
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
那么,具体方式大概有以下方式
//1.el选项->template
new Vue({
el:"#app",
template:"<div>哈哈哈</div>"
})
//2.el选项->render
new Vue({
el:"#app",
render:c=>c(App)
})
//3. $mount ->template选项
new Vue({
template:"<div>哈哈哈</div>"
}).$mount("#app")
//4. $mount ->render
new Vue({
render:c=>c(App)
}).$mount("#app")
接下来,就从源码来分析下vm.$mount
的原理。
vue挂载原理
在文章开头的时候已经介绍过,vue构建方式分为完整版
和运行时
,所以$mount
的实现原理也会不同,首先请看以下代码
// public mount method
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
这段代码就是运行时的$mount
,具体逻辑先不讲,后续会讲,下面先讲下完整版的$mount
的方法
完整版vm.$mount的实现原理
ok,先贴上源码
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);
/* istanbul ignore if */
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 function
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);
}
if (template) {
/* istanbul ignore if */
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;
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile end');
measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
}
}
}
return mount.call(this, el, hydrating)
};
可以看出,完整版是在运行时的基础上利用了函数劫持
,将原型上的$mount
方法保存在mount
属性上,这里的$mount
的方法就是上面该章开头贴的那段代码,以便后续使用,然后在原型上重新定义一个$mount
方法,通过这种方式就可以在执行原有方法之前新增一些功能,那么接下来让我看下他加了哪些功能?
1.首先解析el
,就是传入的被挂载对象
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
}
}
以上代码可以看出,首先必须要传的,然后去找这个元素,如果没有找到,发出警告,然后新建一个div,如果找到直接返回。
2.接下来,判断el是否是html
或者body
,如果是发出警告,并直接返回vue实例
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
3.在接下来就是完整版
和运行时
的区别了,那就是判断是否有render
函数,如果没有,就把template/el
编译成render
函数
var options = this.$options;
if (!options.render) {
// 编译过程
}
return mount.call(this, el, hydrating)
注意,以上的$options
是实例化的时候传入的所有参数,这个值是在初始化的时候赋值的,通过这个条件可以明显看出如果给出render
选项,那么template
是无效的,下面就看下如果tempale
有效,它是怎么处理的
4.首先,如源码所示
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
选项,也就是挂载模板,如果提供了,又如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板。常用的技巧是用<script type="x-template">
包含模板,如果template
是字符串,但不是以#
号开头的,就说明template
是用户设置的模板,不需要改进,直接使用即可。如果template
选项不是字符串,而是一个DOM元素,则使用DOM元素的innerHTML作为模板,如果template
选择既不是字符串又不是DOM元素,那么VUE会发出警告,提示template
选项无效。
如果没有提供,那么就通过getOuterHTML
方法从用户提供的el
选项获取模板,getOuterHTML
逻辑如下
function getOuterHTML (el) {
if (el.outerHTML) {
return el.outerHTML
} else {
var container = document.createElement('div');
container.appendChild(el.cloneNode(true));
return container.innerHTML
}
}
上面逻辑就不说了,相信大家都能看懂,然后有了template
,怎么去解析呢?
5.解析模板
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile');
}
var ref = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== '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;
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end');
measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
}
}
这里重点是在compileToFunctions
,这里我只简单提一下,因为关于模板编译需要单独抽出来,单独写一章了,这里compileToFunctions
主要是把模板编译成渲染函数并设置到this.$options
上,而模板编译,总共有三部分组成,分别是解析器
,优化器
,代码生成器
,解析器
主要是将模板解析成AST
,优化器
主要是遍历AST
标记静态节点,这样再虚拟DOM中更新节点时,就不会重新渲染它了,而代码生成器
就是把AST
转化为代码字符串,从而转换成渲染函数。
讲到这里就是完整版vm.$mount的原理所有逻辑,下面讲下运行时
的逻辑。
只包含运行时版本的vm.$mount的原理
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
1.首先将id
转换为DOM元素,这里之前有讲过,在这不做讲解
2.mountComponent
函数,它的作用就是将vue实例绑定到DOM元素上,并实现持续性,也就是说每当数据变化,依然可以渲染到指定的DOM元素中。
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
);
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
);
}
}
}
callHook(vm, 'beforeMount');
var updateComponent;
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = "vue-perf-start:" + id;
var endTag = "vue-perf-end:" + id;
mark(startTag);
var vnode = vm._render();
mark(endTag);
measure(("vue " + name + " render"), startTag, endTag);
mark(startTag);
vm._update(vnode, hydrating);
mark(endTag);
measure(("vue " + name + " patch"), startTag, endTag);
};
} else {
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
首先判断是否存在渲染函数,如果不存在,则判断template
,这时的template
必须是id选择器
,因为运行时不包含编译阶段,只能是dom元素,如果是生产环境,则生成一个默认的渲染函数,也就是返回一个注释类型的VNode节点,并发出警告~
然后触发beforeMount
生命周期钩子,钩子出发后则真正的开始执行挂载,因为是持续性挂载,所以重点在于watcher
的使用,需要注意的是这句代码
vm._update(vm._render(), hydrating);
_update
作用是调用虚拟DOM的patch
方法进行新旧对比,而_render
则是生成一个新的VNode
节点数,那么vm._update(vm._render(), hydrating);
的作用就很明显了,就是将新的VNode
和旧的VNode
进行对比并更新DOM,简单的说就是执行了渲染操作~
okok,到这里咱们算真正的分享完了,有什么不对的地方,非常欢迎大家指教哦~谢谢大家