在 core/instance/init.js
中,我们可以看到是这样执行$mount
:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
看一下官网的描述,我们也能很好地理解:
如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。
下面,我们主要看一下$mount
内部实现的机制。
在Vue.js官网教程里,开始就介绍了关于vue的完整版
和runtime
两个版本:
-
完整版:同时包含编译器和运行时的版本。
-
编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。
-
运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。
其大致过程为: html字符串 → render函数 → vnode → 真实dom节点
而运行时渲染即所谓的去掉编译器的过程:render函数 → vnode → 真实dom节点
。我们来看看源码里是不是这么实现的。
首先是完整版
的实现:
// core/platforms/entry-runtime-with-compiler.js
// 此处mount即为运行时版的 $mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
...
// 如果不存在 render 函数,则会将模板转换成render函数
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
...
}
// 调用$mount方法
return mount.call(this, el, hydrating)
}
然后我们再看看runtime
的实现:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// 没有经过compileToFunctions这样的步骤,而是直接进行接下来 render --> VNode 的过程
// hydrating 是服务端渲染会用到的,此处我们姑且忽略
return mountComponent(this, el, hydrating)
}
我们知道,$mount
主要是实现了mountComponent
方法,我们找到mountComponent
方法,可以看一下:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果不存在render函数,则直接创建一个空的VNode节点
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
...
}
// 检测完render后,开始调用beforeMount声明周期
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 这里是上面所说的观察者,这里注意第二个expOrFn参数是一个函数
// 会在new Watcher的时候通过get方法执行一次
// 也就是会触发第一次Dom的更新
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
// 触发$mount函数
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
注意:这里用到了我们之前所一直说明的
Watcher
,也就是在这里定义的。这里先说明一下依赖收集的过程:因为Vue数据里定义的Data可能并不是所有数据都是视图渲染所需要的。也就是说,我们需要知道哪些数据的变动是需要更新视图,哪些是不需要重新渲染视图的, 在我们执行vm._watcher = new Watcher(vm, updateComponent, noop)
这句话的时候,会触发我们定义的Watcher
里面的get
方法。同时设置了Dep.target = watcher
。get 方法会去执行传入的updateComponent
方法,也就是说会去做template --> AST --> render Function --> VNode --> patch Dom
这样一个流程。这个过程中,会去读取我们绑定的数据。由于之前我们通过observer
进行了数据劫持,这样会触发数据的get
方法。此时会将watcher
添加到 对应的dep
中。当有数据更新时,通过dep.notify()
去通知到watcher
,然后执行watcher
中的update
方法。此时又会去重新执行get updateComponent
,至此完成对视图的重新渲染。
这里我们主要注意渲染部分
vm._update(vm._render(), hydrating)
vm._render
函数返回一个vnode
作为 vm._update
的第一个参数:
...
var render = vm.$options.render
try{
vnode = render.call(vm._renderProxy, vm.$createElement);
}catch{
...
}
此处,使用call方法, 将this指向 vm.renderProxy
,vm.renderProxy
是个代理,代理vm,主要用来报错,如果render函数上使用了vm上没有的属性或方法,就会报错。 vm.$createElement
这个是创建vnode的方法,作为第一个参数传入。
我们写render函数的时候会是这样:
render: function (createElement) {
return createElement('h1', '标题')
}
也就是说我们这里定义的render
,将会被执行返回一个VNode:
...
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.functionalContext = undefined
this.functionalOptions = undefined
this.functionalScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
...
接下来也就是将我们的虚拟节点VNode
转成真实的node节点。实现过程主要是通过vm.$el = vm.__patch__(prevVnode, vnode);
这部分
也就是patch
函数。
这里我们粗略的过了一遍数据渲染成dom的过程,后面章节我们会详细介绍其中的整体过程。