重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
本篇以上图为例,过一下Vue的生命周期函数,代码在 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] if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { try { handlers[i].call(vm) } catch (e) { handleError(e, vm, `${hook} hook`) } } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook) } popTarget() }
传进来两个参数:组件类型的 vm
实例 和 钩子函数字符串名称 hook
,在上篇的合并配置里面有提到最终合并出来的 vm.$options
的 hook
是一个数组(比如上篇的created),所以这里的 handlers
就是一个数组,数组里每个元素都是一个函数,然后通过 call
把当前上下文vm传入到 handlers
里面,从而页面中的 this
就指向了当前vue的实例。下面来看下都哪些地方执行了这个 callHook
。
beforeCreate 和 created
首先是init初始化过程:
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
,一个是 beforeCreated
,一个是 created
,这两个的区别就在于执行 beforeCreated
的时候,只进行了初始化生命周期,事件和渲染,此时还拿不到数据,而 created
是在初始化注入,数据和provider之后走的,所以可以拿到数据。
beforeMount 和 mounted
之前提到在执行挂载的时候会执行一个 mountComponent
方法,它里面有 vm._update(vm._render(),hydrating)
和 Watcher
等等,再来看下这个方法:
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // ... 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) { 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 }
它在执行 vm._render()
渲染VNode之前,执行了一个 beforeMount
钩子,在执行完 new Watcher
里有个逻辑,如果 vm.$vnode
是空,也就是当前vm没有父节点,也就意味着当前节点就是根节点,就执行一次 mounted
,这个是根组件执行的,那在子组件创建的时候,内部也有patch过程,这个过程在最后调用了一个 invokeInsertHook()
:
export function createPatchFunction (backend) { // ... return function patch (oldVnode, vnode, hydrating, removeOnly) { // ... invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }
看下它的定义:
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
定义在 createVNodeHooks
里面,也就是之前文章中说到的创建子组件过程中的安装组件钩子那一步:
const componentVNodeHooks = { // ... insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } // ... }, }
这里可以看到如果子组件没有自定义 mounted
,就把子组件 call
一下,加上 mounted
钩子,也就是说每一个子组件都是在这个 insert
执行了 mounted
,而且之前分析过子组件的patch过程,先init,然后如果返回的组件根vnode是一个组件,就重复执行patch的init,在这个过程中上面提到的 queue
也是在不断的往里面添加钩子,而子组件的vnode总是优先插到队列里,所以在全部的patch结束,去调用钩子函数的时候,子组件的 insert
钩子就会先于父组件执行,也就是说子组件的 mounted
会先执行,父组件的 mounted
会后执行。
对于 beforeMount
而言,则是父优先于子,因为父组件的 mountComponent
会优先于子的执行,然后才会执行子组件的patch,里面会调用子组件的初始化,而子组件初始化的时候又会调用子组件的 mountComponent
,所以是先父后子。
beforeUpdate 和 updated
在组件执行 mountComponent
的时候会执行一个 new Watcher
,里面有一个 before
函数它调用了 beforeUpdate
钩子:
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) // ... }
这里有个判断,只有组件在执行 mounted
之后,才会调用 beforeUpdate
钩子,也就是说 beforeUpdate
和 updated
都是在 mounted
之后才会执行的。
update
的执行是在 flushSchedulerQueue
函数中,它的定义在 src/core/observer/scheduler.js
:
function flushSchedulerQueue () { // ... // 获取到 updatedQueue callUpdatedHooks(updatedQueue) } function callUpdatedHooks (queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted) { callHook(vm, 'updated') } } }
flushSchedulerQueue
的 updatedQueue
参数是更新了的 watcher
数组,然后在调用 callUpdatedHooks
的时候,做了一个判断,只有当前的 watcher
是 vm._watcher
并且组件已经执行完毕 mounted
,也就是mounted过了并且数据发生变化了,才会执行 update
钩子。而 vm._watcher === watcher
这个判断是从哪里来的?
前面提到组件 mounted
过程中,会实例化一个渲染 Watcher
去监听 vm
上的数据变化:
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... // 这里是简写 let updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) // ... }
而在实例化这个渲染 Watcher
过程中,会判断一个 isRenderWatcher
,Watcher
的构造函数如下,定义在 src/core/observer/watcher.js
中:
export default class Watcher { // ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // ... } }
这里的 vm._watcher
是专门用来监听 vm
上数据变化然后重新渲染的,所以 vm._watcher
是一个渲染 watcher
,接着判断如果当前 watcher
是一个渲染 watcher
,那就把这个渲染 watcher
赋值给 vm._watcher
,并且把它,也就是当前的 watcher
实例push到 vm._watchers
中,所以在 callUpdatedHooks
里的 vm._watcher === watcher
的意思就是当前 watcher
是一个渲染 watcher
的时候并且mounted之后,这个渲染 watcher
才会执行 updated
钩子。
vm._isMounted
是定义在 insert
里:
insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } // ... }
首次渲染的话, componentInstance._isMounted
是false,所以只会执行一个 mounted
,当后面重新渲染的时候,这个 _isMounted
就是true了,就会跳过这里,去执行 updated
。(重新渲染在响应式原理文章里会提到)
beforeDestroy 和 destroyed
这两个钩子执行是在 $destroy
的时候:
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 } }
这个方法会在组件销毁过程中会执行,从上面的代码可以看出来先执行了 beforeDestroy
,然后进行了一系列的销毁工作,销毁完成之后会执行 destroyed
,这里注意有一个:vm.__patch__(vm._vnode, null)
,第二个参数是null,因为每一个组件都是一个vue实例,当前组件执行 $destroy
的时候,就会递归销毁子组件。
所以对于 beforeDestroy
过程是先父后子,也就是说父组件先执行 $destroy
,然后在patch过程中,走子组件的 $destroy
,这样在执行 destroyed
的时候就是先子后父了,因为patch过程会递归把最内层子组件优先执行。