Vue生命周期详解

131 阅读2分钟

每个实例在被创建之前都要经过一系列的初始化过程,这个过程就是vue的生命周期;Vue实例的生命周期从构建到销毁大概经过几个阶段:初始化、模板编译、挂载、运行、销毁;在vue实例的生命周期中,钩子函数就是在特定的时间节点会调用的函数(钩子:和回调差不多意思);具体看下官网给出的生命周期示意图:

image.png

初始化创建阶段

1、BeforeCreate () {}

在实例完全被创建之前执行该函数,此时data和methods还没有被初始化,拿不到里面的数据;

2、Created () {}

实例初始化时执行该函数,data和methods初始化完成,如果需要调用methods 中的方法,或操作data中的数据,最早只能在created中进行操作。在当前阶段无法与Dom进行交互,如果你非要想,可以通过vm.$nextTick来访问Dom

3、BeforeMount () {}

模板已经在内存中编译完成,但是还没有将模板渲染到页面中;beforeMount执行之前,页面中的元素没有被真正替换,只是写入的一些模板字符串;

4、Mounted () {}

表示内存中的模板已经真实的挂载到页面中,用户可以看到已经渲染好的页面

mounted是实例创建期间阶段的最后一个生命周期函数,当执行完 mounted 之后就表示实例已被完全创建,此时如果没有其它操作的话,这个实例就在内存中不动了

组件运行阶段

5、beforeUpdate

当执行 beforeUpdate 的时候,页面中的显示的数据还是未更新的旧数据,但此时的data 数据是最新的,页面的数据尚未和最新的数据保持同步更新

6、Updated

updated事件执行的时候,页面和 data 数据已经保持一致了,都是最新的数据

不能在Updated中更新数据,否则会导致死循环

7、Activated

使用keep-alive组件激活时调用

8、Deactivated

使用keep-alive组件销毁时调用

组件销毁阶段

9、BeforeDestroy

生命周期是实例销毁前,在这个函数内还是可以操作实例的

10、Destroyed

此时已不能再操作实例了。生命周期整个流程到此时就已经全部结束;

11、errorCaptured

当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播

你可以在此钩子中修改组件的状态。因此在模板或渲染函数中设置其它内容的短路条件非常重要,它可以防止当一个错误被捕获时该组件进入一个无限的渲染循环。

渲染顺序

1、父子组件的created和mounted的渲染顺序问题!!

加载渲染顺序:父组件先创建,子组件再创建;子组件先挂载,父组件再挂载

父beforeCreated > 父created> 子created> 子mounted > 父mounted

子组件更新过程:

父beforeUpdate > 子beforeUpdate > 子updated > 父updated

父组件更新过程:

父beforeUpdate > 父 updated

销毁过程:

父beforeDestory > 子beforeDestory > 子 destoryed > 父destroyed

(注:页面初始渲染不会调用beforeUpdate ,updated钩子,在挂载完成后改变页面才会调用)

2、规律:

组件的调用顺序和销毁操作都是先父后子(beforexxx),渲染完成或者销毁完成的顺序都是先子后父

async await异步对父子的created、mounted先后执行无阻塞作用,即父组件中有异步,用async await,子组件的created并不等父组件中的await执行完毕(不会等父组件的created执行完毕)再执行子组件的created

但是aysnc await 对生命钩子函数还是有作用的

无异步 父子组件钩子函数正常执行完毕
// 父组件
created () {
    console.log('父 created')
},
mounted () {
  console.log('父 mounted')
}
  
 // 子组件
created () {
  console.log('子 created')
},
mounted () {
  console.log('子 mounted')
},

image.png

有异步 父子组件钩子函数的执行完成由异步模块决定
// 父组件
async created () {
    console.time('father')
    await this.asyncTest()
    console.log('父 created')
    console.timeEnd('father')
  },
  mounted () {
    console.log('父 mounted')
  },
  methods: {
    asyncTest () {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('父 异步任务')
          resolve()
        }, 10000)
      })
    }
  }
  
// 子组件
async created () {
    console.time('son')
    await this.asyncTest()
    console.log('子 created')
    console.timeEnd('son')
  },
  mounted () {
    console.log('子 mounted')
  },
  methods: {
    asyncTest () {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('子 异步任务')
          resolve()
        }, 3000)
      })
    }
  }

image.png

可见,async await对钩子函数一样有用,和其他普通函数无异,不过,钩子函数不能用箭头函数(因为在初始化生命周期时,有一个绑定上下文的操作)

function invokeWithErrorHandling (
 handler,
 context,
 args,
 vm,
 info
) {
  var res;
  try {
    res = args ? handler.apply(context, args) : handler.call(context); // 绑定上下文执行生命周期钩子函数
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true;
    }
  } catch (e) {
    handleError(e, vm, info);
  }
  return res
}
activated 与 deactivated 生命钩子函数

只有使用了keep-alive,组件才会有执行这两个钩子函数

// 父组件
<template>
  <div>
    <keep-alive v-if="showSon">
      <test-son />
    </keep-alive>
    <button @click="test">按钮</button>
  </div>
</template>

<script>
data () {
    return {
      showSon: true
    }
  },
  methods: {
    test () {
      this.showSon = !this.showSon
    }
  }
</script>

// 子组件
activated () {
  console.log('子 activated')
},
deactivated () {
  console.log('子 deactivated')
},
beforeDestroy () {
  console.log('子 beforeDestory')
}

进入页面,activated触发;当去切换showSon的值时,deactivatedbeforeDestroy都触发;

从源码角度理解

1、beforeCreate和created

// src/core/instance/init
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    // 合并选项部分已省略
    
    initLifecycle(vm)  
    // 主要就是给vm对象添加了 $parent$root$children 属性,以及一些其它的生命周期相关的标识
    initEvents(vm) // 初始化事件相关的属性
    initRender(vm)  // vm 添加了一些虚拟 dom、slot 等相关的属性和方法
    callHook(vm, 'beforeCreate')  // 调用 beforeCreate 钩子
    //下面 initInjections(vm) 和 initProvide(vm) 两个配套使用,用于将父组件 _provided 中定义的值,通过 inject 注入到子组件,且这些属性不会被观察
    initInjections(vm) 
    initState(vm)   // props、methods、data、watch、computed等数据初始化
    initProvide(vm) 
    callHook(vm, 'created')  // 调用 created 钩子
  }
}

// src/core/instance/state
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)
  }
}

由上可知,beforeCreate钩子的时候没有对props、methods、data、computed、watch上的数据的访问权限,在created中才可以

2、beforeMount和mounted

// mountComponent 核心就是先实例化一个渲染Watcher
// 在它的回调函数中会调用 updateComponent 方法
// 两个核心方法 vm._render(生成虚拟Dom) 和 vm._update(映射到真实Dom)
// src/core/instance/lifecycle
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    ...
  }
  callHook(vm, 'beforeMount')  // 调用 beforeMount 钩子

  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
    // 将虚拟 Dom 映射到真实 Dom 的函数。
    // vm._update 之前会先调用 vm._render() 函数渲染 VNode
      ...
      const vnode = vm._render()
      ...
      vm._update(vnode, hydrating)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
     // 先判断是否 mouted 完成 并且没有被 destroyed
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')  //调用 mounted 钩子
  }
  return vm
}

在执行vm._render()函数渲染VNode之前,执行了 beforeMount钩子函数,在执行完 vm._update()把VNode patch到真实Dom后,执行 mouted钩子。也就明白了为什么直到mounted阶段才名正言顺的拿到了Dom

3、beforeUpdate和updated

  // src/core/instance/lifecycle
 new Watcher(vm, updateComponent, noop, {
    before () {
     // 先判断是否 mouted 完成 并且没有被 destroyed
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')  // 调用 beforeUpdate 钩子
      }
    }
  }, true /* isRenderWatcher */)
 
 // src/core/observer/scheduler 
 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) {
       // 只有满足当前 watcher 为 vm._watcher(也就是当前的渲染watcher)
       // 以及组件已经 mounted 并且没有被 destroyed 才会执行 updated 钩子函数。
       callHook(vm, 'updated')  // 调用 updated 钩子
       }
     }
   }

看看watch的作用:监听实例上的数据变化,进而控制渲染流程。

export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    // 在它的构造函数里会判断 isRenderWatcher,
    // 接着把当前 watcher 的实例赋值给 vm._watcher
    isRenderWatcher?: boolean
  ) {
    // 还把当前 wathcer 实例 push 到 vm._watchers 中,
    // vm._watcher 是专门用来监听 vm 上数据变化然后重新渲染的,
    // 所以它是一个渲染相关的 watcher,因此在 callUpdatedHooks 函数中,
    // 只有 vm._watcher 的回调执行完毕后,才会执行 updated 钩子函数
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    ...
}

4、beforeDestroy和destroyed

// src/core/instance/lifecycle.js
  // 在 $destroy 的执行过程中,它会执行 vm.__patch__(vm._vnode, null)
  // 触发它子组件的销毁钩子函数,这样一层层的递归调用,
  // 所以 destroy 钩子函数执行顺序是先子后父,和 mounted 过程一样。
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')  // 调用 beforeDestroy 钩子
    vm._isBeingDestroyed = true
    // 一些销毁工作
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // 拆卸 watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    ...
    vm._isDestroyed = true
    // 调用当前 rendered tree 上的 destroy 钩子
    // 发现子组件,会先去销毁子组件
    vm.__patch__(vm._vnode, null)
    callHook(vm, 'destroyed')  // 调用 destroyed 钩子
    // 关闭所有实例侦听器。
    vm.$off()
    // 删除 __vue__ 引用
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // 释放循环引用
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

执行一个__patch__函数去卸载节点,清空watch实例,清空监听等;

生命周期的知识点并不复杂,但是真正通过源码去理解还是有一定难度,我的理解和学习还是很浅层面,希望有大佬批评指教~