接触 Vue 也有一段时间了,最近打算好好的整理一下 Vue 的一些相关知识,所以就打算从 Vue 的生命周期开始写起了。
首先,还是要祭出官网的这张生命周期图示。就像官网文档所说的,你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。

我们就根据官网的这张图,详细的分析一下生命周期的每个阶段都发生了什么
new Vue()
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 关键词创建,然后调用了 this._init(options)
this._init
vm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')...if (vm.$options.el) {vm.$mount(vm.$options.el)}
这里就只贴出和生命周期有关的一部分代码,其他的代码我们找机会再详细的分析,从这里我们可以看到,它进行了一些列的操作,我们一个一个来看
initLifecycle(vm)
export function initLifecycle (vm: Component) {const options = vm.$options// locate first non-abstract parentlet parent = options.parentif (parent && !options.abstract) {while (parent.$options.abstract && parent.$parent) {parent = parent.$parent}parent.$children.push(vm)}vm.$parent = parentvm.$root = parent ? parent.$root : vmvm.$children = []vm.$refs = {}vm._watcher = nullvm._inactive = nullvm._directInactive = falsevm._isMounted = falsevm._isDestroyed = falsevm._isBeingDestroyed = false}
这个方法主要就是给 vm 对象添加了 $parent、$root、$children、$refs 属性,以及一些和其他生命周期相关的标示。options.abstract 用来判断是否是抽象组件,例如 keep-alive 等。
initEvents(vm)
export function initEvents (vm: Component) {vm._events = Object.create(null)vm._hasHookEvent = false// init parent attached eventsconst listeners = vm.$options._parentListenersif (listeners) {updateComponentListeners(vm, listeners)}}
初始化事件相关的属性
initRender(vm)
export function initRender (vm: Component) {vm._vnode = null // the root of the child treevm._staticTrees = null // v-once cached treesconst options = vm.$optionsconst parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent treeconst renderContext = parentVnode && parentVnode.contextvm.$slots = resolveSlots(options._renderChildren, renderContext)vm.$scopedSlots = emptyObject// bind the createElement fn to this instance// so that we get proper render context inside it.// args order: tag, data, children, normalizationType, alwaysNormalize// internal version is used by render functions compiled from templatesvm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)// normalization is always applied for the public version, used in// user-written render functions.vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)// $attrs & $listeners are exposed for easier HOC creation.// they need to be reactive so that HOCs using them are always updatedconst parentData = parentVnode && parentVnode.data/* istanbul ignore else */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)}}
这里给 vm 加了一些虚拟 dom、slots 等相关属性和方法
在这里调用了 beforeCreate 钩子函数
initInjections(vm)、initProvide(vm)
这两个在函数虽然在 initState(vm) 的前后调用,由于它们在同一个文件里,就拿出来一起解释一下
export function initProvide (vm: Component) {const provide = vm.$options.provideif (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)}}
这两个配套使用,用于将父组件_provided中定义的值,通过inject注入到子组件,且这些属性不会被观察。
initState(vm)
export function initState (vm: Component) {vm._watchers = []const opts = vm.$optionsif (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)}}
这个函数就是初始化 State ,包括 props,methods, data等等
然后就调用了 created 钩子函数
我们可以看出,create 阶段,基本就是对传入数据的格式化,数据的双向绑定,和一些属性的初始化。
然后代码要判断 el.$options.el 是否存在,若不存在,就会一直等到 vm.$mount(el) 被调用的时候再进行下一步,如果存在的话,就会调用 vm.$mount(vm.$options.el)
vm.$mount()
const mount = Vue.prototype.$mountVue.prototype.$mount = function (el?: string | Element,hydrating?: boolean): Component {el = el && query(el)/* istanbul ignore if */if (el === document.body || el === document.documentElement) {process.env.NODE_ENV !== 'production' && warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`)return this}const options = this.$options// resolve template/el and convert to render functionif (!options.render) {let template = options.templateif (template) {if (typeof template === 'string') {if (template.charAt(0) === '#') {template = idToTemplate(template)/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && !template) {warn(`Template element not found or is empty: ${options.template}`,this)}}} else if (template.nodeType) {template = template.innerHTML} else {if (process.env.NODE_ENV !== 'production') {warn('invalid template option:' + template, this)}return this}} else if (el) {template = getOuterHTML(el)}if (template) {/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {mark('compile')}const { render, staticRenderFns } = compileToFunctions(template, {shouldDecodeNewlines,shouldDecodeNewlinesForHref,delimiters: options.delimiters,comments: options.comments}, this)options.render = renderoptions.staticRenderFns = staticRenderFns/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {mark('compile end')measure(`vue ${this._name} compile`, 'compile', 'compile end')}}}return mount.call(this, el, hydrating)}
const mount = Vue.prototype.$mount 是为了保存之前的方法,然后再对其进行重写。
根据 el 的定义,通过 query(el) 方法去查找到对应的元素,随进行了一次判断,要求 el 指定的不可以是 document.body 或 document 相关元素。
接下来它从 DOM 中获取到了元素的 String Template,然后调用 compileToFunctions 方法,生成 render 方法,最后将上述返回的 render 方法赋值给了 vm.$options 对象。
结尾处调用 mount.call 方法创建 Watcher 对象并使用 render 方法对 el 元素进行首次绘制
Vue.prototype.$mount = function (el,hydrating) {el = el && inBrowser ? query(el) : undefined;return mountComponent(this, el, hydrating)};
这里可以看出调用了 mountComponent 方法并返回,下面让我们来看一下这个方法
export function mountComponent (vm: Component,el: ?Element,hydrating?: boolean): Component {vm.$el = elif (!vm.$options.render) {vm.$options.render = createEmptyVNodeif (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')let updateComponent/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {updateComponent = () => {const name = vm._nameconst id = vm._uidconst 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 definednew 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 hookif (vm.$vnode == null) {vm._isMounted = truecallHook(vm, 'mounted')}return vm}
在调用 beforeMount() 之前,render 函数已经首次被调用,但是此时 el 还没有对数据进行渲染,也就是虚拟 DOM 内容。
在这里调用了 beforeMount()
随后定义了 updateComponent() 方法,这段代码的重点在
updateComponent = () => {vm._update(vm._render(), hydrating)}
我们来看一下这个 _update() 函数又是什么来头
Vue.prototype._update = function (vnode, hydrating) {var vm = this;if (vm._isMounted) {callHook(vm, 'beforeUpdate');}var prevEl = vm.$el;var prevVnode = vm._vnode;var prevActiveInstance = activeInstance;activeInstance = vm;vm._vnode = vnode;// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.if (!prevVnode) {// initial rendervm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */,vm.$options._parentElm,vm.$options._refElm);// no need for the ref nodes after initial patch// this prevents keeping a detached DOM tree in memory (#5851)vm.$options._parentElm = vm.$options._refElm = null;} else {// updatesvm.$el = vm.__patch__(prevVnode, vnode);}activeInstance = prevActiveInstance;// update __vue__ referenceif (prevEl) {prevEl.__vue__ = null;}if (vm.$el) {vm.$el.__vue__ = vm;}// if parent is an HOC, update its $el as wellif (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {vm.$parent.$el = vm.$el;}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.};
这里它对于新旧 vnode 进行对比,然后重新赋值给 vm.$el,在这里我们发现了一个比较形象的词 patch,打补丁,也就是意味着像打补丁一样有变换的元素进行局部的修补,以达到性能最大优化。可以看出 patch 最为核心的要素就是 vnode。然而我们知道 vnode 是 vm_render() 构造出来的,环环相扣,这些要素看似独立又互相起作用。
在 vm.$el 生成后,原来的 el 被替换掉,并且挂载到实例上,此时,el 的数据渲染也已经完成了。
在这里调用了 mounted()
接下来我们先讲销毁阶段,更新阶段我们放在最后讲。首先还是把代码放出来。
Vue.prototype.$destroy = function () {const vm: Component = thisif (vm._isBeingDestroyed) {return}callHook(vm, 'beforeDestroy')vm._isBeingDestroyed = true// remove self from parentconst parent = vm.$parentif (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {remove(parent.$children, vm)}// teardown watchersif (vm._watcher) {vm._watcher.teardown()}let i = vm._watchers.lengthwhile (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 treevm.__patch__(vm._vnode, null)// fire destroyed hookcallHook(vm, 'destroyed')// turn off all instance listeners.vm.$off()// remove __vue__ referenceif (vm.$el) {vm.$el.__vue__ = null}// release circular reference (#6759)if (vm.$vnode) {vm.$vnode.parent = null}}}
当我们调用 vm.$destroy 时,会先判断是否已经在销毁。
在这里调用了beforeDestroy()
在这里我们可以知道,在这个函数里,实例仍然完全可用。
接下来,就开始了销毁过程,实例所指示的所有东西都会解除绑定,所有事件的监听器会被移除,所有的子实例也会被销毁。
在这里调用了 destroy()
最后,我们来分析一下 update 这个阶段
在上面的讲解中,我们提到了 updateComponent() 这个函数,我们得知 updateComponent() 是根据 vnode 绘制其所关联的 DOM 节点,那么 updateComponent() 又是怎么被触发的呢?
通过上面的代码我们可以看出,这个函数好像和 Watcher 有点关系,那我们就来看看这个 Watcher 究竟是什么。
new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted) {callHook(vm, 'beforeUpdate')}}}, true /* isRenderWatcher */)
我们可以看出,updateComponent 被当作参数之一被传进了 Watcher 构造函数,我们来看一下 Watcher 的具体结构。
export default class Watcher {...constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean) {this.vm = vmif (isRenderWatcher) {vm._watcher = this}vm._watchers.push(this)// optionsif (options) {this.deep = !!options.deepthis.user = !!options.userthis.computed = !!options.computedthis.sync = !!options.syncthis.before = options.before} else {this.deep = this.user = this.computed = this.sync = false}this.cb = cbthis.id = ++uid // uid for batchingthis.active = truethis.dirty = this.computed // for computed watchersthis.deps = []this.newDeps = []this.depIds = new Set()this.newDepIds = new Set()this.expression = process.env.NODE_ENV !== 'production'? expOrFn.toString(): ''// parse expression for getterif (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = function () {}process.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm)}}if (this.computed) {this.value = undefinedthis.dep = new Dep()} else {this.value = this.get()}}
首先,将 Watcher 实例 push 到 vm._watchers 中,由此可见 Watcher 与 vnode 之间是一对一的关系,而 vm 和 Watcher 是一对多关系。然后将 updateComponent 赋值给 this.getter。最后执行 this.get() 方法。
Watcher.prototype.get = function get () {pushTarget(this);var value;var vm = this.vm;try {value = this.getter.call(vm, vm);} catch (e) {if (this.user) {handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));} else {throw e}} finally {// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value);}popTarget();this.cleanupDeps();}return value};
This.getter 就是我们之前说的 updateComponent。所以这里实际上就是在调用 updateComponent 函数,而 updateComponent 方法的作用就是根据 vnode 绘制相关的 DOM 节点。
到此,Vue 的生命周期的详解就全部结束了,对于初学者来说,一开始可能不需要了解的这么深入,知道哪个阶段可以调用 methods 哪个阶段 data 已经存在了就可以了,但是随着对于 Vue 的逐渐熟练,就不仅仅只停留在知其然,更要知其所以然。所以还是推荐大家去看一下 Vue 源码,肯定会有很大收获的。
