回顾$mount 之前发生了什么

借用 Vue 官方这张经典的图来说明:

Vue2.x源码解析系列八:深入$mount内部理解组件挂载和更新原理 - 图1

然我们回顾下在我们调用 $mount 挂载组件之前都发生了什么?,如上图所示,主要发生了这么些事情:

  • 构建 Vue 类,通过 initMixin, stateMixin等各种 xxxxMixinVue.prototype 上添加属性和方法,这在图中是没有显示的,因为这不是组件的生命周期,而是类的创建。
  • new Vue() 开始构建Vue实例,组件的生命周期在这里开始。
  • _init 函数中做 mergeOptions 合并配置,其中包括一些由Vue提供的默认配置比如 directives 等。图中并没有显示这一块
  • beforeCreate 阶段,还是在_init 函数中, 做 events,lifecycle 等初始化
  • create 阶段,做数据响应和数据注入。
  • template 编译为 render 函数,到这里就完成了 mount 的准备工作。

_init 结束之后,会调用 $mount 挂载组件,进入一个新的阶段 mount 阶段。那么,从 $mount 函数开始,到生成真实DOM,中间经历了哪些步骤呢?

Mount 开始

首先我们看 $mount 函数的定义:

platforms/web/runtime/index.js

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

可以看到它主要是调用了 mountComponent 函数,这个函数我们在之前讲到过,他会创建一个 Watcher 来监听 vm 的变动,一旦发生任何变化都会调用 vm._update 更新组件。mountComponent 核心的代码如下所示:

core/instance/lifecycle.js

  1. updateComponent = () => {
  2. vm._update(vm._render(), hydrating)
  3. }
  4. new Watcher(vm, updateComponent, noop, {
  5. before () {
  6. if (vm._isMounted) {
  7. callHook(vm, 'beforeUpdate')
  8. }
  9. }
  10. }, true /* isRenderWatcher */)

由于这里 lazy === false 所以在创建 watcher 的时候会立刻进行一次求值,也就是调用 vm._update(vm._render(), hydrating) 方法更新组件。这里的第一个参数是 vm._update 的结果,因此我们先看看这个函数做了什么:

core/instance/render.js

  1. Vue.prototype._render = function (): VNode {
  2. const vm: Component = this
  3. const { render, _parentVnode } = vm.$options
  4. // set parent vnode. this allows render functions to have access
  5. // to the data on the placeholder node.
  6. vm.$vnode = _parentVnode
  7. // render self
  8. let vnode
  9. try {
  10. vnode = render.call(vm._renderProxy, vm.$createElement)
  11. } catch (e) {
  12. // 省略
  13. vnode = vm._vnode
  14. }
  15. // set parent
  16. vnode.parent = _parentVnode
  17. return vnode
  18. }

上面的代码经过了我的简化。我们可以看到,_render 函数主要作用就是调用了 _renderProxy 生成 vnode,并返回,而 _renderProxy 其实就是调用了我们编译好的 render 函数。所以我们知道了,每当组件有任何更新的时候,都会调用 render 函数生成新的 vnode。那么我们再看 updateComponent 函数,当组件更新的时候就变成了这样:

  1. vm._update(vnode, hydrating)

因为我们更新了 vnode ,这个 _update 函数的目的显然是会进行 patch ,把更新同步到真实的 DOM 上。来看看 _update 函数的代码:

core/instance/lifecycle.js

  1. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  2. const vm: Component = this
  3. const prevEl = vm.$el
  4. const prevVnode = vm._vnode
  5. const prevActiveInstance = activeInstance
  6. activeInstance = vm
  7. vm._vnode = vnode
  8. // Vue.prototype.__patch__ is injected in entry points
  9. // based on the rendering backend used.
  10. if (!prevVnode) {
  11. // initial render
  12. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  13. } else {
  14. // updates
  15. vm.$el = vm.__patch__(prevVnode, vnode)
  16. }
  17. // 省略
  18. }

正如我们所料,_update 函数主要做的事情就是调用了 __patch__ 方法。 __patch__ 方法会负责把 vnode 渲染为真实的 DOM.我画了一个图,来表示整个 $mount 以及 update 的过程:

Vue2.x源码解析系列八:深入$mount内部理解组件挂载和更新原理 - 图2

$mount 函数其实是创建了上面这个流程,而不是发起了一次 渲染,因为在创建watcher的时候 lazy === false 所以创建完成之后立刻会触发一次更新。

图中的两个重要阶段 renderpatch 我们分别在接下来的两章详细讲解。

下一章,让我们看看 render 函数是如何生成 vnode 的。

下一章:Vue2.x源码解析系列九:vnode的生成与更新机制