Vue源码探秘(十二)(生命周期)
2020/4/23 11:22:22
本文主要是介绍Vue源码探秘(十二)(生命周期),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
引言
回顾我们之前学习的内容,在创建一个 Vue
实例的时候需要经过一系列的初始化过程,比如设置数据监听
、编译模板
、挂载实例到 DOM
、在数据变化时更新 DOM
等。
同时在这个过程中也会运行一些叫做生命周期钩子
的函数,这给了用户在不同阶段添加自己的代码的机会。
下面引用官网的一张图,这张图展示了Vue
实例的生命周期以及在它生命周期的各个阶段分别调用的钩子函数:
除了上图中展示的之外,还有activated
和 deactivated
,这两个是和 keep-alive
相关的函数,会放在 keep-alive
的章节再来详细介绍。
callHook
回顾 _init
函数有这么一段代码:
// src/core/instance/init.js Vue.prototype._init = function(options?: Object) { // ... 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"); // ... }; 复制代码
这里调用了两次 callHook
函数,分别执行了生命周期钩子函数 beforeCreate
和 created
。来看 callHook
函数的定义:
// src/core/instance/lifecycle.js export function callHook(vm: Component, hook: string) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget(); const handlers = vm.$options[hook]; const info = `${hook} hook`; if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { invokeWithErrorHandling(handlers[i], vm, null, vm, info); } } if (vm._hasHookEvent) { vm.$emit("hook:" + hook); } popTarget(); } 复制代码
callHook
函数接收两个参数,一个是 vm
实例,一个是要执行的钩子函数名
。这里通过 vm.$options[hook]
拿到对应的函数数组,然后遍历这个数组调用 invokeWithErrorHandling
函数。 invokeWithErrorHandling
函数定义如下:
export function invokeWithErrorHandling( handler: Function, context: any, args: null | any[], vm: any, info: string ) { let res; try { res = args ? handler.apply(context, args) : handler.call(context); if (res && !res._isVue && isPromise(res) && !res._handled) { res.catch(e => handleError(e, vm, info + ` (Promise/async)`)); // issue #9511 // avoid catch triggering multiple times when nested calls res._handled = true; } } catch (e) { handleError(e, vm, info); } return res; } 复制代码
invokeWithErrorHandling
函数主要逻辑就是执行传入的 handler
函数。在调用 invokeWithErrorHandling
函数的时候传入 vm
作为 context
参数,也就是说生命周期函数的 this
会指向当前实例 vm
。另外这里设置一个标识符 _handled
保证函数只被调用一次,避免递归调用。
了解了生命周期的执行方式后,接下来我们会具体介绍每一个生命周期函数它的调用时机。
beforeCreate & created
beforeCreate
和 created
这两个钩子函数的调用时机前面也提到过了,在执行 _init
函数时被调用:
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"); 复制代码
可以看到,在完成初始化生命周期
、事件
、render
后调用了 beforeCreate
。在调用 beforeCreate
之后才调用 initState
。也就是说在 beforeCreate
函数中是访问不到 data
、props
等属性的,因为这个时候还没有初始化。
而 created
是在初始化 data
、props
后才被调用,因此在 created
中可以访问这些属性。
beforeMount & mounted
beforeMount
和 mounted
这两个的调用时机是什么时候呢?
顾名思义,beforeMount
钩子函数发生在 mount
,也就是 DOM
挂载之前,它的调用时机是在 mountComponent
函数中,定义在 src/core/instance/lifecycle.js
中:
// src/core/instance/lifecycle.js export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... callHook(vm, "beforeMount"); let updateComponent; /* istanbul ignore if */ if (process.env.NODE_ENV !== "production" && config.performance && mark) { updateComponent = () => { const name = vm._name; const id = vm._uid; const startTag = `vue-perf-start:${id}`; const endTag = `vue-perf-end:${id}`; mark(startTag); const vnode = vm._render(); mark(endTag); measure(`vue ${name} render`, startTag, endTag); mark(startTag); vm._update(vnode, hydrating); mark(endTag); measure(`vue ${name} patch`, startTag, endTag); }; } else { updateComponent = () => { 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 new Watcher( vm, updateComponent, noop, { 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 if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, "mounted"); } return vm; } 复制代码
可以看到,在组件挂载前就会调用 beforeMount
函数,然后在执行了一系列挂载操作后,在最后的 if
语句判断这个 vm
是外部 new Vue
的实例还是内部的组件实例
。
组件实例会有一个
$vnode
属性,指向组件的占位符VNode
。
如果是外部实例则执行 mounted
函数。
因此组件实例的 mounted
函数调用时机不在 mountComponent
函数中,那是在什么地方呢?
回顾 patch
函数:
function patch(oldVnode, vnode, hydrating, removeOnly) { // ... invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm; } 复制代码
组件的 VNode
patch 到 DOM
后,会执行 invokeInsertHook
函数,把 insertedVnodeQueue
里保存的钩子函数依次执行一遍,它的定义在 src/core/vdom/patch.js
中:
// src/core/vdom/patch.js function invokeInsertHook(vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue; } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]); } } } 复制代码
该函数会执行 insert
这个钩子函数,对于组件而言,insert
钩子函数的定义在 src/core/vdom/create-component.js
中的 componentVNodeHooks
中:
// src/core/vdom/create-component.js const componentVNodeHooks = { insert(vnode: MountedComponentVNode) { const { context, componentInstance } = vnode; if (!componentInstance._isMounted) { componentInstance._isMounted = true; callHook(componentInstance, "mounted"); } if (vnode.data.keepAlive) { if (context._isMounted) { // vue-router#1212 // During updates, a kept-alive component's child components may // change, so directly walking the tree here may call activated hooks // on incorrect children. Instead we push them into a queue which will // be processed after the whole patch process ended. queueActivatedComponent(componentInstance); } else { activateChildComponent(componentInstance, true /* direct */); } } } }; 复制代码
可以看到,组件的 mounted
就是在这里通过 callHook
调用的。
beforeUpdate & updated
beforeUpdate
和 updated
是和数据更新相关的,数据更新这一部分会在下一章详细讲解。
beforeUpdate
的调用时机在 mountComponent
创建 Watcher
实例时:
// src/core/instance/lifecycle.js export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, "beforeUpdate"); } } }, true /* isRenderWatcher */ ); hydrating = false; // ... } 复制代码
在 Watcher
的参数中有一个对象,对象中有一个 before
函数,这个函数判断如果组件已经 mounted
并且还没有 destroyed
,就调用 callHook
执行 beforeUpdate
。
而 before
函数的执行时机是在 flushSchedulerQueue
函数调用的时候,它被定义在 src/core/observer/scheduler.js
中:
// src/core/observer/scheduler.js function flushSchedulerQueue() { // ... for (index = 0; index < queue.length; index++) { watcher = queue[index]; if (watcher.before) { watcher.before(); } id = watcher.id; has[id] = null; watcher.run(); // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== "production" && has[id] != null) { circular[id] = (circular[id] || 0) + 1; if (circular[id] > MAX_UPDATE_COUNT) { warn( "You may have an infinite update loop " + (watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.`), watcher.vm ); break; } } } // keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice(); const updatedQueue = queue.slice(); resetSchedulerState(); // call component updated and activated hooks callActivatedHooks(activatedQueue); callUpdatedHooks(updatedQueue); // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit("flush"); } } 复制代码
现在我们只需要知道这里的 queue
是一个个 Watcher
,flushSchedulerQueue
函数会遍历 queue
然后执行每一个 Watcher
的 before
方法。
flushSchedulerQueue
函数中还调用了 callUpdatedHooks
函数:
function callUpdatedHooks(queue) { let i = queue.length; while (i--) { const watcher = queue[i]; const vm = watcher.vm; if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) { callHook(vm, "updated"); } } } 复制代码
可以看到 updated
是在这里被调用的。
beforeDestroy & destroyed
beforeDestroy
和 destroyed
都在执行 $destroy
函数时被调用。$destroy
函数是定义在 Vue.prototype
上的一个方法,在 src/core/instance/lifecycle.js
文件中:
// src/core/instance/lifecycle.js Vue.prototype.$destroy = function() { const vm: Component = this; if (vm._isBeingDestroyed) { return; } 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 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 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; } }; 复制代码
可以看到在 $destroy
函数一开始就调用了 beforeDestroy
,然后执行一系列销毁操作后再调用 destroyed
,这些销毁操作会在后面章节再来具体分析。
这里调用了我们之前介绍过的__pacth__
函数,实际上调用__pacth__
函数后会触发子组件的 $destroy
函数,然后又执行__pacth__
函数。
也就是说会通过递归调用
按先父后子
的顺序把组件一层一层地销毁掉。因此 beforeDestroy
的调用顺序是先父后子
,因为它会随着递归被调用;而 destroyed
是递归结束后执行,因此执行顺序是先子后父
。
总结
这一小节我们学习了生命周期函数的调用时机以及执行顺序。大概整理一下就是:
created
钩子函数中可以访问到data
、props
等属性mounted
钩子函数中可以访问到DOM
destroyed
函数中可以执行定时器销毁工作beforeMount
/beforeDestroy
的执行顺序是先父后子
mounted
/destroyed
的执行顺序是先子后父
这篇关于Vue源码探秘(十二)(生命周期)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南