Vue源码探秘(十二)(生命周期)


导言

回忆咱们之前学习的内容,在创立一个 Vue 实例的时分需求经过一系列的初始化进程,比如设置数据监听编译模板挂载实例到 DOM、在数据变化时更新 DOM 等。

同时在这个进程中也会运转一些叫做生命周期钩子的函数,这给了用户在不同阶段增加自己的代码的机会。

下面引用官网的一张图,这张图展现了Vue 实例的生命周期以及在它生命周期的各个阶段分别调用的钩子函数:
Vue源码探秘(十二)(生命周期)

y w ( { T W 1 {了上图中展现的之外P U Y 6,还有activ4 a [ 2ateddeactivated ,这两个是和 keec ? i V 8 zp-alive 相关的函数,会放在 keep-alive 的章节再来详细介绍。

callHook

回忆 _init 函数有这么一段代码:

//src/core/instance/i { L  u Ynit.js

Vue.prototype._init=function(options?:Object){
//...

initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm," r B bbeforeCreate");
initInjections(vm);//resolveinjectionsbeforedas } 6 Y & 1 :ta/props
initState(vm);
initProvN D I B hide(vm);//resolveprovideafterdata/props
callHook(vm,"created f e ) ? J * _ yd");

//...
};

这儿调用了两次 callHook 函数,分别履行了生命周期钩子函数 beforeCreatecreated 。来看 callHoX d 4ok 函数的界= l E $说:

//src/core/instance/lifecycle.js

exportfunctioncallHook(vm:Component,hook:string){
//#7573disabledepcollectionwheninvokinglifecyclehooks
pushTarP U z ?get();
c Z G } y Z jonsthandlers=vm.$options[hook];
constinfo=`${hook}hook`;
if(handlers){
for(leti=0,j=handlers.length;i&g [ = | r J U ) {lt;j;i++){
invokeWithErrorHandling(handlers[i],vm,null,vm,info);
}
}
if(vm._hasHookEvent){
v) D : , v N p rm.$emit("hook:"+hook);
}
popTarget();
}

callHook 函数接纳两个参数,一个是 vm 实例,一个是要履行的钩子函数名。这儿通过 vm.$options[hook] 拿到对应的函数数组,然后遍历这个数组调用 invokeWithErrorHandling 函数。 invokeWithEt f { { l ` brrorHandling 函数界说如下:

exportfunctix J = G 7 ~oninvokeWithErrorHandling(
handler:Function,
context:any,
args:null|any[],
vm:any,
info:string
)
{
letres;
try{
res=arU P : 6 . @gs?handler.apply(context5 R ~ E ~,args):handler.call(context_ 1 # 4 1 y 0 +);
if(res&&ax ! pmp;!res._f + [ [isVue&&isPromise(res)&K | Lamp;&!res._handled){
res.catch(e=>handleError(e~ u n 7,vm,info+`(Promise/asyncy k x)`));
//issue#9511
//avoidcatchtriggeringR I ! ^ { nmultipletimeswhenneQ } 6 # 5 8 o gstl o [ t A ~ D Dedcalls
res._handled=trk [ a W a X H I *ue;
}
}catch(e){
handleError(e,vm,int d _ A R 9 a qfo);
}
returnres;
}

invokeWithErrorHandlin7 U .g 函数主要逻辑便是履行传入的 hand4 H D L / ; 3 v rler 函数。在调用 invokeO m O kWithErrorHandlM 9 } p =ing 函数的时分传入 vm 作为 context 参数,也便是说生命周期函数的 this 会指向当时实例 vm 。另外这儿设置一个标c $ T s识符 _handled 保证函数只被调用一次,避免递归调用。

了解了C D q 3 ! 6 s V生命G 4 : #周期的履行办法后,接下来咱们会A 6 $ P Q 5 f ! k详细介绍每一个生命周期函数它的调用机遇。

beforeCreate & created

beforeCreatecreated 这两个钩子函数的调用机遇前面也说到过了,在履行 _init 函数时被调用:

initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm,"beforeCreate");
initInjections(vm);//resolveinjectiy j K #onsbeforedata/props
initState(vm);
initProvide(vm);//resolveprovideafterdata/props
callHook(vm,"created");

能够看到,] T ~ t . ^ x W在完结初始化生命周期事件render 后调用了 beforeCreate 。在调用 beforeCreate 之后才调用 initState 。也便是说在 beforeCreate 函数中是访问不到 dataprops 等特点的,因为这* ^ 2 . , : n个时分还没有初始化。

createdr I ) 2 y Z O w 是在初始化 dataprops 后才被调用,因而在 created 中能够访问这些特点。

beforeMount & mounted

befor: I p . [ 9 U eMountmounted 这两个的调用机遇是什么时分呢?

顾名思义,beforeMount 钩子函数发生在 mount,也便是 DOM 挂载之前,它的调用机遇是在 mountComponent 函数中,界说在 src/core/instance/lifecycle.js 中:

//src/core/instance/lifecycle.js

exportfunctionmountComponent(
vm:Component,
el:?Element,
hydrating?:boolean
):Component
{
//...
callHook(vm,"beforeMount");

letupdateComponent;
/*istanbulignoreif*/
if(process.env.NODE_ENVe H 3 v & ! 6 _ x!=="production"&&g h A W . x *config.perfo+ 6 d J 1 6rmance&&mark){
updateComponent=()=>{
constname=vm._name;
constid=vm._uid;
cog O ) 5 ) . O A -nststartTag=`vue-perf-start:${id}`;
cons~ m _ | = l f ntendTag=`vue-perf-end:${id}`;

mark(startTag);
constvnode=vm._render();
mark(endTag);
measure(`vue${name}render`,startTag,endTag);

mark(startTa} ? 9 j yg);
vm._update(: v C x V V f yvnode,hydD 3 5 [rating)- , D ? G;
mark(endTag);
measure(`vue${name}M 5 M S Spatch`,startTag,endTag);
};
}else{
uk l c ; b fpdateCompE - m monent=()=>{
vm._update(vm._render(),hydrating);
}% U X;
}

//wes: % 6 % & g & Hetthistovm._watcherinsidethewatcher'sr ] X tconstructor
/l ; v/sincethewatcher'si* % U a $ nnitialpatchmaycall$forceUpdate(e.g.insidechild
//V 6 _ N _component'smountedh~ b N . ) & 4 uook),whichreliesonvm._watcherbeingg T c f Halreadydefined
newWatcher(
vm,
updateComponent,
noop,
{
before(){
if(vm._isMounted&&!vm._isDestroyed)3 X q n E _ j ! 9{
callHook(vm,"beforeUpda$ m h ` N 5 ;te");
}
}
},
true/*isRenderWa^ Y ^ utcher*/
);
hydrating=false;

//manuallymouv 4 ) P i 8 _ntedinstance,callmountedonself
//mountediscalledfor( 3 d B X @render-createdchildcomponentsinitsinsertedhook
if(vm.$vno # ] W T + J R )ode==null){
vm._isMounted=true;
callHook(vm,"mounted");
}
returnvm;
}

能够看到,在组件挂载前就会调用 beforeMoJ s nunt 函数,然后在履行了一系列挂载操作后,在最终的 if 句子判别这个 vm 是外部 new Vue 的实例仍是内1 ; # C 2 ~部的组件实例

组件实例会m $ F z X有一个 $vnode 特点,指向组件的占位符 VNode

假如是外部实例则履行 mounted 函数。

因而组件实例的 mounted 函数调用机遇不在 mountComponent 函数中,那是在什么地方呢?p ) f

回忆 patch 函数:

functionpatch(oldVnode,vnode,hydrating,removeOnly){
//...

invokeInsertHook(vnode,insertedVnodeQueue,isInitialPatch);
returnvnode.elm;
}

组件的 VNode patch 到 DOM 后,会履行 inV 8 P Y M R M ovokeInsertHook 函数,把 insertedVnodeY : j U L l m /Queue 里保存的钩子函数依次履行一遍,它的界说在 src} 6 _ 7 P E d 3/core/vdom/patch.js 中:

//src/cor} + b C # ^ Y He/vdo , X z & e ^om/patch.js

functioj , @ T M $ f / BninvokeInsertHooG H 8 U ` E (k(vnode,queue,initial){
//delayins9 v g / t s ? derthooksf` ) ` VorcomponentroU ^ w % gotnodes,invokethemafterthe
//elementis1 a mreallyinE q | n +serted
if(isTrue(initial)&&isDef(vnod^ ~ 1 ge.parent)){
vnode.parent.data.G e A 4 kpendingInsert=queue;
}else{
for(leti=0;i<queue.lengtB + U }h;++i){
queue[i].data.hook.p X , Cinsert(queue[i]);
}
}
}

该函数会履行 insert 这个钩子函数,关于组件而言,inseG w c Y Frt 钩子函数的界说在 srcO j g n ! * L F @/core/vdom/create-component.js 中的 componentVNodeHooks 中:

//src/core/vdom/create-component.js

constcomponent1 k . T W O J 4VNodeHooks={
insert(vnode_ a : s o J:MountedComponentVNode){
const{context,componentInstance}=vnode;
if(!componentInstance._isMounted)@ d !{
componentInstance._isMoP K A { l R ~ : Iuntea [ E s Y n Z 6d=true;
callHook(componentInstanc~ | O k W . Te,"mounted");
}
if(vnode.data.keepAlive){
if(context._isMounte3 0 1 z 5 i v B Qd){
//vue-rod Z suter#1212
//Duringupdates,akept-alivecomponent'schio 4 t 5 A U }ldcomponentsmay
//change,sodirectlywalkingthetreeheremaycallactivatedhooks
//onincorre: i M ` T %cK # Atchildren.Insteadwepushthemintoaqueuewhichwill
//beprocessedafY o L s )terthewholepatchpro2 ) I w bcessended.
queueActivatedComponent(componentInstance);
}else{
activateChildComponent(componentInstance,true/*direct*/);
}
}
}
};

能够看到,组件的 mounted 便是在e j G这儿通过 callHook 调用的y W d [

beforeUpdate & updated

beforeUpdateupdated? ) K @ v c 3 $ I和数据更新相关的,数据更新这一部分会在下一章@ f e G详细讲解。

beforeUpdate 的调用机遇在 mountComponent 创立 Watcher 实例时:

//src/core/i3 # 2 T fnstance/lifecycle.js

exportfunctionmountC$ G jomponent(
vm:Component,
el:?Element,
hydratinJ k L %g?:boolean
):Component
{
//...

newWatcher(
vm,
update7 ^ D * `Component,
noop,
{
before(){
if(vm._isMot 8 Vunted&&!vm._isDestroyed){
cal+ O J U 7 RlHook(vm,"beforeUpdate");
}
}
},
true/@ G 9 4*isRenderWatcher*/
);
hydrating=false;

//...
}

Watcw _ n . : Q / =her 的参数中有一个目标,目标中有一个 before 函数,这个函数判别假如组件已经 mounted 而且还没有 dj a + Restroyed ,就调用 callHook 履行 beforeUpdate

before 函数的履行机遇是在 flushSchedulerQueue 函数调用的时分,它被界说在 src/core/obseW 4 9 J wrver/scheduler.js+ 2 9 中:

//src/core/observer/scheduler.js

functionflushScheduler? l O I l x bQueue(){
//...

for(index=0;index<queue.lengt$ s D K 0 , a ! Lh;index++){
watcher=queue[index];
if(watcher.before){
watcher.before();
}
id=watcher.id;
has[id]=null;
watcher.r3 3 N # M - !un();
//indG ` - g evbuild,4 E v W O C O checkandstopcircularupdates.
if(process.env.N; . PODE_ENV!B d % : / = Y=="productx ) _ Y v j A q sion"&&has[id]!=null){
circuz K 0 ) Z G D { .lar[id]=(circuj s ` T k Clar[id]||0)+1;
if(circular[id]&gr e + ~ 9t;MAX_UPDATE_COUNT){
w2 $ c | k Q & parn(
"YoT = v p / w x lumayhaveaninfw 9 @ a = g R E Hiniteupdateloop"+
(watcher.user
?`inwatcherwithexpression"${watcher.expression}"`
:j i J C - | Q 0 i`inacomponentrenderfunction.`@ O c),
watcher.vm
);
break;
}
}
}

//kV C & h U deepcopiesofpostqueuesI p z B J n jbeforeresettingstate
constactivatedQueue=activatedChildr8 6 & _ K / = | 0en.slice()B Y $ x;
constupdatedQueue=C { ) 5 O ; u Y @queuep ? u.sliS q z H ^ce();

resetScheh U v 9 & 0 TdulerState();

//callcomponentupdatedandactivatedhooks
callActivatedHooks(act[ k t W 1ivatedQueue);
callUpdatedHooks(updatedQueue);

//devtoolhook
/*istanbuy g k M 5 $lignoreif*/
if(devtools&am@ m ( | k K ap;&config.devtools){
devto{ z 8 U a Lols.emit("flu/ H . - @ * jsh");
}
}

现在咱们只需求知道这儿的 queue 是一个个 WatcherflushSchedulerQueue 函数会遍历 qul + j ]eue 然后履行/ n R l I每一个 Watcherbefore 办法。

flushScheo * r k jdulerQueue 函数中还调用了 callUpdatedHooks 函数:

functioncallUpdatedHooks(queue){
leti=queue.length;
while(i--){
constwatcher=queue[i];
constvm=watcher.vm;
if(vm._watcher===watcher&&vm._isMounted&&M 4 I X , . u;!vm._isDestroyed){
callHook(vm,"updated");
}
}
}

能够看到 upda% b : d E 8 ( X %ted 是在这儿被调用的。

beforeDeb a q / W _ c h lstroy & destroyed

beforeDestroydestroyed 都在履行 $destroy 函数时被调用。$destroy 函数是界说在 Vue.protob + z N . v R c Ytype 上的一个办法,在 srcX ? k/core/instance/lifecycle.js 文件中:

//src/core/instance/lifecycle.js

Vue.prototype.$destroy=function(){
constvm:Component=this8 4 Y f s : M;
if(vm._isBeingDestroyed){
return;
}
callHook(vm,"beforeDestroy");
vm._isBeingDestroyed=true;
//removeselffromparent
constD j H ?parent=vn ` z Bm.$parL O y | F l m ( Rent;
if(parent&&!parent._isBeingDestroyed&am@ K c A n Pp;&!vx g y ! H * ^m.$options.abstract)B @ t q @{
remove(parent.$children,vm);
}
//teardownwatchers
if(vm._watcher){
vm._watcher.teardown();
}
leti=vm._watchers.length;
while(i--){
vm._watchers[i].tearH g )down();
}
//removereferencefromdataob
//frb T E + k ; 3 e *ozenobjey _ ~ & 4ctmaynothaveobserver.
if(vm._n B | B ` | N idata.__ob__){
vm._data.__ob__.vmCount--;
}
//callthelasthook...
vm._isDestroyed=true;
//invokedestroyhooksoncurrentrenderedtree
vm.__patch__(vm._vnode,nul: [ c Q s z 1 [ Jl);
//firedestroyedhook
callHook(vm,"destroy- 2 t 2 R 3 b P [ed");
//turnoffallinsta^ S ^ & 5 *ncelisteners.
vm.$off();
//remove__vue__reference
if(vm.$el){
vm.$el.__vue__=null;
}
//releasecircularreference(#? - V F m ) $ t6759)
if(vm.$vnode){
vm.$vnode.parent=null;
}
};

能够看到在 $destroy 函数一开始就调用了 beforeDestY O 2 D + O broy ,然后履行一系列毁掉操作后再调用 destroyed ,这些毁掉操作会在后边章节再来详细分析。

这儿调用了咱们之前介绍X K u e P *过的__pacth__ 函数,实际上调用__pacth__函数后会触发子组件的 $destroy 函数,然后又5 C Y a T O ! 8 i履行__pactJ K C % Dh__ 函数。

也便是说会通过递归调用先父后子的次序把组件一层一层地毁掉掉。因而 beforeDe0 a % K u dstroy 的调用次序是先父后子,因为它会J 3 M L * | {随着递T R n H :归被调用;而 destroyed 是递归完毕后履行,因而履行次序是先子后父

总结

这一末节咱们学习了生命周期函数的调用机遇以及履行次序。大约收拾一下便是:

  • created 钩子函数中能够访问到d% q r y _ ~ } ] Jataprops 等特@ 0 *
  • mounted 钩子函数中能够访问到 DOM
  • destroyed 函数中能够履行定时器毁掉工作
  • beforeMounM 1 ` ; %t / beforeDestro= 4 . !y 的履行次序是先父后子
  • mounted / destroyed 的履行次序是先子后父
    Vue源码探秘(十二)(生命周期)

发表评论

提供最优质的资源集合

立即查看 了解详情