学习主线:从vue2生命周期图出发,找出背后的源码实现,来探索vue成长之路!
[TOC]
生命周期图
vue2_lifecycle.png vue2_lifecycle_cn.pngvue2.6.12源码目录结构
src
├── compiler # 编译相关
├── codegen # 根据抽象语法树(AST)生成render函数
├── directives # 通过生成render函数之前需要处理的指令
├── parser # 模板解析,存放将模板字符串转换成元素抽象语法树的代码
├── optimizer.js # 分析静态树,优化vdom渲染
├── core # 核心代码
├── components # 全局的组件,这里只有keep-alive
├── global-api # 全局方法,也就是添加在Vue对象上的方法,如Vue.use,Vue.extend,,Vue.mixin等
├── instance # 实例相关内容,包括实例方法,生命周期,事件等
├── observer # 双向数据绑定相关文件
├── util # 工具方法
├── vdom # 虚拟dom相关
├── platforms # 不同平台的支持
├── web # web端独有文件
├── compiler # 编译阶段需要处理的指令和模块
├── runtime # 运行阶段需要处理的组件、指令和模块
├── server # 服务端渲染相关
├── util # 工具库
├── weex # weex端独有文件
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── parser.js # 单文件 Vue 组件 (*.vue) 的解析逻辑。在 vue-template-compiler 包中被使用
├── shared # 共享代码
new Vue()
每个 Vue 实例在被创建之前都要经过一系列的初始化过程。需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等
Vue源代码目录下的/src/core/instance/index.js的Vue函数。这个函数主要的作用是调用Vue原型链上的_init函数以实现Vue对象的初始化过程
init events & Lifecycle
1.生命周期的初始化 initLifecycle
从创建Vue对象到BeforeCreated过程,其中第一个过程就是生命周期的初始化
在vue初始化的时候会执行initLifecycle,initLifecycle会在beforeCreated钩子触发前调用,是在生命周期开始之前设置一些相关的属性的初始值(源代码目录src/core/instance/lifecycle.js)
export function initLifecycle (vm: Component) {
// 把所有同类钩子先合并成数组,然后存放在 vm.$options
const options = vm.$options
// 变量 parent用于获取此Vue对象的祖宗对象,如果存在祖宗对象在此祖宗对象的子对象数组中添加此节点
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm //此Vue对象的根节点
vm.$children = [] //初始化此Vue对象的子对象为空数组
vm.$refs = {} //初始化此Vue对象的中的元素或者是子组件的注册引用信息为空对象
//初始化设置一些标志位,用于表明是否已经完成某种钩子
vm._watcher = null //初始化Vue对象的监听器为null
vm._inactive = null //初始化此Vue对象的活跃状态为null
vm._directInactive = false //初始化此Vue对象的暂停状态为false
// 生命周期相关的私有属性
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
执行生命周期的函数都是调用 callHook
方法,它的定义在 src/core/instance/lifecycle
中:
// 根据传入的字符串 `hook`,去拿到 `vm.$options[hook]` 对应的回调函数数组,然后遍历执行,执行的时候把 `vm` 作为函数执行的上下文
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
// 各个阶段的生命周期的函数也被合并到 vm.$options
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()
}
2. 事件初始化initEvents
初始化本组件的监听事件对象和Hook事件监听,以及更新父组件的监听器
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
3. 渲染初始化
渲染初始化完成之后便完成了BeforeCreated,使用callhook函数调用beforeCreated函数
export function initRender (vm: Component) {
// 首先初始化虚拟节点为null
vm._vnode = null
// 定义变量options存储Vue对象$options属性
const options = vm.$options
// 定义变量parentVnode同时设置Vue对象的值为options._parentVnode即获取父级的虚拟节点
const parentVnode = vm.$vnode = options._parentVnode
// 定义变量renderContext存储父级虚拟节点的渲染内容
const renderContext = parentVnode && parentVnode.context
// 设置Vue对象的$slots属性用于处理此对象中的具名插槽和你们插槽
vm.$slots = resolveSlots(options._renderChildren, renderContext)
// 设置Vue对象的$scopedSlots属性用于处理此对象中的范围插槽
vm.$scopedSlots = emptyObject
// 设置Vue对象的_c属性其值为createElement函数
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 设置Vue对象的$createElement属性其值为createElement函数
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
const parentData = parentVnode && parentVnode.data
// 给Vue对象的$attrs和$listeners添加setter和getter函数,以及对属性和事件的相关的监听处理
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
至此完成了从创建Vue对象到BeforeCreate的所有过程
beforeCreate
在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用,组件实例刚刚被创建,组件属性计算之前,如data属性, 可以在这加个loading事件
在BeforeCreate期间做了三件事情,初始化生命周期,初始化事件,初始化渲染
初始化生命周期主要是初始化Vue对象的一些过程状态查找父节点,并在父节点注册自己的相关信息。
初始化事件主要是获取父节点的监听的事件,并添加到子节点上。
初始化渲染主要是获取父节点的渲染内容,以及插槽,范围插槽,创建DOM元素函数的定义,继承父节点的listeners属性。
create过程
进入create状态的第二个过程就是状态的初始化,状态的初始化是对于Vue对象的Props,Methods,Data,watch,computed进行初始化,经过这里Vue的一些关键的属性才被初始化可以去使用。
src/core/instance/state.js
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)
}
}
initInjections & reactivity
1.初始化注入 & 校验
inject和provide(src/core/instance/inject.js)
祖先组件在provide中提供后代可使用的数据,后代组件在inject中设置使用祖先组件的属性名。
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
created
在这结束loading,还做一些初始化,实现函数自执行, 组件实例创建完成,属性已绑定,但是DOM还未完成,$el属性还不存在, 已经具有响应式的data,可以发送events。可以在这里去发送请求。
created过程完成将会调用hook调用组件的created函数。表明组件所需要的必备数据准备完成,后续将会进行组件的挂载过程。
Has "el" option?
实例是否含有 el
选项,如果没有指定该选项就不需要进行挂载执行,如果后续要进行挂载,需要通过 $mount
方法挂载
Has "template" option?
是否含有 template
选项, 如果含有该选项,需要将 template
编译成 render
函数,render
函数是用来将模板和 data
数据编译成 html。如果没有 template
选项,就将外部的 HTML 作为模板编译,也就是在 template
标签中写的 HTML
beforeMount
beforeMount
钩子函数发生在 mount
,也就是 DOM 挂载之前,它的调用时机是在 mountComponent
函数中,并在该函数内,调用了beforeMount
与mounted
, 定义在 src/core/instance/lifecycle.js
中
开始渲染虚拟 dom,会执行一个 new Watcher
用来监听数据更新的
mounted 钩子函数的执行顺序也是先子后父(子组件的 mounted 先执行,在渲染父组件的 mounted 方法)
当Vue组件的$options属性中具有el属性将会在此元素上进行挂载内容
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
挂载要区分runtime only和runtime+compile,一个最主要的特征是runtime only的Vue对象中有渲染函数而runtime+compile的版本是需要经过编译生成渲染函数。
runtime only版本 => \src\platforms\web\runtime\index.js
runtime+compile => \src\platforms\web\entry-runtime-with-compiler.js => \src\platforms\web\runtime\index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el // 组件挂载时 `el` 为`undefined`
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount') // 所以获取到的`$el`为`undefined`
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
// 渲染watch。 经过渲染后,即可获取`$el`
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
// 因为已经渲染,`$el`此时已经可以成功获取
callHook(vm, 'mounted')
}
return vm
}
在执行 vm._render()
函数渲染 VNode 之前,执行了 beforeMount
钩子函数,在执行完 vm._update()
把 VNode patch 到真实 DOM 后,执行 mounted
钩子。
updateComponent函数,这个函数是整个挂载的核心,它由2部分组成,_render函数和_update函数
- render函数最终会执行之前在
initRender
定义的createElement函数,作用是创建vnode - update函数会将上面的render函数生成的vnode渲染成一个真实的DOM树,并挂载到挂载点上
mounted
组件的 VNode patch 到 DOM 后,会执行 invokeInsertHook
函数,把 insertedVnodeQueue
里保存的钩子函数依次执行一遍,它的定义在 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
中:
const componentVNodeHooks = {
// ...
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
// ...
},
}
每个子组件都是在这个钩子函数中执行 mounted
钩子函数,insertedVnodeQueue
的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted
钩子函数的执行顺序也是先子后父
beforeUpdate
beforeUpdate
的执行时机是在渲染 Watcher 的 before
函数中
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// 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 */)
// ...
}
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')
}
}
}
updated
updatedQueue
是更新了的 wathcer
数组,那么在 callUpdatedHooks
函数中,它对这些数组做遍历,只有满足当前 watcher
为 vm._watcher
以及组件已经 mounted
这两个条件,才会执行 updated
钩子函数
在实例化 Watcher
的过程中,在它的构造函数里会判断 isRenderWatcher
,接着把当前 watcher
的实例赋值给 vm._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)
// ...
}
}
还把当前 wathcer
实例 push 到 vm._watchers
中,vm._watcher
是专门用来监听 vm
上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher
,因此在 callUpdatedHooks
函数中,只有 vm._watcher
的回调执行完毕后,才会执行 updated
钩子函数
beforeDestroy
beforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段,最终会调用 $destroy
方法,它的定义在 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
}
}
从 parent
的 $children
中删掉自身,删除 watcher
,当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroy
钩子函数
在 $destroy
的执行过程中,它又会执行 vm.__patch__(vm._vnode, null)
触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy
钩子函数执行顺序是先子后父,和 mounted
过程一样
destroyed
destroyed钩子函数在Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁
参考链接:https://blog.csdn.net/jifukui/article/details/106756103
详解vue生命周期 https://segmentfault.com/a/1190000011381906
vue生命周期各阶段详情分析 https://blog.csdn.net/weixin_43456275/article/details/105754927
网友评论