vue源码阅读(四)mount基本流程

在上篇中, 我们实现了一个简单的Compiler类来实现最简单的编译, 不难发现Compiler类还存在很多缺陷, 那么接下来我们看看vue是怎么做到

在initMixin函数中有这么一段代码来执行$mount

1
2
3
4
// 第六步:render & mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}

官网介绍

vm.$mount( [elementOrSelector] )
参数:

{Element | string} [elementOrSelector]
{boolean} [hydrating]
返回值:vm - 实例自身

用法:

如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。

如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。

这个方法返回实例自身,因而可以链式调用其它实例方法。

其大致过程为: html字符串 → render函数 → vnode → 真实dom节点 而运行时渲染即所谓的去掉编译器的过程:render函数 → vnode → 真实dom节点。

$mount的实现

我们可以发现vue运行 run dev的入口文件是 core/platforms/entry-runtime-with-compiler.js

所以我们先看看entry-runtime-with-compiler.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 此处mount即为运行时版的 $mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
....
// 如果不存在 render 函数,则会将模板转换成render函数
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, 发现这里的Vue来着import Vue from ‘./runtime/index’

看下这里对mount做了什么

1
2
3
4
5
6
7
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

render –> VNode

下面我们来看mountComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
// 如果不存在render函数,则直接创建一个空的VNode节点
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
}
callHook(vm, 'beforeMount');
var updateComponent;
updateComponent = function () {
// 渲染
vm._update(vm._render(), hydrating);
};
// 这里是上面所说的观察者,这里注意第二个expOrFn参数是一个函数
// 会在new Watcher的时候通过get方法执行一次
// 也就是会触发第一次Dom的更新
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
hydrating = false;
// 触发$mount函数
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}

注意:这里用到了我们之前所一直说明的Watcher,也就是在这里定义的。

这里先说明一下依赖收集的过程

​ 因为Vue数据里定义的Data可能并不是所有数据都是视图渲染所需要的。也就是说,我们需要知道哪些数据的变动是需要更新视图,哪些是不需要重新渲染视图的, 在我们执行new Watcher(vm, updateComponent, noop) 时,或执行到构造函数中的 vm._watcher =this,会触发我们定义的Watcher里面的get方法。同时设置了Dep.target = watcher

​ get 方法会去执行传入的updateComponent 方法,也就是说会去做template --> AST --> render Function --> VNode --> patch Dom这样一个流程。这个过程中,会去读取我们绑定的数据。

​ 由于之前我们通过observer进行了数据劫持( 在Vue的构造中执行了observer(vm._data) ),这样会触发数据的get方法。此时会将watcher添加到 对应的dep中。当有数据更新时,通过dep.notify()去通知到watcher,然后执行watcher中的update方法。此时又会去重新执行get updateComponent,至此完成对视图的重新渲染。

那么updateComponent函数是怎么来渲染的呢?

1
vm._update(vm._render(), hydrating)

vm._render

vm._render函数(在renderMixin中被混入Vue)返回一个vnode作为vm._update(在lifecycleMixin中定义)的第一个参数

在 core/render.js 中我们可以看到

1
2
3
4
5
6
7
...
var render = vm.$options.render
try{
vnode = render.call(vm._renderProxy, vm.$createElement);
}catch{
...
}

此处,使用call方法, 将this指向 vm.renderProxyvm.renderProxy是个代理,代理vm,主要用来报错,如果render函数上使用了vm上没有的属性或方法,就会报错。 vm.$createElement 这个是创建vnode的方法,作为第一个参数传入。 我们写render函数的时候会是这样:

1
2
3
render: function (createElement) {
return createElement('h1', '标题')
}

也就是说我们这里定义的render,将会被执行返回一个VNode:

vnode –> 真实的el节点

接下来也就是将我们的虚拟节点VNode转成真实的node节点。实现过程主要是通过vm.$el = vm.__patch__(prevVnode, vnode);这部分 也就是patch函数。