从源码看Vue生命周期
使用Vue开发对于Vue生命周期的理解自然少不了,以前在面试时候也被问到过,当时也就只能回答出生命周期的几个钩子函数beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、beforeDestroy
、destroyed
。还有当使用
进行组件缓存的时候会有activated
、deactivated
生命周期。
先看官网图:
文章图片
lifecycle.png 本文就我根据资料阅读Vue生命周期源码来了解生命周期中都做了哪些事情。
初始化流程
// src/core/instance/index.js :8
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
当我们调用
new Vue()
的时候,会直接调用Vue
实例上的_init
方法。// src/core/instance/init.js :15
Vue.prototype._init = function (options?: Object) {
//...
initLifecycle(vm) // 初始化生命周期相关属性
initEvents(vm) // 初始化事件
initRender(vm) //初始化渲染函数
callHook(vm, 'beforeCreate')
}
在_init方法中,先对实例上的属性进行一些处理,比如合并组件options,初始化生命周期相关属性(绑定父组件,根组件等),初始化事件(父组件添加的事件),初始化渲染函数(给实例添加
$attrs
和$listeners
属性),然后调用beforeCreate
生命周期钩子函数。beforeCreate被调用完成之后做了以下几件事
// src/core/instance/init.js :15
Vue.prototype._init = function (options?: Object) {
//...
callHook(vm, 'beforeCreate')
initInjections(vm) // 初始化inject
initState(vm)//初始化state
initProvide(vm) // 初始化provide
callHook(vm, 'created') //调用created生命周期
}
- 初始化
inject
- 初始化
state
- 初始化
props
- 初始化
methods
- 初始化
data
- 初始化
computed
- 初始化
watch
- 初始化
- 初始化
provide
//src/core/instance/state.js :48
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) //初始化props
if (opts.methods) initMethods(vm, opts.methods) //初始化methods
if (opts.data) {
initData(vm)//初始化data
} else {
observe(vm._data = https://www.it610.com/article/{}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) //初始化computed
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch) //初始化watch
}
}
由方法执行顺序可知,在
data
中可以使用props
,不会报错,反过来则不行。然后执行
created
钩子函数created
执行完成之后,会去调用vm.$mount()
方法,开始挂载组件到dom
上。//src/core/instance/init.js :68
//...
callHook(vm,"created")
//...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
感觉
$mount
方法比较重要,先看看$mount方法。// src/platforms/web/runtime/index.js :37
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
其中核心是
mountComponent
方法export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
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')
//...
}
这一步主要是判断当前实例是否含有
render
函数,如果有render
函数那么就准备开始执行beforeMount
钩子函数,否则直接提示报错“Failed to mount component: template or render function not defined”。如果使用了
runtime-with-compile
版本(没有render函数)详情见官网运行时 + 编译器 vs. 只包含运行时 在实例化Vue
时,将传入的template
通过一系列编译生成render
函数。- 编译这个
template
,生成AST
抽象语法树。 - 优化这个
AST
,标记静态节点。(渲染过程中不会变得那些节点,优化性能)。 - 根据
AST
,生成render
函数。
const ast = parse(template.trim(),options)
if(options.optimize !== false){
optimize(ast,options)
}
const code = generate(ast,options)
总之,在有了
render
函数之后,就可以进行渲染步骤了,执行beforeMount
钩子函数。beforeMount
执行完成之后接着往下走://...
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)
}
}
//...
定义一个渲染组件的函数
updateComponent
updateComponent =() => {
vm._update(vm._render,hydrating)
}
vm._render
就是调用render
函数生成一个vnode
,而vm._update
方法则会对这个vnode
进行patch
操作,帮我们把vnode
通过cleateElm
函数创建新节点,并且渲染到dom
中。//src/core/instance/lifecycle.js :59
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
//...
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
//...
}
看完
updateComponent
方法里面的具体实现之后接下来就是要执行,
updateComponent
方法了。执行是由
Watcher
类负责执行的。//src/core/instance/lifecycle.js :197
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
为什么要用
Watcher
来执行呢?因为在执行过程中,需要去观测这个函数依赖了哪些响应式的数据,将来在数据更新的时候,我们需要再重新执行updateComponent
函数。如果是更新后调用
updateComponent
函数,updateComponent
内部的path
就不再是初始化的时候创建节点,而是通过diff算法
将差异的地方找到,以最小化的代价更新到真实dom
上。Watcher
中有一个before方法,逻辑是当vm._isMounted
,也就是第一次挂载完成之后,再次更新视图之前,会先调用beforeUpdate
钩子函数。注意:如果在
render
过程中有子组件的话,此时子组件也会有一系列的初始化过程,也会走之前所说的所有过程,因此这是一个递归构建过程。当有子组件时生命周期执行过程:
父 beforeCreate
父 created
父 beforeMount
之后--->render
子 beforeCreate
子 created
子 beforeMount
之后--->render
子 mounted
父 mounted
//src/core/instance/lifecycle.js :208
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
最终
mounted
生命周期钩子函数触发更新流程
当一个响应式属性被更新,触发了
Watcher
的回调函数,也就是updateComponent
方法updateComponent = () => {
vm._update(vm._render(), hydrating)
}new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
在更新之前会先进行判断,是否是更新
vm._isMounted
如果是更新那么就会直接执行beforeUpdate
生命周期钩子。Vue 异步执行 DOM 更新 由于Vue组件的异步更新机制,当响应式数据发生变化,根据数据劫持此时会调用
dep.notify
(src/core/observer/index.js : 191),之后会在Dep中去遍历所有watcher
进行更新subs[i]update()
(src/core/observer/dep.js : 47),之后在Watcher中会发现,在调用update
方法后,会调用Watcher
中的update
方法()会发现它执行了一个叫queueWatcher
的方法,具体可以看下代码。// src/core/observer/scheduler.js :164
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
从代码中可以看到,
queueWatcher
方法主要做了几件事- 将watcher存到一个队列queue中,
- 在nextTick方法中执行
flushSchedulerQueue
方法
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0;
index < queue.length;
index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
在
flushSchedulerQueue
方法中主要干的事情就是,遍历执行存放watcher的queue
,并且判断如果当前watcher有before方法,那么就先执行watcher.before,此时就会触发之前的callHook(vm, 'beforeUpdate')
方法,触发beforeUpdate
生命周期钩子函数(src/core/instance/lifecycle.js)。而nextTick
方法就是将flushSchedulerQueue
方法存到一个数组callbacks
中// src/core/util/next-tick.js :87
export function nextTick (cb?: Function, ctx?: Object) {
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
}
最终会执行
timerFunc
方法//src/core/instance/util/next-tick.js :33
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = https://www.it610.com/article/String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !=='undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
这段代码就是nextTick的核心代码了,Vue响应式更新为什么是异步的也在这里能找到答案,首先判断运行环境是否支持
Promise
,如果不支持就判断是否支持MutationObserver,如果不支持,就去判断是否支持setImmediate
,如果还不支持就用setTimeout
大法。最终最终会将callbacks
数组中的方法在异步方法中执行,到此,Vue响应式异步更新也差不多梳理了一遍。beforeUpdate
生命周期执行完之后,就开始一系列的patch
、diff
流程后组件渲染完毕之后就会调用updated
生命周期钩子函数。//src/core/observer/scheduler.js :130
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')
}
}
}
【从源码看Vue生命周期】注意:这里
watcher
中的updated
是倒序调用的,所以当同一个属性在父-子
组件中都有使用,收集依赖是按照父->子
收集,但是触发updated
钩子函数却是子->父
。updated
生命周期钩子函数执行完成之后渲染更新流程也就到此结束。销毁流程
在更新流程中,如果发现有组件在下一轮渲染中消失,比如
v-for
对应的数组中少了数据,或者v-if
控制的组件由true变为false,那么就会调用removeVnodes
进入组件的销毁流程。removeVnodes
会调用vnode
的destroy
生命周期,而destroy
内部则会调用vm.destroy
.(keep-alive包裹的子组件除外)这时就会调用
callHook(vm,‘beforeDestroy’)
//src/core/instance/lifecycle.js :97
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
vm._isDestroyed = true
vm.__patch__(vm._vnode, null)
callHook(vm, 'destroyed')
vm.$off()
if (vm.$el) {
vm.$el.__vue__ = null
}
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
beforDestroy
生命周期钩子函数调用完之后,就会进行一系列的清理逻辑,比如断开父子关系,关闭watcher,移除引用的data等,之后会调用callHook(vm, 'destroyed')
destroyed
生命周期钩子函数执行完成。注意这里的销毁不是指将组件从试图中移除,使用dom获取方法还能获取到,这里指的销毁是切断组件生命,使其失去活力,Vue响应式等其他生命活动,销毁后的组件全程不会参与而已。
到此Vue整个生命周期也就结束了,实际上官网的生命周期图就已经完美的诠释了一切,这里只是从源码中又多窥探到一些东西而已,源码中其实还掺有复杂的逻辑,我在看的时候选择性的抛开,只看它主线上的逻辑。不过也还有看不懂的地方,记录如有错误,还望不吝指出。
参考:实例生命周期钩子
Vue 的生命周期之间到底做了什么事清?(源码详解)
写在最后:文中内容大多为自己平时从各种途径学习总结,文中参考文章大多收录在我的个人博客里,欢迎阅览http://www.tianleilei.cn
推荐阅读
- 一个小故事,我的思考。
- Docker应用:容器间通信与Mariadb数据库主从复制
- 第三节|第三节 快乐和幸福(12)
- 开学第一天(下)
- 一个人的碎碎念
- 死结。
- 我从来不做坏事
- “成长”读书社群招募
- 拍照一年啦,如果你想了解我,那就请先看看这篇文章
- 危险也是机会