从图中我们可以看到,Vue
实例的生命周期大致可分为4个阶段:
- 初始化阶段:为
Vue
实例上初始化一些属性,事件以及响应式数据; - 模板编译阶段:将模板编译成渲染函数;
- 挂载阶段:将实例挂载到指定的
DOM
上,即将模板渲染到真实DOM
中; - 销毁阶段:将实例自身从父组件中删除,并取消依赖追踪及事件监听器;
一、初始化阶段
1)new Vue()都干了什么?
new Vue()
会执行Vue
类的构造函数,构造函数内部会执行_init
方法,所以new Vue()
所干的事情其实就是_init
方法所干的事情
首先,把Vue
实例赋值给变量vm
,并且把用户传递的options
选项与当前构造函数的options
属性及其父级构造函数的options
属性进行合并,得到一个新的options
选项赋值给$options
属性,并将$options
属性挂载到Vue
实例上,
最后通过 extend(Vue.options.components, builtInComponents)
把一些内置组件扩展到 Vue.options.components
上,Vue
的内置组件目前 有<keep-alive>
、<transition>
和<transition-group>
组件,这也就是为什么我们在其它组件中使用这些组件不需要注册的原因。
通过调用一些初始化函数来为Vue
实例初始化一些属性,事件,响应式数据等
在所有的初始化工作都完成以后,最后,会判断用户是否传入了el
选项,如果传入了则调用$mount
函数进入模板编译与挂载阶段,如果没有传入el
选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount
方法才进入下一个生命周期阶段。
2)初始化函数initLifecycle -> 就是给实例初始化了一些属性,包括以$
开头的供用户使用的外部属性,也包括以_
开头的供内部使用的内部属性。
其主要是给Vue
实例上挂载了一些属性并设置了默认值,值得一提的是挂载$parent
属性和$root
属性
简单来说就是,找父亲,找到了话将其赋值vm.$parent,
那么会把自身(实例)添加到父亲的children数组中。这样就确保了在子组件的$parent
属性上能访问到父组件实例,在父组件的$children
属性上也能访问子组件的实例。
实例的$root
属性表示当前实例的根实例
该函数的逻辑非常简单,就是给实例初始化了一些属性,包括以$
开头的供用户使用的外部属性,也包括以_
开头的供内部使用的内部属性。
3)初始化函数——initEvents -> 初始化实例的事件系统
判断事件是一个浏览器原生事件还是自定义事件
模板编译的最终目的是创建render
函数供挂载的时候调用生成虚拟DOM,那么在挂载阶段, 如果被挂载的节点是一个组件节点,则通过
createComponent
函数创建一个组件vnode.
父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。
实例初始化阶段调用的初始化事件函数initEvents
实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。
4)初始化函数——initInjections ->该函数是用来初始化实例中的
inject
选项的
说到inject
选项,那必然离不开provide
选项,这两个选项都是成对出现的,它们的作用是:允许一个祖先组件向其所有子孙后代(给孙子传数据,这也是子组件只能prop收到父组件的区别,这个是给子子孙孙,在这个家族链上的)注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
provide
和 inject
选项绑定的数据不是响应式的。
既然inject
选项和provide
选项都是成对出现的,那为什么在初始化的时候不一起初始化呢?为什么在init
函数中调用initInjections
函数和initProvide
函数之间穿插一个initState
函数呢?
因为我们通常所写data
、props
、watch
、computed
及method
,所以inject
选项接收到注入的值有可能被以上这些数据所使用到,所以在初始化完inject
后需要先初始化这些数据,然后才能再初始化provide
,所以在调用initInjections
函数对inject
初始化完之后需要先调用initState
函数对数据进行初始化,最后再调用initProvide
函数对provide
进行初始化。
5)初始化函数——initState
initState
函数就是用来初始化:Vue
组件中会写一些如props
、data
、methods
、computed
、watch
选项,我们把这些选项称为实例的状态选项,这5个选项中的所有属性最终都会被绑定到实例上,这也就是我们为什么可以使用this.xxx
来访问任意属性。
首先,给实例上新增了一个属性_watchers
,用来存储当前实例中所有的watcher
实例,无论是使用vm.$watch
注册的watcher
实例还是使用watch
选项注册的watcher
实例,都会被保存到该属性中。
Vue
中对数据变化的侦测是使用属性拦截的方式实现的,但是Vue
并不是对所有数据都使用属性拦截的方式侦测变化,这是因为数据越多,数据上所绑定的依赖就会多,从而造成依赖追踪的内存开销就会很大,所以从Vue 2.0
版本起,Vue
不再对所有数据都进行侦测,而是将侦测粒度提高到了组件层面,对每个组件进行侦测,所以在每个组件上新增了vm._watchers
属性,用来存放这个组件内用到的所有状态的依赖,当其中一个状态发生变化时,就会通知到组件,然后由组件内部使用虚拟DOM
进行数据比对,从而降低内存开销,提高性能。
initState
函数的所有逻辑,其实你会发现,在函数内部初始化这5个选项的时候它的顺序是有意安排的,不是毫无章法的。如果你在开发中有注意到我们在data
中可以使用props
,在watch
中可以观察data
和props
,之所以可以这样做,就是因为在初始化的时候遵循了这种顺序,先初始化props
,接着初始化data
,最后初始化watch
。
二、模板编译阶段:
前面我们介绍了生命周期的初始化阶段,我们知道,在初始化阶段各项工作做完之后调用了vm.$mount
方法(看后面解释),该方法的调用标志着初始化阶段的结束和进入下一个阶段,从官方文档给出的生命周期流程图中可以看到,下一个阶段就进入了模板编译阶段,该阶段所做的主要工作是获取到用户传入的模板内容并将其编译成渲染函数。
vue
基于源码构建的有两个版本:
1.runtime only
(一个只包含运行时的版本)
2.runtime + compiler
(一个同时包含编译器和运行时的完整版本)
完整版本:
一个完整的Vue
版本是包含编译器的,我们可以使用template
选项进行模板编写。编译器会自动将template
选项中的模板字符串编译成渲染函数的代码,源码中就是render
函数。如果你需要在客户端编译模板 (比如传入一个字符串给 template
选项,或挂载到一个元素上并以其 DOM
内部的 HTML 作为模板),就需要一个包含编译器的版本。 如下:
只包含运行时版本:
只包含运行时的版本拥有创建Vue
实例、渲染并处理Virtual DOM
等功能,基本上就是除去编译器外的完整代码。该版本的适用场景有两种:
1.我们在选项中通过手写render
函数去定义渲染过程,这个时候并不需要包含编译器的版本便可完整执行。
2.借助vue-loader
这样的编译工具进行编译,当我们利用webpack
进行Vue
的工程化开发时,常常会利用vue-loader
对*.vue
文件进行编译,尽管我们也是利用template
模板标签去书写代码,但是此时的Vue
已经不需要利用编译器去负责模板的编译工作了,这个过程交给了插件去实现。
编译过程对性能会造成一定的损耗,并且由于加入了编译的流程代码,Vue
代码的总体积也更加庞大(运行时版本相比完整版体积要小大约 30%)。因此在实际开发中,我们需要借助像webpack
的vue-loader
这类工具进行编译,将Vue
对模板的编译阶段合并到webpack
的构建流程中,这样不仅减少了生产环境代码的体积,也大大提高了运行时的性能,一举两得。
总结:
完整版文件名:vue.js
非完整版文件名: vue.runtime.js
如果我们new Vue的时候,完整版的视图是写在HTML里或template选项的。完整版内含compiler,编译器可以把视图上的html转成DOM节点,转成html中的内容,所以体积大,不建议用,是从HTML获得视图。templete是视图的内容,可以直接包含html内容,常直接放在视图层,用在完整版就直接把html显示在视图
非完整版:
非完整版中的html只是字符串,且不能从html中获取视图,但是可以通过webpack内的vue-loader,来调用pomliler,把其转成h()函数,用户下载的js文件体积变小了。
render是非完整版常用的,常把视图写在rander函数中,用h来创造标签,templete的内容放在vue文件中,vue-loader可以把内容被转换成render(h){h(‘div’)},render函数为render:h=>h(demo)。
如render函数中的参数为App.vue。App.vue中就放了一个router-view,用作路由组件切换的容器,每一个组件里面就有自己的template,这就印证了上面说的,template内容放在vue文件中,也就是App.vue中,只不过最后使用vue-loader去解析转为render函数。
注意:这个$mount是挂载阶段,render是编译->编译为虚拟dom
完整版和非完整版两种版本$mount
方法有着不一样的作用:
它们的区别在于在$mount
方法中是否进行了模板编译。在只包含运行时版本的$mount
方法中获取到DOM
元素后直接进入挂载阶段,而在完整版本的$mount
方法中是先将模板进行编译,然后回过头调只包含运行时版本的$mount
方法进入挂载阶段。
综上:这个$mount方法就是编译结束的标志。完整版的$mount是从用户传入的el
选项和template
选项中获取到用户传入的内部或外部模板,然后将获取到的模板编译成渲染函数。然后再调用运行时版本的$mount方法进入挂载阶段。
最后总结:不管是完整版还是运行时版本,完整版是自己用编译器去编译,并且用html作为模板;运行版本则靠vue-loader靠的是webpack去解析,组件(.vue文件)内部的模板在构建时编译成渲染函数,所以是不需要编译的。最终不管完整版是通过自带的编译器还是非完整版靠webpack的插件,目的都会变成渲染函数
三、挂载阶段
模板编译阶段完成之后,接下来就进入了挂载阶段,从官方文档给出的生命周期流程图中可以看到,挂载阶段所做的主要工作是创建Vue
实例并用其替换el
选项对应的DOM
元素,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新。
执行渲染函数vm._render()
得到一份最新的VNode
节点树,然后执行vm._update()
方法对最新的VNode
节点树与上一次渲染的旧VNode
节点树进行对比并更新DOM
节点(即patch
操作),完成一次渲染。
我们将挂载阶段所做的工作分成两部分进行了分析,第一部分是将模板渲染到视图上,第二部分是开启对模板中数据(状态)的监控。两部分工作都完成以后挂载阶段才算真正的完成了。
四、销毁阶段
当调用了vm.$destroy
方法,Vue
实例就进入了销毁阶段,该阶段所做的主要工作是将当前的Vue
实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器。也就是说,当这个阶段完成之后,当前的Vue
实例的整个生命流程就全部走完了