接触 Vue 也有一段时间了,最近打算好好的整理一下 Vue 的一些相关知识,所以就打算从 Vue 的生命周期开始写起了。

首先,还是要祭出官网的这张生命周期图示。就像官网文档所说的,你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。

Life Cycle - 图1

我们就根据官网的这张图,详细的分析一下生命周期的每个阶段都发生了什么

new Vue()

  1. function Vue (options) {
  2. if (process.env.NODE_ENV !== 'production' &&
  3. !(this instanceof Vue)) {
  4. warn('Vue is a constructor and should be called with the `new` keyword')
  5. }
  6. this._init(options)
  7. }

通过对源码的阅读,了解到它首先判断了是不是通过 new 关键词创建,然后调用了 this._init(options)

this._init

  1. vm._self = vm
  2. initLifecycle(vm)
  3. initEvents(vm)
  4. initRender(vm)
  5. callHook(vm, 'beforeCreate')
  6. initInjections(vm) // resolve injections before data/props
  7. initState(vm)
  8. initProvide(vm) // resolve provide after data/props
  9. callHook(vm, 'created')
  10. ...
  11. if (vm.$options.el) {
  12. vm.$mount(vm.$options.el)
  13. }

这里就只贴出和生命周期有关的一部分代码,其他的代码我们找机会再详细的分析,从这里我们可以看到,它进行了一些列的操作,我们一个一个来看

initLifecycle(vm)

  1. export function initLifecycle (vm: Component) {
  2. const options = vm.$options
  3. // locate first non-abstract parent
  4. let parent = options.parent
  5. if (parent && !options.abstract) {
  6. while (parent.$options.abstract && parent.$parent) {
  7. parent = parent.$parent
  8. }
  9. parent.$children.push(vm)
  10. }
  11. vm.$parent = parent
  12. vm.$root = parent ? parent.$root : vm
  13. vm.$children = []
  14. vm.$refs = {}
  15. vm._watcher = null
  16. vm._inactive = null
  17. vm._directInactive = false
  18. vm._isMounted = false
  19. vm._isDestroyed = false
  20. vm._isBeingDestroyed = false
  21. }

这个方法主要就是给 vm 对象添加了 $parent、$root、$children、$refs 属性,以及一些和其他生命周期相关的标示。options.abstract 用来判断是否是抽象组件,例如 keep-alive 等。

initEvents(vm)

  1. export function initEvents (vm: Component) {
  2. vm._events = Object.create(null)
  3. vm._hasHookEvent = false
  4. // init parent attached events
  5. const listeners = vm.$options._parentListeners
  6. if (listeners) {
  7. updateComponentListeners(vm, listeners)
  8. }
  9. }

初始化事件相关的属性

initRender(vm)

  1. export function initRender (vm: Component) {
  2. vm._vnode = null // the root of the child tree
  3. vm._staticTrees = null // v-once cached trees
  4. const options = vm.$options
  5. const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  6. const renderContext = parentVnode && parentVnode.context
  7. vm.$slots = resolveSlots(options._renderChildren, renderContext)
  8. vm.$scopedSlots = emptyObject
  9. // bind the createElement fn to this instance
  10. // so that we get proper render context inside it.
  11. // args order: tag, data, children, normalizationType, alwaysNormalize
  12. // internal version is used by render functions compiled from templates
  13. vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  14. // normalization is always applied for the public version, used in
  15. // user-written render functions.
  16. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  17. // $attrs & $listeners are exposed for easier HOC creation.
  18. // they need to be reactive so that HOCs using them are always updated
  19. const parentData = parentVnode && parentVnode.data
  20. /* istanbul ignore else */
  21. if (process.env.NODE_ENV !== 'production') {
  22. defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
  23. !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
  24. }, true)
  25. defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
  26. !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
  27. }, true)
  28. } else {
  29. defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  30. defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  31. }
  32. }

这里给 vm 加了一些虚拟 dom、slots 等相关属性和方法

在这里调用了 beforeCreate 钩子函数

initInjections(vm)、initProvide(vm)

这两个在函数虽然在 initState(vm) 的前后调用,由于它们在同一个文件里,就拿出来一起解释一下

  1. export function initProvide (vm: Component) {
  2. const provide = vm.$options.provide
  3. if (provide) {
  4. vm._provided = typeof provide === 'function'
  5. ? provide.call(vm)
  6. : provide
  7. }
  8. }
  9. export function initInjections (vm: Component) {
  10. const result = resolveInject(vm.$options.inject, vm)
  11. if (result) {
  12. toggleObserving(false)
  13. Object.keys(result).forEach(key => {
  14. /* istanbul ignore else */
  15. if (process.env.NODE_ENV !== 'production') {
  16. defineReactive(vm, key, result[key], () => {
  17. warn(
  18. `Avoid mutating an injected value directly since the changes will be ` +
  19. `overwritten whenever the provided component re-renders. ` +
  20. `injection being mutated: "${key}"`,
  21. vm
  22. )
  23. })
  24. } else {
  25. defineReactive(vm, key, result[key])
  26. }
  27. })
  28. toggleObserving(true)
  29. }
  30. }

这两个配套使用,用于将父组件_provided中定义的值,通过inject注入到子组件,且这些属性不会被观察。

initState(vm)

  1. export function initState (vm: Component) {
  2. vm._watchers = []
  3. const opts = vm.$options
  4. if (opts.props) initProps(vm, opts.props)
  5. if (opts.methods) initMethods(vm, opts.methods)
  6. if (opts.data) {
  7. initData(vm)
  8. } else {
  9. observe(vm._data = {}, true /* asRootData */)
  10. }
  11. if (opts.computed) initComputed(vm, opts.computed)
  12. if (opts.watch && opts.watch !== nativeWatch) {
  13. initWatch(vm, opts.watch)
  14. }
  15. }

这个函数就是初始化 State ,包括 props,methods, data等等

然后就调用了 created 钩子函数

我们可以看出,create 阶段,基本就是对传入数据的格式化,数据的双向绑定,和一些属性的初始化。

然后代码要判断 el.$options.el 是否存在,若不存在,就会一直等到 vm.$mount(el) 被调用的时候再进行下一步,如果存在的话,就会调用 vm.$mount(vm.$options.el)

vm.$mount()

  1. const mount = Vue.prototype.$mount
  2. Vue.prototype.$mount = function (
  3. el?: string | Element,
  4. hydrating?: boolean
  5. ): Component {
  6. el = el && query(el)
  7. /* istanbul ignore if */
  8. if (el === document.body || el === document.documentElement) {
  9. process.env.NODE_ENV !== 'production' && warn(
  10. `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
  11. )
  12. return this
  13. }
  14. const options = this.$options
  15. // resolve template/el and convert to render function
  16. if (!options.render) {
  17. let template = options.template
  18. if (template) {
  19. if (typeof template === 'string') {
  20. if (template.charAt(0) === '#') {
  21. template = idToTemplate(template)
  22. /* istanbul ignore if */
  23. if (process.env.NODE_ENV !== 'production' && !template) {
  24. warn(
  25. `Template element not found or is empty: ${options.template}`,
  26. this
  27. )
  28. }
  29. }
  30. } else if (template.nodeType) {
  31. template = template.innerHTML
  32. } else {
  33. if (process.env.NODE_ENV !== 'production') {
  34. warn('invalid template option:' + template, this)
  35. }
  36. return this
  37. }
  38. } else if (el) {
  39. template = getOuterHTML(el)
  40. }
  41. if (template) {
  42. /* istanbul ignore if */
  43. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  44. mark('compile')
  45. }
  46. const { render, staticRenderFns } = compileToFunctions(template, {
  47. shouldDecodeNewlines,
  48. shouldDecodeNewlinesForHref,
  49. delimiters: options.delimiters,
  50. comments: options.comments
  51. }, this)
  52. options.render = render
  53. options.staticRenderFns = staticRenderFns
  54. /* istanbul ignore if */
  55. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  56. mark('compile end')
  57. measure(`vue ${this._name} compile`, 'compile', 'compile end')
  58. }
  59. }
  60. }
  61. return mount.call(this, el, hydrating)
  62. }

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 元素进行首次绘制

  1. Vue.prototype.$mount = function (
  2. el,
  3. hydrating
  4. ) {
  5. el = el && inBrowser ? query(el) : undefined;
  6. return mountComponent(this, el, hydrating)
  7. };

这里可以看出调用了 mountComponent 方法并返回,下面让我们来看一下这个方法

  1. export function mountComponent (
  2. vm: Component,
  3. el: ?Element,
  4. hydrating?: boolean
  5. ): Component {
  6. vm.$el = el
  7. if (!vm.$options.render) {
  8. vm.$options.render = createEmptyVNode
  9. if (process.env.NODE_ENV !== 'production') {
  10. /* istanbul ignore if */
  11. if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
  12. vm.$options.el || el) {
  13. warn(
  14. 'You are using the runtime-only build of Vue where the template ' +
  15. 'compiler is not available. Either pre-compile the templates into ' +
  16. 'render functions, or use the compiler-included build.',
  17. vm
  18. )
  19. } else {
  20. warn(
  21. 'Failed to mount component: template or render function not defined.',
  22. vm
  23. )
  24. }
  25. }
  26. }
  27. callHook(vm, 'beforeMount')
  28. let updateComponent
  29. /* istanbul ignore if */
  30. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  31. updateComponent = () => {
  32. const name = vm._name
  33. const id = vm._uid
  34. const startTag = `vue-perf-start:${id}`
  35. const endTag = `vue-perf-end:${id}`
  36. mark(startTag)
  37. const vnode = vm._render()
  38. mark(endTag)
  39. measure(`vue ${name} render`, startTag, endTag)
  40. mark(startTag)
  41. vm._update(vnode, hydrating)
  42. mark(endTag)
  43. measure(`vue ${name} patch`, startTag, endTag)
  44. }
  45. } else {
  46. updateComponent = () => {
  47. vm._update(vm._render(), hydrating)
  48. }
  49. }
  50. // we set this to vm._watcher inside the watcher's constructor
  51. // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  52. // component's mounted hook), which relies on vm._watcher being already defined
  53. new Watcher(vm, updateComponent, noop, {
  54. before () {
  55. if (vm._isMounted) {
  56. callHook(vm, 'beforeUpdate')
  57. }
  58. }
  59. }, true /* isRenderWatcher */)
  60. hydrating = false
  61. // manually mounted instance, call mounted on self
  62. // mounted is called for render-created child components in its inserted hook
  63. if (vm.$vnode == null) {
  64. vm._isMounted = true
  65. callHook(vm, 'mounted')
  66. }
  67. return vm
  68. }

在调用 beforeMount() 之前,render 函数已经首次被调用,但是此时 el 还没有对数据进行渲染,也就是虚拟 DOM 内容。

在这里调用了 beforeMount()

随后定义了 updateComponent() 方法,这段代码的重点在

  1. updateComponent = () => {
  2. vm._update(vm._render(), hydrating)
  3. }

我们来看一下这个 _update() 函数又是什么来头

  1. Vue.prototype._update = function (vnode, hydrating) {
  2. var vm = this;
  3. if (vm._isMounted) {
  4. callHook(vm, 'beforeUpdate');
  5. }
  6. var prevEl = vm.$el;
  7. var prevVnode = vm._vnode;
  8. var prevActiveInstance = activeInstance;
  9. activeInstance = vm;
  10. vm._vnode = vnode;
  11. // Vue.prototype.__patch__ is injected in entry points
  12. // based on the rendering backend used.
  13. if (!prevVnode) {
  14. // initial render
  15. vm.$el = vm.__patch__(
  16. vm.$el, vnode, hydrating, false /* removeOnly */,
  17. vm.$options._parentElm,
  18. vm.$options._refElm
  19. );
  20. // no need for the ref nodes after initial patch
  21. // this prevents keeping a detached DOM tree in memory (#5851)
  22. vm.$options._parentElm = vm.$options._refElm = null;
  23. } else {
  24. // updates
  25. vm.$el = vm.__patch__(prevVnode, vnode);
  26. }
  27. activeInstance = prevActiveInstance;
  28. // update __vue__ reference
  29. if (prevEl) {
  30. prevEl.__vue__ = null;
  31. }
  32. if (vm.$el) {
  33. vm.$el.__vue__ = vm;
  34. }
  35. // if parent is an HOC, update its $el as well
  36. if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
  37. vm.$parent.$el = vm.$el;
  38. }
  39. // updated hook is called by the scheduler to ensure that children are
  40. // updated in a parent's updated hook.
  41. };

这里它对于新旧 vnode 进行对比,然后重新赋值给 vm.$el,在这里我们发现了一个比较形象的词 patch,打补丁,也就是意味着像打补丁一样有变换的元素进行局部的修补,以达到性能最大优化。可以看出 patch 最为核心的要素就是 vnode。然而我们知道 vnode 是 vm_render() 构造出来的,环环相扣,这些要素看似独立又互相起作用。

在 vm.$el 生成后,原来的 el 被替换掉,并且挂载到实例上,此时,el 的数据渲染也已经完成了。

在这里调用了 mounted()

接下来我们先讲销毁阶段,更新阶段我们放在最后讲。首先还是把代码放出来。

  1. Vue.prototype.$destroy = function () {
  2. const vm: Component = this
  3. if (vm._isBeingDestroyed) {
  4. return
  5. }
  6. callHook(vm, 'beforeDestroy')
  7. vm._isBeingDestroyed = true
  8. // remove self from parent
  9. const parent = vm.$parent
  10. if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
  11. remove(parent.$children, vm)
  12. }
  13. // teardown watchers
  14. if (vm._watcher) {
  15. vm._watcher.teardown()
  16. }
  17. let i = vm._watchers.length
  18. while (i--) {
  19. vm._watchers[i].teardown()
  20. }
  21. // remove reference from data ob
  22. // frozen object may not have observer.
  23. if (vm._data.__ob__) {
  24. vm._data.__ob__.vmCount--
  25. }
  26. // call the last hook...
  27. vm._isDestroyed = true
  28. // invoke destroy hooks on current rendered tree
  29. vm.__patch__(vm._vnode, null)
  30. // fire destroyed hook
  31. callHook(vm, 'destroyed')
  32. // turn off all instance listeners.
  33. vm.$off()
  34. // remove __vue__ reference
  35. if (vm.$el) {
  36. vm.$el.__vue__ = null
  37. }
  38. // release circular reference (#6759)
  39. if (vm.$vnode) {
  40. vm.$vnode.parent = null
  41. }
  42. }
  43. }

当我们调用 vm.$destroy 时,会先判断是否已经在销毁。

在这里调用了beforeDestroy()

在这里我们可以知道,在这个函数里,实例仍然完全可用。

接下来,就开始了销毁过程,实例所指示的所有东西都会解除绑定,所有事件的监听器会被移除,所有的子实例也会被销毁。

在这里调用了 destroy()

最后,我们来分析一下 update 这个阶段

在上面的讲解中,我们提到了 updateComponent() 这个函数,我们得知 updateComponent() 是根据 vnode 绘制其所关联的 DOM 节点,那么 updateComponent() 又是怎么被触发的呢?

通过上面的代码我们可以看出,这个函数好像和 Watcher 有点关系,那我们就来看看这个 Watcher 究竟是什么。

  1. new Watcher(vm, updateComponent, noop, {
  2. before () {
  3. if (vm._isMounted) {
  4. callHook(vm, 'beforeUpdate')
  5. }
  6. }
  7. }, true /* isRenderWatcher */)

我们可以看出,updateComponent 被当作参数之一被传进了 Watcher 构造函数,我们来看一下 Watcher 的具体结构。

  1. export default class Watcher {
  2. ...
  3. constructor (
  4. vm: Component,
  5. expOrFn: string | Function,
  6. cb: Function,
  7. options?: ?Object,
  8. isRenderWatcher?: boolean
  9. ) {
  10. this.vm = vm
  11. if (isRenderWatcher) {
  12. vm._watcher = this
  13. }
  14. vm._watchers.push(this)
  15. // options
  16. if (options) {
  17. this.deep = !!options.deep
  18. this.user = !!options.user
  19. this.computed = !!options.computed
  20. this.sync = !!options.sync
  21. this.before = options.before
  22. } else {
  23. this.deep = this.user = this.computed = this.sync = false
  24. }
  25. this.cb = cb
  26. this.id = ++uid // uid for batching
  27. this.active = true
  28. this.dirty = this.computed // for computed watchers
  29. this.deps = []
  30. this.newDeps = []
  31. this.depIds = new Set()
  32. this.newDepIds = new Set()
  33. this.expression = process.env.NODE_ENV !== 'production'
  34. ? expOrFn.toString()
  35. : ''
  36. // parse expression for getter
  37. if (typeof expOrFn === 'function') {
  38. this.getter = expOrFn
  39. } else {
  40. this.getter = parsePath(expOrFn)
  41. if (!this.getter) {
  42. this.getter = function () {}
  43. process.env.NODE_ENV !== 'production' && warn(
  44. `Failed watching path: "${expOrFn}" ` +
  45. 'Watcher only accepts simple dot-delimited paths. ' +
  46. 'For full control, use a function instead.',
  47. vm
  48. )
  49. }
  50. }
  51. if (this.computed) {
  52. this.value = undefined
  53. this.dep = new Dep()
  54. } else {
  55. this.value = this.get()
  56. }
  57. }

首先,将 Watcher 实例 push 到 vm._watchers 中,由此可见 Watcher 与 vnode 之间是一对一的关系,而 vm 和 Watcher 是一对多关系。然后将 updateComponent 赋值给 this.getter。最后执行 this.get() 方法。

  1. Watcher.prototype.get = function get () {
  2. pushTarget(this);
  3. var value;
  4. var vm = this.vm;
  5. try {
  6. value = this.getter.call(vm, vm);
  7. } catch (e) {
  8. if (this.user) {
  9. handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
  10. } else {
  11. throw e
  12. }
  13. } finally {
  14. // "touch" every property so they are all tracked as
  15. // dependencies for deep watching
  16. if (this.deep) {
  17. traverse(value);
  18. }
  19. popTarget();
  20. this.cleanupDeps();
  21. }
  22. return value
  23. };

This.getter 就是我们之前说的 updateComponent。所以这里实际上就是在调用 updateComponent 函数,而 updateComponent 方法的作用就是根据 vnode 绘制相关的 DOM 节点。

到此,Vue 的生命周期的详解就全部结束了,对于初学者来说,一开始可能不需要了解的这么深入,知道哪个阶段可以调用 methods 哪个阶段 data 已经存在了就可以了,但是随着对于 Vue 的逐渐熟练,就不仅仅只停留在知其然,更要知其所以然。所以还是推荐大家去看一下 Vue 源码,肯定会有很大收获的。