重学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过程会递归把最内层子组件优先执行。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!