# Vue实例挂载的实现

入口的路径为: 1、 src/platforms/web/entry-runtime-with-compiler.js,运行时+编译版本的打包入口,重写了$mount=> 2、 src/platforms/web/runtime/index.js,Vue入口,对Vue的扩展=> 3、 src/core/index.js,对Vue初始化,initGlobalAPI(src/core/global-api/index.js,一些全局API)=>

4、 src/core/instance/index.js (opens new window),Vue的定义文件

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

this instanceof Vue如果不是用new创建Vue实例的话,this会指向上下文,放在全局就是window,会触发警告。

然后会调用 _init (opens new window) 方法,该方法在src/core/instance/init.js中,主要做了初始化生命周期、初始化事件、初始化渲染等。

_init方法主要最后调用了$mount方法,compiler版本的 $mount (opens new window) 定义在src/platforms/web/entry-runtime-with-compiler.js中。

先缓存了原型上的$mount,然后判断如果没有定义render方法的话,就把el或者template转为render,这个转化的过程是调用compileToFunctions方法实现的。

最后调用原先原型上的$mount方法实现挂载。

原先原型上的 $mount (opens new window) 方法在src/platforms/web/runtime/index.js中。

$mount方法实际上会去调用 mountComponent (opens new window) 方法,这个方法定义在src/core/instance/lifecycle.js文件中。

mountComponent 核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。

Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数。

if (vm.$vnode == null) {
  vm._isMounted = true
  callHook(vm, 'mounted')
}

这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。

# render

Vue 的 _render (opens new window) 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在 src/core/instance/render.js 文件中。

这段代码最关键的是 render (opens new window) 方法的调用,在 Vue 的官方文档中介绍了 render 函数的第一个参数是 createElement,那么结合之前的例子:

<div id="app">
  {{ message }}
</div>

相当于我们编写如下 render 函数:

render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}

再回到 _render 函数中的 render 方法的调用:

vnode = render.call(vm._renderProxy, vm.$createElement)

可以看到,render 函数中的 createElement 方法就是 vm.$createElement 方法。

vm.$createElement 方法定义是在执行 initRender 方法的时候,可以看到除了 vm.$createElement 方法,还有一个 vm._c 方法,它是被模板编译成的 render 函数使用,而 vm.$createElement是用户手写 render 方法使用的, 这俩个方法支持的参数相同,并且内部都调用了 createElement 方法。

# createElement

Vue.js利用 createElement (opens new window) 方法创建 VNode,它定义在 src/core/vdom/create-element.js 中:

createElement 方法实际上是对 _createElement (opens new window) 方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 _createElement

主要分析下children的规范化以及VNode的创建。

# children的规范化

由于 Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。_createElement 接收的第 4 个参数 children 是任意类型的,因此我们需要把它们规范成 VNode 类型。

这里根据 normalizationType 的不同,调用了 normalizeChildren(children) (opens new window)simpleNormalizeChildren(children) (opens new window) 方法,它们的定义都在 src/core/vdom/helpers/normalzie-children.js 中。

simpleNormalizeChildren 方法调用场景是 render 函数是编译生成的。理论上编译生成的 children 都已经是 VNode 类型的,但这里有一个例外,就是 functional component 函数式组件返回的是一个数组而不是一个根节点,所以会通过 Array.prototype.concat 方法把整个 children 数组打平,让它的深度只有一层。

normalizeChildren 方法的调用场景有 2 种,一个场景是 render 函数是用户手写的,当 children 只有一个节点的时候,Vue.js 从接口层面允许用户把 children 写成基础类型用来创建单个简单的文本节点,这种情况会调用 createTextVNode 创建一个文本节点的 VNode;另一个场景是当编译 slot、v-for 的时候会产生嵌套数组的情况,会调用 normalizeArrayChildren (opens new window) 方法,接下来看一下它的实现。

normalizeArrayChildren 接收 2 个参数,children 表示要规范的子节点,nestedIndex 表示嵌套的索引,因为单个 child 可能是一个数组类型。 normalizeArrayChildren 主要的逻辑就是遍历 children,获得单个节点 c,然后对 c 的类型判断,如果是一个数组类型,则递归调用 normalizeArrayChildren; 如果是基础类型,则通过 createTextVNode 方法转换成 VNode 类型;否则就已经是 VNode 类型了,如果 children 是一个列表并且列表还存在嵌套的情况,则根据 nestedIndex 去更新它的 key。这里需要注意一点,在遍历的过程中,对这 3 种情况都做了如下处理:如果存在两个连续的 text 节点,会把它们合并成一个 text 节点。

经过对 children 的规范化,children 变成了一个类型为 VNode 的 Array。

# VNode的创建

这里先对 tag 做判断,如果是 string 类型,则接着判断如果是内置的一些节点,则直接创建一个普通 VNode,如果是为已注册的组件名,则通过 createComponent 创建一个组件类型的 VNode,否则创建一个未知的标签的 VNode。 如果是 tag 一个 Component 类型,则直接调用 createComponent 创建一个组件类型的 VNode 节点,本质上它还是返回了一个 VNode

回到 mountComponent 函数的过程,我们已经知道 vm._render 是如何创建了一个 VNode,接下来就是要把这个 VNode 渲染成一个真实的 DOM 并渲染出来,这个过程是通过 vm._update 完成的

# update

Vue 的 _update (opens new window) 是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候。_update 方法的作用是把 VNode 渲染成真实的 DOM,它的定义在 src/core/instance/lifecycle.js 中:

_update 的核心就是调用 vm.__patch__ 方法,这个方法实际上在不同的平台,比如 web 和 weex 上的定义是不一样的,因此在 web 平台中它的定义在 src/platforms/web/runtime/index.js 中:

Vue.prototype.__patch__ = inBrowser ? patch : noop

可以看到,甚至在 web 平台上,是否是服务端渲染也会对这个方法产生影响。因为在服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是一个空函数,而在浏览器端渲染中,它指向了 patch (opens new window) 方法,它的定义在 src/platforms/web/runtime/patch.js

该方法的定义是调用 createPatchFunction 方法的返回值,这里传入了一个对象,包含 nodeOps 参数和 modules 参数。其中,nodeOps 封装了一系列 DOM 操作的方法,modules 定义了一些模块的钩子函数的实现,来看一下 createPatchFunction (opens new window) 的实现,它定义在 src/core/vdom/patch.js

createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch (opens new window) 方法,这个方法就赋值给了 vm._update 函数里调用的 vm.__patch__

patch方法,它接收 4个参数,oldVnode 表示旧的 VNode 节点,它也可以不存在或者是一个 DOM 对象;vnode 表示执行 _render 后返回的 VNode 的节点;hydrating 表示是否是服务端渲染;removeOnly 是给 transition-group 用的,之后会介绍。

patch 的逻辑看上去相对复杂,因为它有着非常多的分支逻辑,为了方便理解,我们并不会在这里介绍所有的逻辑,仅会针对我们之前的例子分析它的执行逻辑。之后我们对其它场景做源码分析的时候会再次回顾 patch 方法

先来回顾我们的例子:

var app = new Vue({
  el: '#app',
  render: function (createElement) {
    return createElement('div', {
      attrs: {
        id: 'app'
      },
    }, this.message)
  },
  data: {
    message: 'Hello Vue!'
  }
})

然后我们在 vm._update 的方法里是这么调用 patch 方法的:

// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

结合我们的例子,我们的场景是首次渲染,所以在执行 patch 函数的时候,传入的 vm.$el 对应的是例子中 id 为 app 的 DOM 对象,这个也就是我们在 index.html 模板中写的<div id="app">vm.$el 的赋值是在之前 mountComponent 函数做的,vnode 对应的是调用 render 函数的返回值,hydrating 在非服务端渲染情况下为 falseremoveOnlyfalse

确定了这些入参后,我们回到 patch 函数的执行过程,看几个关键步骤。

由于我们传入的 oldVnode 实际上是一个 DOM container,所以 isRealElement 为 true,接下来又通过 emptyNodeAt 方法把 oldVnode 转换成 VNode 对象,然后再调用 createElm (opens new window) 方法,这个方法在这里非常重要,来看一下它的实现:

createElm 的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。 我们来看一下它的一些关键逻辑,createComponent 方法目的是尝试创建子组件,这个逻辑在之后组件的章节会详细介绍,在当前这个 case 下它的返回值为 false;接下来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签;然后再去调用平台 DOM 的操作去创建一个占位符元素。