上一篇中,我们一起探讨了new Vue({...})背后发生了什么。那么当我们实例化vue之后,进行dom挂载又发生了什么呢?
细心的同学会发现:$mount方法在多个文件中被定义,如:
- src/platform/web/entry-runtime-with-compiler.js
- src/platform/web/runtime/index.js
- src/platform/weex/runtime/index.js
之所以有多个地方,是因为$mount实现是和平台、构建方式都相关的
下面,我们选择compiler版本分析
一. $mount 主干代码如下:
Vue.prototype.$mount = function(el?: string | Element, hydrating?: boolean): Component {
el = el && query(el)
// query方法,实际上是对el参数做了一个转化,el可能是string 或者 element。如果是string,将返回document.querySelector(el)
// ...
const options = this.$options
if (!options.render) {
// render函数不存在
let template = options.template
if (template) {
// 如果存在template配置项:
// 1. template可能是"#xx",那么根据id获取element内容
// 2. 如果template存在nodeType,那么获取template.innerHTML 内容
}else {
// 如果template配置项不存在template,但是存在el:
/*
* 例如: new Vue({
* el: "#app",
* ...
* })
*
*/
// 那么根据el获取对应的element内容
}
// 经过上面的处理,将获取的template做为参数调用compileToFunctions方法
// compileToFunctions方法会返回render函数方法,render方法会保存到vm.$options下面
const { render, staticRenderFns } = compileToFunctions(template, {...})
options.render = render
}
return mount.call(this, el, hydrating)
}
从主干代码我们可以看出做了以下几件事
- 由于el参数有两种类型,可能是string 或者 element,调用query方法,统一转化为Element类型
- 如果没有手写render函数, 那么先获取template内容。再将template做为参数,调用compileToFunctions方法,返回render函数。
- 最后调用mount.call,这个方法实际上会调用runtime/index.js的mount方法
注:
- vue compiler分别2个版本:一个是构建时版本,即我们使用vue-loader + webpack。另一个版本是:运行时版本,运行的时候,再去compiler解析。我们这里分析的是 运行时
- vue最终只认render函数,所以如果我们手动写render函数,那么就直接调用mount.call。反之,vue会将template做为参数,运行时调用compileToFunctions方法,转化为render函数,再去调用mount.call方法。
- 如果是构建时版本,vue-loader + webpack,会先将我们本地的代码转化成render函数,运行将直接调用mount.call。生产环境,我们推荐构建时的版本。个人学习推荐运行时版本。
- mount.call方法,实际上会调用runtime/index.js下面的$mount方法,而这个方法很简单,将会调用mountComponent方法。
二. mountComponent 主干代码如下:
export function mountComponent(vm: Component, el: ?Element, hydrating?: boolean): Component {
// ...
// 调用beforeMount生命周期函数
callHook(vm, 'beforeMount')
// ...
// 定义updateComonent方法
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// ...
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true)
// ...
// 调用生命周期函数mounted
callHook(vm, 'mounted')
}
Watch类相关代码
Watch类有许多逻辑,这里我们只看和$mount相关的:
class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
){
// ...
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}else {
// ...
}
// ...
this.value = this.lazy ? undefined : this.get()
}
get() {
// ...
value = this.getter.call(vm, vm)
// ...
// cleanupDeps方法后面我们会分析,这个在性能优化上比较重要
return value;
}
}
从上面代码,可以看出:
- 先调用beforeMount钩子函数
- 将updateComponent方法做为参数,实例化Watch。Watch在这个有2个作用:
1、初始化的时候会执行回调函数
2、当 vm 实例中的监测的数据发生变化的时候执行回调函数
这里,我们先看第1个。 第2个将在数据变化监测章节分析
执行回调后,我们看到vm._update(vm._render(), hydrating)方法,这个方法分2个步骤:
(1) 执行render方法,返回最新的 VNode节点树
(2) 调用update方法,实际上进行diff算法比较,完成一次渲染 - 调用mounted钩子函数
三. 总结
- options上无render函数,对template, el做处理,获取template内容。
- 调用compileToFunctions方法,获取render函数,添加到options.render上
- 调用mount.call,实际上是调用mountComponent函数
- 调用beforeMount钩子
- 实例化渲染watcher,执行回调
- 根据render函数获取VNode节点树 (其实是一个js对象)
- 执行update方法,实际上是patch过程,vue会执行diff算法,完成一次渲染
- 调用mounted钩子
在下面的章节,我们将陆续分析: 响应式,compileToFunctions, 虚拟DOM,以及patch
码字不易,多多关注~😽
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。