10 Vue生命周期相关

Vue的生命周期方法有哪些

  1. Vue 实例有一个完整的生命周期,也就是从开始创建初始化数据编译模版挂载Dom -> 渲染更新 -> 渲染卸载等一系列过程,我们称这是Vue的生命周期
  2. Vue 生命周期总共分为8个阶段创建前/后载入前/后更新前/后销毁前/后

beforeCreate => created => beforeMount => Mounted => beforeUpdate => updated => beforeDestroy => destroyedkeep-alive下:activateddeactivated

生命周期vue2生命周期vue3描述
beforeCreatebeforeCreate在实例初始化之后,数据观测(data observer) 之前被调用。
createdcreated实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
beforeMountbeforeMount在挂载开始之前被调用:相关的 render 函数首次被调用
mountedmountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdatebeforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
updatedupdated由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子
beforeDestroybeforeUnmount实例销毁之前调用。在这一步,实例仍然完全可用
destroyedunmounted实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

其他几个生命周期

生命周期vue2生命周期vue3描述
activatedactivatedkeep-alive专属,组件被激活时调用
deactivateddeactivatedkeep-alive专属,组件被销毁时调用
errorCapturederrorCaptured捕获一个来自子孙组件的错误时被调用
  • | renderTracked | 调试钩子,响应式依赖被收集时调用

  • | renderTriggered | 调试钩子,响应式依赖被触发时调用

  • | serverPrefetch | ssr only,组件实例在服务器上被渲染前调用

    1. 要掌握每个生命周期内部可以做什么事
    • beforeCreate 初始化vue实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
    • created 组件初始化完毕,可以访问各种数据,获取接口数据等
    • beforeMount 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上
    • mounted 实例已经挂载完成,可以进行一些DOM操作
    • beforeUpdate 更新前,可用于获取更新前各种状态。此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
    • updated 完成view层的更新,更新后,所有状态已是最新。可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
    • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件
    • vue3 beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消
    • vue3 unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

    <div id="app">{{name}}</div>
    <script>
        const vm = new Vue({
            data(){
                return {name:'poetries'}
            },
            el: '#app',
            beforeCreate(){
                // 数据观测(data observer) 和 event/watcher 事件配置之前被调用。
                console.log('beforeCreate');
            },
            created(){
                // 属性和方法的运算, watch/event 事件回调。这里没有$el
                console.log('created')
            },
            beforeMount(){
                // 相关的 render 函数首次被调用。
                console.log('beforeMount')
            },
            mounted(){
                // 被新创建的 vm.$el 替换
                console.log('mounted')
            },
            beforeUpdate(){
                //  数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
                console.log('beforeUpdate')
            },
            updated(){
                //  由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
                console.log('updated')
            },
            beforeDestroy(){
                // 实例销毁之前调用 实例仍然完全可用
                console.log('beforeDestroy')
            },
            destroyed(){ 
                // 所有东西都会解绑定,所有的事件监听器会被移除
                console.log('destroyed')
            }
        });
        setTimeout(() => {
            vm.name = 'poetry';
            setTimeout(() => {
                vm.$destroy()  
            }, 1000);
        }, 1000);
    </script>
  1. 组合式API生命周期钩子

你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

下表包含如何在 setup() 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreate不需要*
created不需要*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写

    export default {
      setup() {
        // mounted
        onMounted(() => {
          console.log('Component is mounted!')
        })
      }
    }

setupcreated谁先执行?

  • beforeCreate:组件被创建出来,组件的methodsdata还没初始化好
  • setup:在beforeCreatecreated之前执行
  • created:组件被创建出来,组件的methodsdata已经初始化好了

由于在执行setup的时候,created还没有创建好,所以在setup函数内我们是无法使用datamethods的。所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined

    import { ref } from "vue"
    export default {
      // setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去
      setup(){
        let count = ref(1)
        function myFn(){
          count.value +=1
        }
        return {count,myFn}
      },
      
    }
  1. 其他问题
  • 什么是vue生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
  • vue生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
  • vue生命周期总共有几个阶段? 它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。
  • 第一次页面加载会触发哪几个钩子? 会触发下面这几个beforeCreatecreatedbeforeMountmounted
  • 你的接口请求一般放在哪个生命周期中? 接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created
  • DOM 渲染在哪个周期中就已经完成?mounted中,
    • 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
          mounted: function () {
        this.$nextTick(function () {
            // Code that will run only after the
            // entire view has been rendered
        })
      }

父组件可以监听到子组件的生命周期吗

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

    // Parent.vue
    <Child @mounted="doSomething"/>
        
    // Child.vue
    mounted() {
      this.$emit("mounted");
    }

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

    //  Parent.vue
    <Child @hook:mounted="doSomething" ></Child>
    
    doSomething() {
       console.log('父组件监听到 mounted 钩子函数 ...');
    },
        
    //  Child.vue
    mounted(){
       console.log('子组件触发 mounted 钩子函数 ...');
    },    
        
    // 以上输出顺序为:
    // 子组件触发 mounted 钩子函数 ...
    // 父组件监听到 mounted 钩子函数 ...     

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:createdupdated 等都可以监听

Vue生命周期钩子是如何实现的

  • vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法
  • 内部会对钩子函数进行处理,将钩子函数维护成数组的形式

Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)

    <script>
        // Vue.options 中会存放所有全局属性
    
        // 会用自身的 + Vue.options 中的属性进行合并
        // Vue.mixin({
        //     beforeCreate() {
        //         console.log('before 0')
        //     },
        // })
        debugger;
        const vm = new Vue({
            el: '#app',
            beforeCreate: [
                function() {
                    console.log('before 1')
                },
                function() {
                    console.log('before 2')
                }
            ]
        });
        console.log(vm);
    </script>

相关代码如下

    export function callHook(vm, hook) {
      // 依次执行生命周期对应的方法
      const handlers = vm.$options[hook];
      if (handlers) {
        for (let i = 0; i < handlers.length; i++) {
          handlers[i].call(vm); //生命周期里面的this指向当前实例
        }
      }
    }
    
    // 调用的时候
    Vue.prototype._init = function (options) {
      const vm = this;
      vm.$options = mergeOptions(vm.constructor.options, options);
      callHook(vm, "beforeCreate"); //初始化数据之前
      // 初始化状态
      initState(vm);
      callHook(vm, "created"); //初始化数据之后
      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };
    
    // 销毁实例实现
    Vue.prototype.$destory = function() {
    	 // 触发钩子
        callHook(vm, 'beforeDestory')
        // 自身及子节点
        remove() 
        // 删除依赖
        watcher.teardown() 
        // 删除监听
        vm.$off() 
        // 触发钩子
        callHook(vm, 'destoryed')
    }

原理流程图

Vue 的父子组件生命周期钩子函数执行顺序

  • 渲染顺序 :先父后子,完成顺序:先子后父
  • 更新顺序 :父更新导致子更新,子更新完成后父
  • 销毁顺序 :先父后子,完成顺序:先子后父

加载渲染过程

beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted子组件先挂载,然后到父组件

子组件更新过程

beforeUpdate->子 beforeUpdate->子 updated->父 updated

父组件更新过程

beforeUpdate->父 updated

销毁过程

beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

之所以会这样是因为Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

    function patch (oldVnode, vnode, hydrating, removeOnly) { 
        if (isUndef(vnode)) { 
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return 
        }
        let isInitialPatch = false 
        const insertedVnodeQueue = [] // 定义收集所有组件的insert hook方法的数组 // somthing ... 
        createElm( 
            vnode, 
            insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, 
            nodeOps.nextSibling(oldElm) 
        )// somthing... 
        // 最终会依次调用收集的insert hook 
        invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
        return vnode.elm
    }
    
    function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { 
        // createChildren 会递归创建儿子组件 
        createChildren(vnode, children, insertedVnodeQueue) // something... 
    } 
    // 将组件的vnode插入到数组中 
    function invokeCreateHooks (vnode, insertedVnodeQueue) { 
        for (let i = 0; i < cbs.create.length; ++i) { 
            cbs.create[i](emptyNode, vnode) 
        }
        i = vnode.data.hook // Reuse variable 
        if (isDef(i)) { 
            if (isDef(i.create)) i.create(emptyNode, vnode) 
            if (isDef(i.insert)) insertedVnodeQueue.push(vnode) 
        } 
    } 
    // insert方法中会依次调用mounted方法 
    insert (vnode: MountedComponentVNode) { 
        const { context, componentInstance } = vnode 
        if (!componentInstance._isMounted) { 
            componentInstance._isMounted = true 
            callHook(componentInstance, 'mounted') 
        } 
    }
    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方法 
            } 
        } 
    }
    
    Vue.prototype.$destroy = function () { 
        callHook(vm, 'beforeDestroy') 
        // invoke destroy hooks on current rendered tree 
        vm.__patch__(vm._vnode, null) // 先销毁儿子 
        // fire destroyed hook 
        callHook(vm, 'destroyed') 
    }
Last Updated:
Contributors: leeguooooo