Skip to content

new Vue过程总结

创建 Vue 实例时,我们传 eldata 这两个 options 配置,vue 会帮我们把模版渲染到页面上,那么它是首次创建时页面是如何被渲染呢,过程如何?下面是其渲染过程的简要概述:

  1. 首先:我们使用 new Vue 进行 vm 初始化时会调用 Vue 的构造函数,因为 Vue 为方便后续拓展及插件机制的实现时的其他属性的定义及挂载,选择 function 函数定义,而非使用 class 类来定义; 我们在 Vue 的函数初次定义时发现其在构建实例时会调用其实例的_init(options)Vue 的首次渲染过程便是在这其中发生的。

  2. 接着:vm 实例的 _init()方法是在 Vue 实例成员初始化时被定义的,在initMixin(Vue)模块中定义的。

  • _init()方法中,为当前实例设置了 uid 属性,uid 是实例 vm 的唯一标识,它是从 0 开始自增的自然数值。
  • 忽略开发环境的性能检测代码,它接着会给实例设置 isVuetrue,标记当前 vm 是一个 vue 的实例,以便在后续的响应式数据处理过程中不被监视。
  1. 然后,会判断当前实例的初始化参数选项 options 中是否包含设置 vm 为组件 component_isComponent,如果是内置组件的话,通过initInternalComponent(vm, options)来合并初始化选项; 因为我们当前创建的是 Vue 实例而非组件,所以会走合并选项的逻辑 mergeOptions(),并把合并后的选项挂载在 vm.$options上。mergeOptions会把我们Vue的构造函数的配置选项和用户new Vue()传入的options选项进行合并。合并过后的$options={components,directives,filters,_base(Vue 的原始定义), el, data}

  2. 紧接着,会给 Vue 实例 vm 设置 _renderProxy ,环境不一样开发模式不一样渲染的代理不一样,开发环境中的可能采用的是 SSR 的后端渲染方式,所以有自己的 renderProxy,而生产模式下的是前端渲染,这里会直接把 vm 本身设置其成_renderProxy,亦即:vm._renderProxy = vm;

  3. 随后,把 vm 作为备份设置到其vm._self属性上,并初始化一些跟 vm 生命周期相关的变量如$root/$parent/$children/$refs等;初始化 vm 的事件监听,即父组件绑定到当前的组件上的事件;初始化 vm 的编译 render 相关变量,如$slots/$scopedSolts/_c/$createElement/$attrs/$listeners等。触发beforeCreate生命钩子回调;把injects的成员注入 vm 上。初始化 vm 的状态相关变量,如_props/methods/_data/computed/watch等;接着初始化provide的成员;触发created钩子回调。

  4. 之后,判断 Vue 实例的选项$options中是否设置了 el,如果设置了 el,则调用vm.$mount(vm.$options.el)方法;

提示

$mount()方法是在entry-runtime-with-compiler.js入口文件中定义的,它的核心作用是把模版编译成 render 函数。 它先根据 el 获取 el 对象,注意 el 不可以是 bodyhtml,如果是非生产环境则会有相应的错误提示。

  1. 接着获取 vm 上的 options 选项,先判断 options 中是否设置了 render 函数,如果没有,就会获取 options 中的 template,先判断 template 是否存在,存在的话再判断:template 是否是字符串如果是字符串的话,还要判断该 template 字符串的第一个字符是否是 #,如果是#,就会认为是 id 选择器,此时会调用 idToTemplate(template) 把结果赋值给 template,即把 template 作为 id 选择器获取其中的 innerHTML 作为我们的 template 模版;最后判断是否具有 nodeType 这个属性。如果有 nodeType 属性,说明是个 DOM 元素,此时把 template.innerHTML 赋值给 template 作为我们的模版。最后判断 template 既不是字符串也不是 DOM 元素的时候,如果是非生产环境则会有相应的不合法的 template 选项的错误提示;最后返回当前实例。如果 options 中的没有 template. 判断是否有 el,有 el 则获取当前 elouterHTML 作为模版,赋值给 template。如果 el 没有 outerHTML 属性,则此时的 el 可能不是一个 DOM 元素例如文本节点或者注释节点,此时需要创建一个 div 元素,并把 el 克隆一份通过 appendChild 添加到刚创建的 div 元素中来,最后把 div 元素的 innerHTML 作为模版返回赋值给 template

  2. 此时我们的 template 就有了明确的结果,再往下就要对获得的模版字符串内容进行编译了。也就是要把 template 转换成 render 函数和 staticRenderFns 静态节点编译函数,这个 staticRenderFns 主要是在编译阶段进行静态节点的优化,避免了不必要的页面响应式更新的消耗。

  3. render 函数生成之后会把它存储到 options 选项的 render 成员里面。同时编译生成的 staticRenderFns 也存储到 options 选项中。

  4. render 函数有了以后,最终会执行我们的 mount 方法,这里的 mount 方法是在 runtime/index.js 中定义的$mount()方法,我们在entry入口文件中重写了该方法。$mount()方法中会重新获取 el,因为如果我们当前使用的是仅运行时版本的 vue 的话,上面所提到的编译过程是没有执行的。所以这里需要重新获取。接着调用 mountComponent()mountComponent()方法是跟浏览器环境无关的,也是我们 vue 的核心代码。它是在 core/instance/lifecycle.js 文件中定义的。

  5. mountComponent()函数中,先把 el 设置到 vm.$el 上,

  • 首先判断 vm.$options.render函数是否存在,不存在则创建一个空的Vnode节点(也就是一个注释节点)赋值给$options.render。此判断的目的是如果我们当前上运行时环境,并且去我们通过 options 选项传入了 template 模版,此时如果是非生产环境的话会发送一个警告,告诉用户当前使用的是运行时版本,编译器是无效的,应该传入 render 函数或者使用包含编译器的版本。
  1. 接着,触发 beforeMount()声明钩子回调
  2. 然后定义了 updateComponent()方法,再往下就是创建了一个 Watcher 对象,创建 Watcher 对象是把刚才定义的 updateComponent 方法传递进去。所以 updateComponent 方法是在 Watcher 对象中被地调用的。
  3. 接下来,vm._isMounted = true, 并触发了 mounted 生命钩子函数。
  4. 再回到 Watcher,看其中做了哪些事项:Watcher 是在 core/observe/watcher.js 文件中定义的,都是跟响应式相关的。在 Watcher 的构造函数中,接收了几个参数,第一个参数是 vue 的实例 vm,第二个就是我们传入的 updateComponent()方法,第三个是个回调函数,第四个是个配置对象,其中配置了 before 钩子回调函数也就是 before update,最后一个参数是 isRenderWatcher,值为 true,表示当前是个 render Watcher
  5. 当前 Watcher 中我们传入的的第二个参数是 function 类型的,所以它的 getter 会存储我们传入的 updateComponent()方法。当前首次渲染 lazyfalse,所以会立即执行 this.get()方法。
  6. Watcherget 方法中,首先把当前 watcher 通过 pushTarget(this)存入栈中,每个组件都会对应一个 watcherwatcher 会去渲染视图,如果组件有嵌套的话,他会先渲染内部组件,所以它要把我们的父组件对应的 watcher 先保存起来,这就是这里得到 pushTarget(this)的作用。
  7. 接着调用 watchergetter 方法,也就是我们传入的第二个参数 updateComponent()方法。updateComponent 里面会调用 vm._render()和 vm._update()这两个方法,这两个方法执行完毕之后,就会把我们的模版渲染到界面上来。我们此处视图已经渲染完毕。
  8. 接着继续回到 watcher 继续往下执行,直到 Vue._init 执行完毕,new Vue 的执行结束,到此首次渲染过程结束。