vue生命周期图(探究源码之路)

学习主线:从vue2生命周期图出发,找出背后的源码实现,来探索vue成长之路!

[TOC]

生命周期图

vue2_lifecycle.png
vue2_lifecycle_cn.png

vue2.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元素函数的定义,继承父节点的attrs属性和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 函数中,并在该函数内,调用了beforeMountmounted, 定义在 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树,并挂载到挂载点上
image

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 函数中,它对这些数组做遍历,只有满足当前 watchervm._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

beforeDestroydestroyed 钩子函数的执行时机在组件销毁的阶段,最终会调用 $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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容