认识Vue2中的生命周期
- beforeCreate 在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。
- created 在实例创建完成后被立即同步调用。
- beforeMount 在挂载开始之前被调用
- mounted 实例被挂载后调用
- beforeUpdate 在数据发生改变后,DOM 被更新之前被调用。
- updated 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。
- activated 被 keep-alive 缓存的组件激活时调用。
- deactivated 被 keep-alive 缓存的组件失活时调用。
- beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
- destroyed 实例销毁后调用。
如上是官方文档对生命周期的简要描述,那接下来我们就来详细介绍一下每个生命周期调用前实例是怎样的,此时实例上究竟挂载了哪些东西,在该阶段我们能用的属性有哪些
beforeCreate
这是我们组件进入时最先调用的生命周期,在这里我们先介绍一下如何走到这一步调用的,后面就不在赘述了 我们通常会在main.js中引入vue,然后使用如下代码;
import vue from "vue";
new Vue({
options//传入的参数
}).$mount("#app");
首先,我们来看看导入的Vue是什么
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
如上代码,我们导入的是一个构造函数,在我们使用import导入的时候,已经执行了initMixin这几个函数;我们使用new Vue()的时候创建了vue的一个实例;
接下来我们来介绍一下initMixin这几个函数分别都干了什么事
- 第一个initMixin,这个函数只干了一件事,就是在vue原型对象上加了_init方法,就是Vue.prototype._init=function(vue){},后面用到我们在说这个方法具体干了什么;
- 第二个stateMixin,看名字大概就能猜到是初始化状态之类的,我们来看看吧,这个方法初始化了我们常用的data,set,data',{get:function(){return this._data}}) Object.defineProperty(Vue.prototype, '$props', {get:function(){return this._props}}) 这也是为什么我们在模版中使用data和props中的数据时可以直接用而不用写上data.xxx;
- 第三个eventsMixin,初始化了事件扩展,我们常用的总线时间once,emit就是在这里添加的;
- lifecycleMixin就是生命周期的扩展了,这里给实例添加了_update,destroy
- renderMixin就初始化了渲染事件,将$nextTick,_render挂载到实例上
回到我们上面,当我们创建vue实例时,在构造函数里面调用了_init函数,现在我们来看看这个函数干了什么;
- 首先给组件加一个_uid属性,然后合并我们传入的属性信息和构造函数中的属性信息放到$options属性上,接着执行下面几个函数
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
我们看到在执行beforeCreate钩子函数前,执行了3个函数,这3个函数分别干了什么;
- initLifecycle,这个函数主要给组件添加了一些属性,并且找到组件的根结点并将组件实例放入根节点的$children数组中; 添加的属性如下所示:
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent //循环向上查找根节点
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
- initEvents和initRender,前者是初始化父组件传递的事件,后者是将创建虚拟节点的方法绑定到实例上
综上,我们可以知道,在beforeCreate中,我们仅仅能拿到组件的实例,这个阶段可以给组件实例添加属性什么的,但是我们没法操作数据以及dom
created
从上文我们可以看到在beforeCreate和created之间也执行了3个函数,我们来看看这三个函数;
-
initInjections(vm),initProvide(vm)这个从名字就可以看到是初始化inject和provide了, inject是注入组件的内容,provide是组件提供给外部使用的内容,这个通常开发框架的时候才会用到,我们这里就不展开讲了;
-
initState(vm),我们重点来看看这里做了什么,如下代码,watcher我们先不管,这个是响应式相关的,接下来我们看到在这里先初始化了props,然后是methods,再是data,computed,watch
vm._watchers = []//给组件添加watcher
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)//添加一些警告,规则之类的
if (opts.methods) initMethods(vm, opts.methods)//将方法挂载到实例上,并给每个方法绑定this
if (opts.data) {
initData(vm)//会在这里判断key有没有在props和methods中重复定义,有重复定义非生产环境会报警告
} else {
observe(vm._data = {}, true /* asRootData */)//如果没有data,则侦听一个空对象{}
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)//对watch中的函数添加监听
}
通过上面的解读及代码注释我们很明确知道在调用created时可以使用data,props,methods,computed中定义的数据,包括通过inject注入的数据也是可以用的
beforeMount
在上面的this._init()函数中最后执行了
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
在我们的main.js中我们通常会这样写
new Vue({el:"#app",...});
//或者
new Vue().$mount("#app");
可以看到这两种写法最终执行的都是vue.$mount("#app")方法,所以是等效的 那接下来我们就来看看mount方法吧
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
export function query (el: string | Element): Element {
if (typeof el === 'string') {
const selected = document.querySelector(el)
if (!selected) {
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + el
)
return document.createElement('div')
}
return selected
} else {
return el
}
}
- 上面代码主要是先找到el对应的dom对象,如果没有则报警告;我们主要来看mountComponent,这个方法我放个图吧,主要太长了,把不重要的折叠起来了,折叠的直接跳过去,不影响我们理解代码
如上图,我们在这里调用beforeMount,可以看到这边对比created时期仅仅只是获取了一下dom对象,在这里其实是可以拿到dom对象的
mounted
接下来我们来仔细看上张图中的折叠部分
我们主要看vm._render()和vm._update()方法,还记得我们前面说到的在初始化的时候给实例挂载的这两个方法吗,忘了的往前翻一下哈;
现在我们来看看这两个方法具体的实现逻辑:

- 先来看_render方法,这个方法的主要作用就是将真实节点转化为虚拟节点,具体怎么转换我们这边就先不讲了,简单来说就是将文本解析成对象;

- 接下来我们来看_update方法,这里判断有没有prevVnode,如果没有执行挂载,有则执行更新;也就是说这个方法执行完后,我们的挂载就完成了,所以接下来我们就可以调用mounted钩子函数了;
这边我们可以看到在调用mounted之前有个判断if(vm.$vnode==null),也就是说我们的mounted钩子函数只有在第一次挂载的时候才会调用,之后的更新就不会再执行这个方法了;
beforeUpdate
在上面讲mountComponent方法时,我们看到截图中有一个new Watcher,我们在这里创建了一个watcher实例,这个watcher实例就是用来收集我们的数据依赖的,怎么个收集法呢,简单来说就是创建一个deps对象,我们数据在使用的时候就会自动调用数据的get方法,我们在这个get方法里面把使用这个数据的方法添加到这个deps里面,这样的话,当数据更改了,调用数据的set方法时我们就吧deps中添加的所有方法都拿出来执行一遍,这样就实现了数据的双向绑定了;beforeCreate具体调用在下面截图中
在我们将deps中的对象拿出来执行前,这个时候就调用了beforeCreate钩子函数,所以这个时候我们的数据是已经是新的了,但是所有的依赖项还没有执行更新
updated
上文说到当数据更新了我们会取出所有的依赖方法执行更新,vue在这里做了一个优化,我们在执行update()方法时,会调用queueWatcher(this)方法,这边会判断watcher.id在不在队列中,如果已经在的话就不添加到更新队列,如果是生产环境我们会调用nextTick(flushSchedulerQueue),flushSchedulerQueue这个方法是真正执行更新的方法,使用nextTick包装是为了优化在一个事件循环中只执行一次,不重复执行;
这边我们可以看到将watcher一一取出的时候调用了watcher.before(),这个其实就是beforeCreate钩子函数,在我们前面创建Watcher实例的时候传入的;然后我们才执行的watcher.run()方法执行更新,当更新完后我们要重置更新队列,之后调用callUpdatedHooks(updatedQueue)执行updated钩子函数
从代码中我们可以看到,在执行updated钩子函数时所有依赖数据的依赖项已经执行完更新,当然run方法也会调用patch方法更新vnode渲染到界面上
activated和deactivated我们用得比较少,碍于篇幅已经比较长,我们就先不展开讲了
beforeDestroy和destroyed
最开始初始化的时候我们就在实例上挂载了off移除组件所有监听;