主题
new Vue过程总结
创建 Vue 实例时,我们传 el 和 data 这两个 options 配置,vue 会帮我们把模版渲染到页面上,那么它是首次创建时页面是如何被渲染呢,过程如何?下面是其渲染过程的简要概述:
首先:我们使用
new Vue进行vm初始化时会调用Vue的构造函数,因为Vue为方便后续拓展及插件机制的实现时的其他属性的定义及挂载,选择function函数定义,而非使用class类来定义; 我们在Vue的函数初次定义时发现其在构建实例时会调用其实例的_init(options),Vue的首次渲染过程便是在这其中发生的。接着:
vm实例的_init()方法是在Vue实例成员初始化时被定义的,在initMixin(Vue)模块中定义的。
- 在
_init()方法中,为当前实例设置了uid属性,uid是实例vm的唯一标识,它是从 0 开始自增的自然数值。 - 忽略开发环境的性能检测代码,它接着会给实例设置
isVue为true,标记当前vm是一个vue的实例,以便在后续的响应式数据处理过程中不被监视。
然后,会判断当前实例的初始化参数选项
options中是否包含设置vm为组件component的_isComponent,如果是内置组件的话,通过initInternalComponent(vm, options)来合并初始化选项; 因为我们当前创建的是Vue实例而非组件,所以会走合并选项的逻辑mergeOptions(),并把合并后的选项挂载在vm.$options上。mergeOptions会把我们Vue的构造函数的配置选项和用户new Vue()传入的options选项进行合并。合并过后的$options={components,directives,filters,_base(Vue 的原始定义), el, data}紧接着,会给
Vue实例vm设置_renderProxy,环境不一样开发模式不一样渲染的代理不一样,开发环境中的可能采用的是SSR的后端渲染方式,所以有自己的renderProxy,而生产模式下的是前端渲染,这里会直接把vm本身设置其成_renderProxy,亦即:vm._renderProxy = vm;随后,把
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钩子回调。之后,判断
Vue实例的选项$options中是否设置了el,如果设置了el,则调用vm.$mount(vm.$options.el)方法;
提示
$mount()方法是在entry-runtime-with-compiler.js入口文件中定义的,它的核心作用是把模版编译成 render 函数。 它先根据 el 获取 el 对象,注意 el 不可以是 body 或 html,如果是非生产环境则会有相应的错误提示。
接着获取
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则获取当前el的outerHTML作为模版,赋值给template。如果el没有outerHTML属性,则此时的el可能不是一个DOM元素例如文本节点或者注释节点,此时需要创建一个div元素,并把el克隆一份通过appendChild添加到刚创建的div元素中来,最后把div元素的innerHTML作为模版返回赋值给template。此时我们的
template就有了明确的结果,再往下就要对获得的模版字符串内容进行编译了。也就是要把template转换成render函数和staticRenderFns静态节点编译函数,这个staticRenderFns主要是在编译阶段进行静态节点的优化,避免了不必要的页面响应式更新的消耗。render函数生成之后会把它存储到options选项的render成员里面。同时编译生成的staticRenderFns也存储到options选项中。render函数有了以后,最终会执行我们的mount方法,这里的mount方法是在runtime/index.js中定义的$mount()方法,我们在entry入口文件中重写了该方法。$mount()方法中会重新获取el,因为如果我们当前使用的是仅运行时版本的vue的话,上面所提到的编译过程是没有执行的。所以这里需要重新获取。接着调用mountComponent(),mountComponent()方法是跟浏览器环境无关的,也是我们vue的核心代码。它是在core/instance/lifecycle.js文件中定义的。在
mountComponent()函数中,先把el设置到vm.$el上,
- 首先判断
vm.$options.render函数是否存在,不存在则创建一个空的Vnode节点(也就是一个注释节点)赋值给$options.render。此判断的目的是如果我们当前上运行时环境,并且去我们通过options选项传入了template模版,此时如果是非生产环境的话会发送一个警告,告诉用户当前使用的是运行时版本,编译器是无效的,应该传入render函数或者使用包含编译器的版本。
- 接着,触发
beforeMount()声明钩子回调 - 然后定义了
updateComponent()方法,再往下就是创建了一个Watcher对象,创建Watcher对象是把刚才定义的updateComponent方法传递进去。所以updateComponent方法是在Watcher对象中被地调用的。 - 接下来,
vm._isMounted = true, 并触发了mounted生命钩子函数。 - 再回到
Watcher,看其中做了哪些事项:Watcher是在core/observe/watcher.js文件中定义的,都是跟响应式相关的。在Watcher的构造函数中,接收了几个参数,第一个参数是vue的实例vm,第二个就是我们传入的updateComponent()方法,第三个是个回调函数,第四个是个配置对象,其中配置了before钩子回调函数也就是beforeupdate,最后一个参数是isRenderWatcher,值为true,表示当前是个render Watcher。 - 当前
Watcher中我们传入的的第二个参数是function类型的,所以它的getter会存储我们传入的updateComponent()方法。当前首次渲染lazy为false,所以会立即执行this.get()方法。 - 在
Watcher的get方法中,首先把当前watcher通过pushTarget(this)存入栈中,每个组件都会对应一个watcher,watcher会去渲染视图,如果组件有嵌套的话,他会先渲染内部组件,所以它要把我们的父组件对应的watcher先保存起来,这就是这里得到pushTarget(this)的作用。 - 接着调用
watcher的getter方法,也就是我们传入的第二个参数updateComponent()方法。updateComponent里面会调用vm._render()和vm._update()这两个方法,这两个方法执行完毕之后,就会把我们的模版渲染到界面上来。我们此处视图已经渲染完毕。 - 接着继续回到
watcher继续往下执行,直到Vue._init执行完毕,new Vue的执行结束,到此首次渲染过程结束。
