Vue源码窥探之new Vue之后发生了什么?

为了完整的了解整个机制,我们最好选择客户端 compile 的版本,也即 Runtime + Compiler

在我们使用 Vue-Cli 初始化一个 Vue 项目的时候,会询问我们使用 Runtime Only 版本还是 Runtime + Compiler 版本。

  • Runtime Only
    使用该版本时,我们需要借助 webpackvue-loader 工具来把 .vue 文件编译成 JavaScript ,体积比较小。
  • Runtime + Compiler
    使用该版本时,客户端即可以对 template 进行编译。
// 需要编译器的版本
new Vue({
  template: '
{{ hi }}
' }) // 这种情况不需要 new Vue({ render (h) { return h('div', this.hi) } })

确定入口

因为我们使用 Runtime + Compiler 版本,所以我们来 src/platforms/web/entry-runtime-with-compiler.js 中,可以看到 Vueimport Vue from './runtime/index' 引入;
我们再来 src/platforms/web/runtime/index.js 中,可以看到 Vueimport Vue from 'core/index' 中引入;
所以我们再来 src/core/index.js 中。

Vue源码窥探之new Vue之后发生了什么?_第1张图片
我们看到,此处引入了 Vue ,又初始化了 GlobalAPI 。我们再去寻找最初的 Vue 。在 src/core/instance/index.js 中,我们可以看到 Vue 的定义,真不容易。终于找到 Vue 了。
Vue源码窥探之new Vue之后发生了什么?_第2张图片
在我们实例化 Vue 的时候会调用其原型上的 _init 方法。这里没有采用 Class 写法,是因为方法把不同功能解耦分开维护。
那我们来看一下 initMixinVue 扩展的 _init 方法中到底做了什么操作。
Vue源码窥探之new Vue之后发生了什么?_第3张图片
可以看到,Vue 初始化过程中会 合并配置,初始化生命周期,初始化事件中心,初始化渲染,调用 beforeCreate 生命周期,初始化 Inject/Provide ,初始化state(props 、 methods、data、computed 和 watch),调用 created 生命周期,最后调用 vm.$mount 函数。
该函数顾名思义,肯定是进行绑定挂载的,因为前边的操作已经执行到了 created 了。而 $mount 方法在 src/platform/web/entry-runtime-with-compiler.js 中定义。
Vue源码窥探之new Vue之后发生了什么?_第4张图片
这里先把之前原型上的 mount 缓存起来,然后这里定义的 mount 方法是针对 Runtime + Compiler 版本,所以这里是有一个编译的过程,也就是把 template 或者 el 编译为 render function 。可以看到这个过程是通过 compileToFunctions 来实现。然后拿到 render function 之后,再重新调用原型上之前的 $mount 进行挂载。而最开始在原型上挂载的 $mount 相当于不管是 Runtime + Compiler 还是 Runtime Only 都可以共用,其在 src/platforms/web/runtime/index.js 中定义。
Vue源码窥探之new Vue之后发生了什么?_第5张图片
该方法比较简单,通过 el 来找到对应的 DOM 元素。最终把需要绑定的 DOM 传入 mountComponent 中调用。我们接着看 mountComponent 中发生了什么?
Vue源码窥探之new Vue之后发生了什么?_第6张图片
我们可以看到 mountComponent 的核心就是实例化了一个 Watcher , 在其回调函数中调用 updateComponent 方法,而该方法中 vm._render 先生成虚拟 Node 当作参数,最终调用 vm._update 来更新 DOM 。而该渲染 Watcher 会在初始化和响应式数据发生变化时执行。所以说 Vuemount(挂载) 就做了俩事儿: vm._rendervm._update
那我们先来看看这个 vm._render ,它的返回值是作为参数传入 vm._update 中的。
Vue源码窥探之new Vue之后发生了什么?_第7张图片
我们可以看到,该函数的最终返回值是一个 vnode 。其中核心逻辑就是:vnode = render.call(vm._renderProxy, vm.$createElement) ,这就相当于执行 render function 。而 render function 中核心就是 createElement

举个例子:

{{ message }}

其实也相当于:

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

而这个 createElement 是通过 vnode = render.call(vm._renderProxy, vm.$createElement) 来传入,而 vm.$createElement 是在 initRender 时定义的。
Vue源码窥探之new Vue之后发生了什么?_第8张图片
我们可以看到 vm.$createElement 通过 createElement 被定义,但是上面 vm._c 也被 createElement 所定义。两者区别就是最后一个参数,vm._c 是来对模板编译而来的 render function 进行处理,而 vm.$createElement 是对用户自定义的 render function 进行处理。总之,都需要调用 createElement。那我们就需要看一下其内部情况,createElementsrc/core/vdom/create-element 中定义:
Vue源码窥探之new Vue之后发生了什么?_第9张图片
对函数进行了一层封装,支持更灵活的参数传入,最终调用 _createElement 方法。
Vue源码窥探之new Vue之后发生了什么?_第10张图片
该方法有两个核心逻辑:

  1. children 中的子节点转化为类型为 VNode 的 Array
  2. 实例化 VNode

在实例化 VNode 的时候需要根据 tag 的不同来分别处理,如果是 string 类型,然后接着判断,如果是内置的节点,就创建一个普通 VNode ,如果是已经注册的组件( components 中注册的),就调用 createComponent 来创建一个组件类型的 VNode ,否则创建一个未知的标签的 VNode。如果 tag 是一个 Component 类型,就直接调用 createComponent 来创建一个组件类型 VNode 。
总体而言,createElement 就是一个创建 VNode 的过程,而每个 VNode 都可能会有 children ,而每个 children 也是一个 VNode,那么就形成了映射到真实 DOM 的 VNode Tree 。

现在我们已经知道了 vm._render 的执行逻辑,下一步我们就来看看怎么把 VNode 渲染成一个真实 DOM 并渲染出来,也就是 vm._update 的执行作用。

这个比较晦涩一些,哎哟,来了哦。

我们先来找到 vm._update 的定义,在 src/core/instance/lifecycle.js
Vue源码窥探之new Vue之后发生了什么?_第11张图片
这段代码写在 lifecycleMixin 中,其在 _init 阶段已经执行过了,所以在原型上扩展了 _update 方法,这个方法因为是作为 Watcher 的回调函数来执行的,所以会在初始化和响应式数据更新时被调用,其内部核心逻辑是 vm.__patch__ 方法。在初始化渲染的场景下,vm.__patch__ 的第一个参数是一个真实DOM,而在更新阶段,第一个参数是一个 VNode。
那我们顺着这条线索,去寻找 vm.__patch__ 的定义,在 src/platforms/web/runtime/index.js 中:

Vue.prototype.__patch__ = inBrowser ? patch : noop

这里根据平台的不同,来执行不同的操作,因为如果是SSR,是没有DOM概念的,那我们来看看 patch 的定义,在 src/platforms/web/runtime/patch.js 中:
Vue源码窥探之new Vue之后发生了什么?_第12张图片
patch 最终调用的是 createPatchFunction,顾名思义,这里肯定返回的是一个 Function,把一些 DOM操作等平台相关的参数作为参数传入,而后在真实调用该函数的时候就不需要传入这些平台相关的参数。我们来看一下 createPatchFunction 都做了什么,在 src/core/vdom/patch.js 中:

这里第一步处理了一些虚拟DOM的钩子函数,而后返回了 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 */)

这里最终生成的 DOM 还是要存在 vm.$el 中,而之前作为参数传入的 vm.$el 是在 mountComponent 中赋值的,初始化渲染的时候,vm.$el 就是一个真实的 DOM对象,具体到我们这个 case 中,vm.$el 就是我们的

,而 vode 对应的是调用 render 函数返回的 VNode,因为 patch 逻辑比较复杂,有很多场景判断,我们这里直接看核心逻辑:
Vue源码窥探之new Vue之后发生了什么?_第13张图片
在我们这个 case 中,oldVnode 是一个 DOM container ,所以 isRealElement 是一个 true,接下来就执行 emptyNodeAt 方法来把真实 DOM 转为 VNode 对象。 其实这一块的处理,是因为之前我们说过,在首次渲染的时候传入的是一个 DOM 对象,更新阶段传入的是一个 VNode 对象,这里把 DOM 对象转为 VNode 对象后我们就可以统一处理了。最终我们调用 createElm 方法。
Vue源码窥探之new Vue之后发生了什么?_第14张图片
这里我们 case 下会进入到 isDef(tag) 中,然后执行 createElement 来创建一个普通DOM,最关键的来了哦,这里执行 createChildren 来先把根节点插入到父元素DOM中
Vue源码窥探之new Vue之后发生了什么?_第15张图片
我们来看一下 createChildren 这个方法到底做了什么? 它遍历了 children ,对每一个调用了 createElm ,相当于是深度优先遍历递归,需要特别注意的是,这里我们为 children 执行的 createElm 的时候,需要把当前 vnode.elm 作为父节点,即这些子节点我们是要挂载到父元素上的。然后我们接着往下执行:
Vue源码窥探之new Vue之后发生了什么?_第16张图片
invokeCreateHooks 方法主要是执行 create 的钩子函数并把 VNode 都 push 到 insertedVnodeQueue 中。然后执行 insert 方法,因为之前我们是先对 children 做递归处理,所以我们的子元素会先执行,会全部挂载到父元素上,最后父元素再去执行 insert 方法,挂载到父元素的父元素上,在我们这个 case 下,父元素是
,父元素的父元素是 。然后我们执行完 createElm,就会回到 patch 方法中继续往下走:
Vue源码窥探之new Vue之后发生了什么?_第17张图片
因为我们已经把新的DOM元素挂载到了真实DOM中了,那么需要把之前的DOM节点删除掉。

最终我们完成了最终的DOM渲染,可以通过下图直观地看出整个运转流程:

Vue源码窥探之new Vue之后发生了什么?_第18张图片

你可能感兴趣的:(js,vue)