Open jingzhiMo opened 5 years ago
这篇文章会先从最基础的vue组件的生命周期开始阐述,后续结合keep-alive与vue-router来梳理一下平常用到的生命周期hook,加强印象。
keep-alive
vue-router
这是一个老生常谈的问题,有时候回顾一下,会有另外的收获;先引用官方的图:
图片引用地址: https://cn.vuejs.org
vue的生命周期分几类:
图中简单描述了生命周期过程,我们从代码上面看一下初始化的过程
// https://github.com/vuejs/vue/blob/dev/src/core/instance/init.js // 截一段相对关键的代码,加上简单的注释 // @function initMixin vm._self = vm 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') // ... if (vm.$options.el) { vm.$mount(vm.$options.el) }
在beforeCreate之前,主要做了三个动作:initLifeCycle, initEvents, initRender;这三个动作完成之后再执行beforeCreate的hook函数,这三个函数分别做的事情:
beforeCreate
initLifeCycle
initEvents
initRender
TL;DR
vm.$parent
vm.$refs
vm.$root
$slot
$attr
$listener
// 1. initLifecycle (src/core/instance/lifecycle.js) // @function initLifecycle export function initLifecycle (vm: Component) { const options = vm.$options // 建立父子组件的关系 let parent = options.parent if (parent && !options.abstract) { // 对于抽象的组件,不断往上找父组件,找到不是抽象的父组件为止 while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } // balabala 初始化很多数据 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 } // 2. initEvents (src/core/instance/events.js) // @function initEvents export function initEvents (vm: Component) { vm._events = Object.create(null) vm._hasHookEvent = false // init parent attached events // 初始化组件监听的事件 const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) } } // 3. initRender (src/core/instance/render.js) // @function initRender // 中间去掉一些声明变量,主要保留一些赋值到vm的数据 export function initRender (vm: Component) { vm._vnode = null // the root of the child tree vm._staticTrees = null // v-once cached trees //... // 赋值 slot 的值与对应的 slot 对应的数据 vm.$slots = resolveSlots(options._renderChildren, renderContext) vm.$scopedSlots = emptyObject //... // 赋值从组件传过来的属性值与没有显式被组件监听的事件,分别赋值到$attr与$listener /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => { !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm) }, true) defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => { !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm) }, true) } else { defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true) } }
执行完这三个初始化函数,就可以触发beforeCreate的hook函数,可以看到还没有初始化$data的相关数据;在beforeCreate与created之间,执行的函数有:initInjections, initState, initProvide;
$data
created
initInjections
initState
initProvide
data
computed
methods
watcher
_provided
// 1. initInjections (src/core/instance/inject.js) // @function initInjections export function initInjections (vm: Component) { // 拿到注入的数据 const result = resolveInject(vm.$options.inject, vm) if (result) { // 标识inject的属性与方法在当前组件不需要成为 observer,不用监听变化进行响应 toggleObserving(false) Object.keys(result).forEach(key => { // ... // 绑定注入的数据到当前组件 defineReactive(vm, key, result[key]) }) // 把 observer 的标识位置为 true toggleObserving(true) } } // 2. initState (src/core/instance/state.js) // @function initState export function initState (vm: Component) { // 初始化依赖的props,methods vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) // 初始化 data if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } // 初始化计算属性 if (opts.computed) initComputed(vm, opts.computed) // 初始化 watch 的数据 if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } // 3. initProvide (src/core/instance/inject.js) // @function initProvide export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { // 对 provide 是函数的情况,执行函数赋值到 _provided;否则直接赋值 vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
执行完这个三个函数之后,就会触发created的hook函数,这个时候就可以拿到data与methods等数据;现在再回去initMixin函数:
initMixin
// 忽略已分析代码 callHook(vm, 'created') // ... if (vm.$options.el) { vm.$mount(vm.$options.el) }
当有el的元素的时候,就触发$mount方法,否则到后面主动调用方法再触发;这个$mount方法在:src/core/instance/lifecycle.js
el
$mount
src/core/instance/lifecycle.js
// 定义挂载组件的方法 // mountComponent (src/core/instance/lifecycle.js) // @function mountComponent export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 没有 render 函数 if (!vm.$options.render) { vm.$options.render = createEmptyVNode // ... } // 触发 beforeMount hook 函数 callHook(vm, 'beforeMount') // ... // 定义数据发生变化的回调方法 updateComponent = () => { // 调用该方法更新当前的组件,执行完毕之后,需要通过 scheduler 来触发 updated 的 hook,为什么不是马上触发hook,是因为需要保证子组件都更新了,才调用当前组件的 updated,详细可以看一下源码,位置如下 // src/core/instance/lifecycle.js // @function Vue.prototype._update vm._update(vm._render(), hydrating) } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined // 新建一个 watcher,用来监听数据发生变化 // 注意 beforeUpdate 的hook也是在这里进行监听调用 new Watcher(vm, updateComponent, noop, { // 在执行 updateComponent 之前先执行 before 函数,也就是触发 beforeUpdate before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook // 挂载的对象如果不是为空,则触发 mounted 回调方法 if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
_isBeingDestroyed
$el
$vnode
// src/core/instance/lifecycle.js // @function Vue.prototype.$destroy Vue.prototype.$destroy = function () { const vm: Component = this if (vm._isBeingDestroyed) { return } // 触发 beforeDestroy 的 hook callHook(vm, 'beforeDestroy') vm._isBeingDestroyed = true // remove self from parent // 移除父组件与当前组件的关系 const parent = vm.$parent if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) } // teardown watchers // 移除所有watcher if (vm._watcher) { vm._watcher.teardown() } let i = vm._watchers.length while (i--) { vm._watchers[i].teardown() } // remove reference from data ob // frozen object may not have observer. if (vm._data.__ob__) { vm._data.__ob__.vmCount-- } // call the last hook... vm._isDestroyed = true // invoke destroy hooks on current rendered tree // 移除 vnode 内容 vm.__patch__(vm._vnode, null) // fire destroyed hook callHook(vm, 'destroyed') // turn off all instance listeners. vm.$off() // remove __vue__ reference if (vm.$el) { vm.$el.__vue__ = null } // release circular reference (#6759) if (vm.$vnode) { vm.$vnode.parent = null } }
至此,基本的声明周期就差不多了,后续的内容主要是对hook的触发顺序进行一个巩固记忆
当从一个组件a切换到组件b的时候,执行的顺序是:
b-component
a-component
注意从a切换到b的时候,并不是a的destroy的相关方法马上执行,而是等到b组件的beforeMount函数执行后再调用之前的destroy的相关方法;当旧的组件被销毁之后,再执行新的组件的mounted的挂载方法,因为挂载完毕之后就会显示组件对应的内容
destroy
beforeMount
mounted
当使用 keep-alive 来缓存组件的时候,keep-alive里面的生命周期会有点不一样;
<keep-alive> <component-a v-if="componentName === 'component-a'"></component-a> <component-b v-if="componentName === 'component-b'"></component-b> </keep-alive>
当切换不同的componentName变量的时候,在没有使用keep-alive的时候,触发周期如前面所说的;但使用keep-alive之后,一个组件进入的周期就变成了:
componentName
注意这里多了一个activated的hook调用,这一个hook是仅在keep-alive中使用的,表示当前组件被激活;对应这另外一个hook就是,deactivated,表示当前组件被停用,那么从component-a切换到component-b的过程中,生命周期hook调用顺序就变成了:
activated
deactivated
component-a
component-b
component-a初始化:
从component-a切换到:component-b:
可以注意到,这个时候没有了之前看到的destroy类的触发,而是deactivated;mounted之后,也是跟之前类似,也会调用activated方法
然后从component-b再切换到: component-a:
因为这个时候component-a已经初始化了,所以没有触发create与mount类的hook,而是先component-b停用,再component-a激活;后续不断切换也是只反复调用这两个hook...
create
mount
keep-alive可以设置一个最大缓存的数量,当超出设置的最大缓存的数量,则最久没有被访问到的实例会被销毁:
<keep-alive :max="2"> <component-a v-if="componentName === 'component-a'"></component-a> <component-b v-if="componentName === 'component-b'"></component-b> <component-c v-if="componentName === 'component-c'"></component-c> </keep-alive>
从component-a切换到component-b的hook调用顺序与没有设置max类似;再从component-b切换到component-c的时候,调用顺序为:
max
component-c
由于设置了最大的缓存数量为2,当切换到component-c的时候,首先触发的是component-a的destroy的相关方法;再执行初始化component-c,然后component-b失活
component-b失活
componet-c
在vue-router当中,定义了好多hook,称之为导航守卫,现在简单结合一下组件的生命周期梳理一下:
实验例子:
<ul> <li> <router-link :to="{name: 'foo'}">jump to foo</router-link> </li> <li> <router-link :to="{name: 'bar'}">jump to bar</router-link> </li> </ul> <router-view></router-view>
当点击跳转到/foo的时候,foo组件的生命周期与路由钩子触发顺序为:
/foo
foo
需要注意的是,有时候我们在beforeRouteEnter的钩子做一些处理,例如判断用户是否有权限进入该组件,没有权限就跳转去别的页面,有权限则进入页面,伪代码如下:
beforeRouteEnter
import router from 'router' // vue-router object export default { beforeRouteEnter (to, from, next) { console.log('beforeRouteEnter') requestPermission().then(allowAccessed => { if (allowAccessed) { next(vm => { console.log('beforeRouteEnter next') vm.allow = true }) } else { router.push({ name: 'homepage' }) } }) } }
那么这个时候触发的顺序为:
next回调函数是最后才执行;因为在next所传的函数里面,已经可以拿到当前组件的实例
next
ok,回到之前的例子,然后点击从foo跳转到/bar,foo与bar组件的生命周期与路由钩子触发顺序为:
/bar
bar
可以看到先触发foo beforeRouteLeave再到bar beforeRouteEnter;而后续3-8点,与之前组件切换类似
foo beforeRouteLeave
bar beforeRouteEnter
实验代码更改为:
<ul> <li> <router-link :to="{name: 'foo'}">jump to foo</router-link> </li> <li> <router-link :to="{name: 'bar'}">jump to bar</router-link> </li> </ul> <keep-alive> <router-view></router-view> </keep-alive>
首次进入/foo路由
进入的顺序没有特别,最后多了一个activated的调用,与之前使用keep-alive类似
然后从/foo进入/bar:
再从/bar进入/foo:
路由的优先级始终是在最高级别,然后再到组件的初始化过程;若组件已经初始化且在缓存当中,则到keep-alive的activated的相关hook
这篇文章会先从最基础的vue组件的生命周期开始阐述,后续结合
keep-alive
与vue-router
来梳理一下平常用到的生命周期hook,加强印象。vue 组件
这是一个老生常谈的问题,有时候回顾一下,会有另外的收获;先引用官方的图:
图片引用地址: https://cn.vuejs.org
vue的生命周期分几类:
整体初始化过程
图中简单描述了生命周期过程,我们从代码上面看一下初始化的过程
beforeCreate 之前
在
beforeCreate
之前,主要做了三个动作:initLifeCycle
,initEvents
,initRender
;这三个动作完成之后再执行beforeCreate
的hook函数,这三个函数分别做的事情:TL;DR
vm.$parent
,vm.$refs
,vm.$root
等$slot
、$attr
、$listener
beforeCreate 到 created
执行完这三个初始化函数,就可以触发
beforeCreate
的hook函数,可以看到还没有初始化$data
的相关数据;在beforeCreate
与created
之间,执行的函数有:initInjections
,initState
,initProvide
;TL;DR
data
,computed
,methods
,watcher
_provided
字段执行完这个三个函数之后,就会触发
created
的hook函数,这个时候就可以拿到data
与methods
等数据;现在再回去initMixin
函数:mount 与 update
当有
el
的元素的时候,就触发$mount
方法,否则到后面主动调用方法再触发;这个$mount
方法在:src/core/instance/lifecycle.js
destroy
TL;DR
_isBeingDestroyed
更改$el
与$vnode
引用至此,基本的声明周期就差不多了,后续的内容主要是对hook的触发顺序进行一个巩固记忆
基础切换组件
当从一个组件a切换到组件b的时候,执行的顺序是:
b-component
beforeCreateb-component
createdb-component
beforeMounta-component
beforeDestroya-component
destroyedb-component
mounted注意从a切换到b的时候,并不是a的
destroy
的相关方法马上执行,而是等到b组件的beforeMount
函数执行后再调用之前的destroy
的相关方法;当旧的组件被销毁之后,再执行新的组件的mounted
的挂载方法,因为挂载完毕之后就会显示组件对应的内容keep-alive
当使用 keep-alive 来缓存组件的时候,keep-alive里面的生命周期会有点不一样;
当切换不同的
componentName
变量的时候,在没有使用keep-alive
的时候,触发周期如前面所说的;但使用keep-alive
之后,一个组件进入的周期就变成了:注意这里多了一个
activated
的hook调用,这一个hook是仅在keep-alive
中使用的,表示当前组件被激活;对应这另外一个hook就是,deactivated
,表示当前组件被停用,那么从component-a
切换到component-b
的过程中,生命周期hook调用顺序就变成了:component-a
初始化:从
component-a
切换到:component-b
:component-b
beforeCreatecomponent-b
createdcomponent-b
beforeMountcomponent-a
deactivatedcomponent-b
mountedcomponent-b
activated可以注意到,这个时候没有了之前看到的
destroy
类的触发,而是deactivated
;mounted
之后,也是跟之前类似,也会调用activated
方法然后从
component-b
再切换到:component-a
:component-b
deactivatedcomponent-a
activated因为这个时候
component-a
已经初始化了,所以没有触发create
与mount
类的hook,而是先component-b
停用,再component-a
激活;后续不断切换也是只反复调用这两个hook...keep-alive的最大缓存数量 max
keep-alive
可以设置一个最大缓存的数量,当超出设置的最大缓存的数量,则最久没有被访问到的实例会被销毁:从
component-a
切换到component-b
的hook调用顺序与没有设置max
类似;再从component-b
切换到component-c
的时候,调用顺序为:component-a
beforeDestroycomponent-a
destroyedcomponent-c
beforeCreatecomponent-c
createdcomponent-c
beforeMountcomponent-b
deactivatedcomponent-c
mountedcomponent-c
activated由于设置了最大的缓存数量为2,当切换到
component-c
的时候,首先触发的是component-a
的destroy
的相关方法;再执行初始化component-c
,然后component-b
失活component-a
=>component-b
: 与没有max一致component-b
=>component-c
:首先component-a
的destroy相关hook被调用,后续的调用顺序是先初始化component-c
,再让component-b失活
component-c
=>component-b
: 仅执行deactivated
与activated
的方法component-b
=>component-a
;首先componet-c
的destroy相关hook被调用,后续hook调用顺序是先初始化component-a
,再让component-b
失活router
在vue-router当中,定义了好多hook,称之为导航守卫,现在简单结合一下组件的生命周期梳理一下:
实验例子:
当点击跳转到
/foo
的时候,foo
组件的生命周期与路由钩子触发顺序为:需要注意的是,有时候我们在
beforeRouteEnter
的钩子做一些处理,例如判断用户是否有权限进入该组件,没有权限就跳转去别的页面,有权限则进入页面,伪代码如下:那么这个时候触发的顺序为:
next
回调函数是最后才执行;因为在next
所传的函数里面,已经可以拿到当前组件的实例ok,回到之前的例子,然后点击从
foo
跳转到/bar
,foo
与bar
组件的生命周期与路由钩子触发顺序为:可以看到先触发
foo beforeRouteLeave
再到bar beforeRouteEnter
;而后续3-8点,与之前组件切换类似keep-alive 包含 router-view
实验代码更改为:
首次进入
/foo
路由进入的顺序没有特别,最后多了一个
activated
的调用,与之前使用keep-alive类似然后从
/foo
进入/bar
:再从
/bar
进入/foo
:路由的优先级始终是在最高级别,然后再到组件的初始化过程;若组件已经初始化且在缓存当中,则到keep-alive的
activated
的相关hook