关于生命周期的源码执行
首先我们先来看一张官网的图:
然后我们来看一下源码里什么时候开始执行各个生命周期的:
1. beforeCreate、created
beforeCreate
和created
钩子在core/instance/init.js
的_init
方法中执行
initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) nitProvide(vm) // resolve provide after data/props callHook(vm, 'created')
这里主要是初始化一些vm的属性,initState
主要为定义的data
属性进行obsever
以及处理一些props
、watch
和computed
:
export function initState (vm: Component) { vm._watchers = [] const 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) } }
2. beforMounted
在执行beforMounted
的钩子的时候,会进行几部判断:
1. 判断存不存在$el
属性
if (vm.$options.el) { vm.$mount(vm.$options.el) }
2. 判断存不存在template
属性:
let template = options.template let template = options.template if (template) { // string if (typeof template === 'string') { // 如果第一个字符是#,则 template = query(id).innerHTML if (template.charAt(0) === '#') { template = idToTemplate(template) } // dom 节点 } else if (template.nodeType) { template = template.innerHTML } else { return this } } else if (el) { template = getOuterHTML(el) }
3. mounted
这一步主要是经过了 render --> VNode --> path
步骤后生成了一个真实的dom
节点,并挂载到el
上:
return function patch () { ... invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } function invokeInsertHook (vnode, queue, initial) { ... for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true // 这里执行 mounted callHook(componentInstance, 'mounted') } }
4. beforeUpdate
当我们执行dom更新之前,且已经经过mounted
。会触发的钩子:
vm._update(vm._render(), hydrating) Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { ... if (vm._isMounted) { callHook(vm, 'beforeUpdate') } ... }
5. updated
这个钩子函数主要是在异步更新队列中执行,也就是nextTick
更新dom后会执行的钩子
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') } } } function flushSchedulerQueue () { ... watcher.run() ... callUpdatedHooks(updatedQueue) }
关于什么是nextTick
?以及Event loop
相关知识,有兴趣可以参考我的这两篇文章:
6. beforeDestroy destroyed
当$destroy
函数被调用时,会首先触发beforeDestroy
钩子:
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
步骤如下:
remove(parent.$children, vm)
从父节点中先移除自己vm._watcher.teardown()
销毁watchersvm._data.__ob__.vmCount--
从数据ob中删除引用vm.__patch__(vm._vnode, null)
调用当前渲染树上的销毁钩子callHook(vm, 'destroyed')
调用destroyed
钩子vm.$off()
销毁事件监听 ...
到这里差不多就执行完了销毁任务,从而触发了destroyed
钩子
一些警告
不要在选项属性或回调上使用箭头函数,比如 created: () => console.log(this.a) 或 vm.$watch('a', newValue => this.myMethod())。因为箭头函数是和父级上下文绑定在一起的,this 不会是如你所预期的 Vue 实例,经常导致
Uncaught TypeError: Cannot read property of undefined
或Uncaught TypeError: this.myMethod is not a function
之类的错误。
我们可以看一下Vue是如何执行生命周期函数的:
export function callHook (vm: Component, hook: string) { 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) } }
比如我们执行beforeCreate
钩子:callHook(vm, 'beforeCreate')
。因为是箭头函数,所以可以先了解箭头函数的几个特性:
- 箭头函数作为函数的一种形式,对于this的处理和普通函数有所区别,其没有自己的this上下文,也就是说通过bind/call/apply函数方法设置this值时无效的,会被忽略
- 因为箭头函数没有自己的this上下文,那么当它作为对象的方法函数运行时,this并不指向这个对象
- 箭头函数的的函数体中出现的this在运行时绑定到最近的作用域上下文对象
- 你可以认为箭头函数的this和调用者无关,只和其定义时所在的上下文相关
说到这里,应该明白了为什么不要在选项属性或回调上使用箭头函数了吧...