建议PC端观看,移动端代码高亮错乱
在 上一章中我们介绍了 _init 方法。 在_init的最后执行了挂载的操作
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
$mount 方法在多个文件中都有定义。因为 $mount 这个方法的实现是和平台、构建方式都相关的。接下来我们重点分析带 compiler 版本的 $mount 实现。
1. 重写的 $mount
src/platforms/web/entry-runtime-with-compiler.js
// src/platforms/web/entry-runtime-with-compiler.js
// 缓存公共的 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 将 el 转换成 DOM 节点
el = el && query(el)
// 挂载节点不能是 html 或 body 节点
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
// 不存在 render 则解析 template 并转换成 render函数
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板
template = idToTemplate(template)
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 没有配置时,将 el 的outerHTML 作为模板,
// 这也就是为什么 el 不能是 body 和 html的原因
template = getOuterHTML(el)
}
if (template) {
// 性能监控...
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
// 性能监控...
}
}
return mount.call(this, el, hydrating)
}
- 先把公共的
$mount方法缓存下来 query方法将el转换成DOM节点,query方法很简单,就是调用document.querySelector(el)返回DOM:
// src/platforms/web/util/index.js
export function query (el: string | Element): Element {
if (typeof el === 'string') {
const selected = document.querySelector(el)
if (!selected) {
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + el
)
return document.createElement('div')
}
return selected
} else {
return el
}
}
回到重写的 $mount 函数中接着往下看:
- 判断
el是否是html或body标签 - 处理
template,下文详细分析 - 将
template转成render,主要是通过compileToFunctions实现,这个我们在以后的编译章节再说 - 执行缓存下来的公共
$mount方法
1.1 处理 template
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板
template = idToTemplate(template)
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);
}
- 首先判断
template是否存在- 如果
template是否以#开头的字符串,如果是的话则它将被用作选择符,并使用匹配元素的innerHTML作为模板。通过idToTemplate的实现,稍后分析一下这个函数。 - 如果
template具有nodeType属性,则其是一个 DOM,然后template = template.innerHTML - 兜底情况是抛出一个错误:
invalid template option:
- 如果
- 不存在则调用
getOuterHTML函数,稍后分析一下这个函数。
先来看看 idToTemplate 这个函数:
// src/platforms/web/entry-runtime-with-compiler.js
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
可以看到用 cached 方法包了一下,参数是一个箭头函数,箭头函数做了两件事:
- 根据
id获得 DOM 对象 - 返回
el.innerHTML。
然后 cached 方法定义在 src/shared/util.js中
// src/shared/util.js
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null)
return (function cachedFn (str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
- 创建一个纯净的
cache对象,用来保存template和innerHTML的键值对 - 返回一个
cachedFn函数 - 当调用
idToTemplate时,会执行这个cachedFn函数,参数传入的是#开头的template字符串,如果template存在于cache对象中,那么直接返回值,否则return cache[str] = fn(str)
最后再看看 getOuterHTML 函数:
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
// cloneNode(true):深度克隆子节点
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
2. 公共的 $mount
定义在 src/platforms/web/runtime/index.js 中
// src/platforms/web/runtime/index.js
// 公共的 mount 方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
传入两个参数:
el:挂载对象hydrating:服务器渲染相关
做了两件事:
- 如果存在
el且是浏览器环境,那么调用query获得 DOM 对象,否则返回undefined。为什么这里还要重复修正el呢?我们的el不是再上面已经处理完了吗? 这是因为runTime Only版本的$mount逻辑只有这里而已 - 执行
mountComponent方法
mountComponent 方法定义在 src/core/instance/lifecycle.js 文件中
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// render函数不存在时
if (!vm.$options.render) {
// 创建一个空的vnode
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
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
)
}
}
}
// 执行 beforeMount 钩子
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 实例化渲染 watcher
new Watcher(vm, updateComponent, noop, {
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
}
- 先实例化一个
渲染Watcher,初始化会触发updateComponent,并且之后vm实例中的监测的数据发生变化的时候也会触发updateComponent,这个以后再介绍。 - 在
updateComponent方法中调用vm._render方法先生成虚拟 Node,最终调用vm._update更新DOM。关于vm._render和vm._update的分析在之后的章节再展开。 - 函数最后判断为根节点的时候设置
vm._isMounted为 true, 表示这个实例已经挂载了,同时执行mounted钩子函数。 这里注意vm.$vnode表示实例的占位符VNode,它为Null则表示当前是根Vue实例。
2.1 分析 渲染Wather
这里我们分析渲染Wather只做简要分析,更多依赖收集的细节不做讨论
// src/core/observer/watcher.js
export default class Watcher {
// 一些实例属性...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
// ...
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// ...
}
// this.lazy 是 false
this.value = this.lazy ? undefined : this.get()
}
}
首先判断是否为 渲染Wather,将 this 赋值给 vm._wather,因为在 watcher 的初始化 patch阶段 可能会调用 $forceUpdate (比如在子组件的 moutned 钩子内部),这取决于 vm._watcher 是否定义。其实看看 forceupdate 的代码就知道了:
Vue.prototype.$forceUpdate = function () {
var vm = this;
if (vm._watcher) {
// 就是调用这个 `vm` 的 `_watcher` 的 `update` 方法。用来强制更新
vm._watcher.update();
}
};
回到实例化 渲染Watcher 过程,最终会执行 this.get(),在当前情况下内部实际上只是执行了 this.getter.call(vm, vm),也就是会执行 updateComponent。
总结
mountComponent 方法的逻辑也是非常清晰的,它会完成整个渲染工作,接下来我们要重点分析其中的细节,也就是最核心的 2 个方法:vm._render 和 vm._update。