new vue 后发生了什么

new vue 后发生了什么_第1张图片

 

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

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

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

确定入口

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

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

举个例子:

{{ message }}
  • 1
  • 2
  • 3

其实也相当于:

render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

  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
在这里插入图片描述
这段代码写在 lifecycleMixin 中,其在 _init 阶段已经执行过了,所以在原型上扩展了 _update 方法,这个方法因为是作为 Watcher 的回调函数来执行的,所以会在初始化和响应式数据更新时被调用,其内部核心逻辑是 vm.__patch__ 方法。在初始化渲染的场景下,vm.__patch__ 的第一个参数是一个真实DOM,而在更新阶段,第一个参数是一个 VNode。
那我们顺着这条线索,去寻找 vm.__patch__ 的定义,在 src/platforms/web/runtime/index.js 中:

Vue.prototype.__patch__ = inBrowser ? patch : noop
  • 1

这里根据平台的不同,来执行不同的操作,因为如果是SSR,是没有DOM概念的,那我们来看看 patch 的定义,在 src/platforms/web/runtime/patch.js 中:
在这里插入图片描述
而 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!'
  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

那我们在 vm._update 中这么调用 patch 方法:

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

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

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

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

在这里插入图片描述

你可能感兴趣的:(vue)