从源码了解Vue生命周期
渣新一枚,近期打算离职,去好点的城市发展,然后发现写了不少业务代码,真正有用的又一问三不知。 虽然一直都在用Vue,但总觉得好像只是背了背文档,成了一个Api调用师。基于职业发展焦虑, 打算好好学习学习,把看到的东西总结下,方便以后复习,也分享给大家,有错还请指正,写的不好望海涵。
生命周期概览[2.x]
- beforeCreate // 调用该生命周期前已初始化生命周期,事件和渲染函数,不能访问到props等属性
- created // 调用该生命周期前已顺序初始化具体的数据—— injections => props => methods => data => computed => watch => initProvide
- beforeMount // 调用该生命周期前已初始化渲染函数$options.render
- mounted // 调用该生命周期前已渲染真实节点
- beforeUpdate // 状态改变时,会在nextTick中更新视图前调用
- updated // 已调用render函数重新渲染
- activated // keep-alive缓存组件渲染时调用 [首次加载时在 mounted 之后]
- deactivated // keep-alive缓存组件销毁后调用
- beforeDestroy // 实例销毁前调用
- destroyed // 实例销毁后调用
- errorCaptured
我们再来看看源码中是如何一步一步创建一个Vue实例的
new Vue(options)
new Vue(options)显示创建一个Vue实例,传入实例配置属性options,我们就来看看Vue构造函数做了什么?
function Vue ( options ) {
if ( !( this instanceof Vue ) ) {
warn( 'Vue is a constructor and should be called with the `new` keyword' );
}
this._init( options );
}
Vue构造函数就只做了一件事,调用内部方法_init() , _init是混入Vue原型对象上的一个方法,我们继续往下看。
Vue.prototype._init
_init简要代码如下(已省略于本文无关代码),记住_init里调用的方法也就大概能记住Vue实例化的过程了。这一步可以多看看
Vue.prototype._init = function ( options ) {
var vm = this;
/////////////////////////////////////
// 根据合并策略完成配置选项合并 //
////////////////////////////////////
vm.$options = mergeOptions(
resolveConstructorOptions( vm.constructor ),
options || {},
vm
);
vm._self = vm;
initLifecycle( vm ); // 初始化$parent, $root, 生命周期相关属性
initEvents( vm ); // 挂载父组件传入的事件监听
initRender( vm ); // 挂载$createElement, $attrs, $listener
callHook( vm, 'beforeCreate' );
initInjections( vm ); // 初始化 injections [在data 和 props 之前]
initState( vm ); // 初始化 props methods data computed watch 属性
initProvide( vm ); // 初始化 provide [在data 和 props 之后]
callHook( vm, 'created' );
if ( vm.$options.el ) { // 存在el时 主动挂载 否则 手动使用$mount
vm.$mount( vm.$options.el );
}
}
_init函数中,mergeOptions会将传入的配置和诸如Mixin配置按照默认或用户自定义的合并策略进行合并。
配置项合并完成后,就可以开始初始化组件数据了。
initLifecycle
function initLifecycle ( vm ) {
var options = vm.$options;
// locate first non-abstract parent
var 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;
}
initLifecycle中首先循环查找当前组件的父级(非抽象),并挂在到$parent属性上,然后初始化了其他的一些属性的默认值。
说明beforeCreate此时已经可以访问$parent和$root
initEvents
function initEvents ( vm ) {
vm._events = Object.create( null );
vm._hasHookEvent = false;
// init parent attached events
var listeners = vm.$options._parentListeners;
if ( listeners ) {
updateComponentListeners( vm, listeners );
}
}
initEvents也不复杂,初始化内部属性并获取父组件注册到该组件上的事件并初始化(updateComponentListeners)。
其中_parentListeners是父组件传入的事件监听,_parentListeners存在时将会在子组件内进行事件注册。
「这里需要注意的是,initEvents初始化的是父组件传入的事件监听。」
initRender
function initRender ( vm ) {
var options = vm.$options;
var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree
var renderContext = parentVnode && parentVnode.context;
vm.$slots = resolveSlots( options._renderChildren, renderContext );
vm.$scopedSlots = emptyObject;
vm._c = function ( a, b, c, d ) { return createElement( vm, a, b, c, d, false ); };
vm.$createElement = function ( a, b, c, d ) { return createElement( vm, a, b, c, d, true ); };
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
var parentData = parentVnode && parentVnode.data;
{
defineReactive$1( vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {
!isUpdatingChildComponent && warn( "$attrs is readonly.", vm );
}, true );
defineReactive$1( vm, '$listeners', options._parentListeners || emptyObject, function () {
!isUpdatingChildComponent && warn( "$listeners is readonly.", vm );
}, true );
}
}
这里可以看到,initRender初始化一些属性,其中主要的函数$createElement已经能在实例中访问。代码的最后几行中,
还将父组件的attrs和listeners(都做了响应式处理)挂载到了子组件实例中,主要是为了高阶组件的使用。
callHook( vm, 'beforeCreate' )
在调用beforeCreate前,vue实例已经能访问$parent, $root, $listeners, $attrs, $createElement, $slots属性了。
这些属性大部分都是从父组件中传入的。
initInjections(vm)
function initInjections ( vm ) {
var result = resolveInject( vm.$options.inject, vm );
if ( result ) {
toggleObserving( false );
Object.keys( result ).forEach( function ( key ) {
/* istanbul ignore else */
{
defineReactive$1( vm, key, result[key], function () {
// ...
} );
}
} );
toggleObserving( true );
}
}
initInjections仅仅对inject选项做处理,resolveInject函数内部会层层遍历父节点,查找所有注入的属性并将inject相关属性转换成键值对的形式。
拿到键值对后,再逐一将这些注入属性挂载到当前实例下。
需要注意的是,挂载前执行了toggleObserving函数,传入false时,后续绑定的属性将不会主动设置为响应式,也就是说,inject属性通常都并非响应的(除非它本身就是响应式)。
inject初始化后再恢复响应式绑定 => toggleObserving( true )。
initState
function initState ( vm ) {
vm._watchers = [];
var opts = vm.$options;
if ( opts.props ) { initProps( vm, opts.props ); }
if ( opts.methods ) { initMethods( vm, opts.methods ); }
if ( opts.data ) {
initData( vm );
} else {
observe( vm._data = {}, true /* asRootData */ );
}
if ( opts.computed ) { initComputed( vm, opts.computed ); }
if ( opts.watch && opts.watch !== nativeWatch ) {
initWatch( vm, opts.watch );
}
}
initState(在created前)中初始化了许多当前实例的属性props methods data computed watch。created钩子中自然能够访问这些属性了。
值得一提的是props是首个完成初始化的,所以我们在data函数中能够访问到props
initProvide
function initProvide ( vm ) {
var provide = vm.$options.provide;
if ( provide ) {
vm._provided = typeof provide === 'function'
? provide.call( vm )
: provide;
}
}
initProvide只做了一件事,将组件提供的Provide选项保存到私有属性_provided中。
这里我们而可能会有疑问,injection 和 provide 为什么没有成双成对的出现,而在其中穿插了initState呢?
injection 属性是从父组件注入的,data, computed等属性很有可能会引用,自然放在initState之前。
provide是将自身属性提供给子组件,也就需要data属性都初始化完成后才能执行。
这也印证了「父->子->孙」的关系
callHook( vm, 'created' )
至此,我们已经能访问绝大部分的组件属性了,如data, props, methods, inject 等
vm.$mount
_init函数最后一步就是挂载实例到DOM中,用到的就是$mount函数。
$mount涉及到的代码可能有些多,篇幅问题,我们就看看主要步骤。
Vue.prototype.$mount = function (el, hydrating) {
if ( !options.render ) {
var template = options.template;
if ( template ) {
// 对template模板进行校验处理
} else if ( el ) {
template = getOuterHTML( el );
}
if ( template ) {
var ref = compileToFunctions( template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this );
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
}
}
return mount.call( this, el, hydrating )
};
$mount根据手写render函数或template选项和el选项决定如何渲染挂在实例。
其中比较主要的就是compileToFunctions函数。
我们看到该函数传入了template并返回了render,staticRenderFns等函数。
很容易猜到Vue在这里对模板进行了解析。返回的render也就是我们常说的渲染函数。
模板转换为渲染函数有三个步骤:
- 模板解析 template => AST
- 优化 => 标记静态节点
- 转化为渲染函数
有渲染函数后,我们就能挂在到真实节点上了mount.call( this, el, hydrating )。
mount
mount function ( el, hydrating ) {
el = el && inBrowser ? query( el ) : undefined;
return mountComponent( this, el, hydrating )
};
function mountComponent ( vm, el, hydrating ) {
vm.$el = el;
if ( !vm.$options.render ) {
vm.$options.render = createEmptyVNode;
}
callHook( vm, 'beforeMount' );
...
}
mountComponent 函数中,确保了render函数存在。紧随其后调用了beforeMount生命周期。
我们回顾以下created和beforeMount,它们之间进行了模板编译,优化,转换为渲染函数。
// 紧接刚才的代码
var updateComponent;
updateComponent = function () {
vm._update( vm._render(), hydrating );
};
new Watcher( vm, updateComponent, noop, {
before: function before () {
if ( vm._isMounted && !vm._isDestroyed ) {
callHook( vm, 'beforeUpdate' );
}
}
}, true /* isRenderWatcher */ );
hydrating = false;
if ( vm.$vnode == null ) {
vm._isMounted = true;
callHook( vm, 'mounted' );
}
return vm
这里主要是初始化了Watcher实例,实例中传入了updateComponent函数。
该函数后续会对比虚拟Dom并更新视图。初始化了Watcher时就会执行一次updateComponent。
然后callHook( vm, 'mounted' )。
所以mounted阶段已经能访问真实DOM了。
beforeUpdate updated
new Watcher( vm, updateComponent, noop, {
before: function before () {
if ( vm._isMounted && !vm._isDestroyed ) {
callHook( vm, 'beforeUpdate' );
}
}
}, true /* isRenderWatcher */ );
new Watche 的before中调用了beforeUpdate,顺着before,我找到了flushSchedulerQueue函数。
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
...
// do not cache length because more watchers might be pushed
// as we run existing watchers
for ( index = 0; index < queue.length; index++ ) {
watcher = queue[index];
if ( watcher.before ) {
watcher.before();
}
...
watcher.run();
...
}
...
// call component updated and activated hooks
callActivatedHooks( activatedQueue );
callUpdatedHooks( updatedQueue );
...
}
当需要更新依赖/状态时($forceUpdate, 数据读取变动),flushSchedulerQueue就会被触发。
flushSchedulerQueue先遍历了watcher,调用了before(也就是生命周期beforeUpdate),
然后执行callActivatedHook函数,该函数调用其「子组件」的activated钩子,
最后再调用callUpdatedHooks也就是updated钩子。
activated
来看看activated, keep-alive缓存的组件激活时调用
componentVNodeHooks: {
insert: function insert ( vnode ) {
var context = vnode.context;
var componentInstance = vnode.componentInstance;
if ( !componentInstance._isMounted ) {
componentInstance._isMounted = true;
callHook( componentInstance, 'mounted' );
}
if ( vnode.data.keepAlive ) {
if ( context._isMounted ) {
queueActivatedComponent( componentInstance );
} else {
activateChildComponent( componentInstance, true /* direct */ );
}
}
},
...
}
以上代码可以看到,mounted调用后会判断是否为缓存组件并调用activated。
所以「首次加载」时activated钩子是在mounted之后调用的。
一般组件失去活性(切换页面等)会直接$destory, keep-alive组件却调用
beforeDestroy destoryed deactivated
componentVNodeHooks: {
destroy: function destroy ( vnode ) {
var componentInstance = vnode.componentInstance;
if ( !componentInstance._isDestroyed ) {
if ( !vnode.data.keepAlive ) {
componentInstance.$destroy();
} else {
deactivateChildComponent( componentInstance, true /* direct */ );
}
}
}
}
对虚拟DOM(VNode)的对比中,如果判定该组件不存在,普通组件将会调用$destroy(),完全销毁示例,
keep-alive组件却会缓存起来, 下一次添加该实例时,keep-alive组件的_isMounted属性仍为true, 就会跳过$mount阶段,直接调用activateChildComponent进入激活态。
「非首次加载时,keep-alive缓存的组件会跳过mounted直接调用activated钩子」
好记性不如烂笔头,看了很快就忘了,还是记一记能有个好的思路,也方便复习。
本文使用 mdnice 排版