Vue 实例挂载的实现
在 上一节 中,我们看到会调用 vm.$mount(vm.$options.el); 来挂载实例,那么 $mount 是什么?
$mount 方法在多个文件中都有定义,分别对应不同构建方式和平台。因为我们这次主要看 runtime-with-compiler,所以先看一下 src/platform/web/entry-runtime-with-compiler.js 中的定义。
// 先保存之前通用的 $mount 方法,定义在 src/platform/web/runtime/index.js
// 此处定义的是针对于 entry-runtime-with-compiler 的 $mount方法
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// el 可以传入字符串或者DOM对象
el = el && query(el);
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== "production" &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
);
return this;
}
const options = this.$options;
// resolve template/el and convert to render function
// 生成 render 函数
if (!options.render) {
let template = options.template;
if (template) {
if (typeof template === "string") {
// Vue.component('anchored-heading', {
// template: '#anchored-heading-template',
// })
if (template.charAt(0) === "#") {
// 会寻找template的内容
template = idToTemplate(template);
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
);
}
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
if (process.env.NODE_ENV !== "production") {
warn("invalid template option:" + template, this);
}
return this;
}
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
// 此时,template为字符串
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
mark("compile");
}
// 生成 render 函数
const { render, staticRenderFns } = compileToFunctions(
template,
{
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
);
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");
}
}
}
return mount.call(this, el, hydrating);
};
- 在此处,先缓存了原型上的
$mount保存在mount中。 - 重新定义
$mount,传入参数为el。 - 对
el做限制,不能传入html和body这样的根节点。 - 如果没有
options.render方法,则会获取template字符串(根据template或者el)。 - 将
template作为参数传入compileToFunctions生成render方法。编译过程暂时不看。 - 最后调用
mount。
原型上的$mount 定义在src/platform/web/runtime/index.js中。
// 通用的 $mount 函数
// 在不同的平台有可能被改写专用的 $mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
$mount 函数支持传入两个参数,第一个是 el,表示挂载的元素,可以为字符串或者 DOM,第二个参数和服务端渲染有关,我们不需要。
$mount 方法实际上会调用mountComponent方法,定义在src/core/instance/lifecycle.js
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
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");
let updateComponent;
// 开发模式下性能分析
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
updateComponent = () => {
const name = vm._name;
const id = vm._uid;
const startTag = `vue-perf-start:${id}`;
const endTag = `vue-perf-end:${id}`;
mark(startTag);
const 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 = () => {
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
// 在此处为渲染 Watcher
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
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;
}
mountComponent 方法首先会校验开发环境和 options,然后触发 beforeMount 生命周期,如果不是开发模式,则定义 updateComponent 函数。
核心是实例化一个渲染 Watcher,在他的回调中调用 updateComponent,在此方法中先调用 vm._render 生成虚拟 Node,最终调用vm._update 更新 DOM,
Watcher 在这里起到两个作用,一是初始化的时候执行回调函数,二是在监测到数据变化时执行回调函数。关于 _render 和 _update 在后面介绍。
最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。
总结
在 Vue 实例挂载的实现中,最核心的就是 mountComponent 方法,其逻辑也很清晰,他会完成整个渲染工作。接下来会着重看_render 和_update 这两个方法。