最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 深入理解Vue实例生命周期

    正文概述 掘金(快狗打车前端团队)   2020-12-05   325

    vue实例生命周期与生命周期钩子

    每个 Vue 实例在被创建时都会经过一系列的初始化过程。例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。

    为了让开发者在Vue实例生命周期的不同阶段有机会插入自己的代码逻辑,vue提供了一种叫做生命周期钩子的函数。主要的生命周期钩子如下:

    • beforeCreate
    • created
    • beforeMount
    • mounted
    • beforeUpdate
    • updated
    • activated
    • deactivated
    • beforeDestroy
    • destroyed

    官方提供的vue实例生命周期示意图很好地说明了整个过程。大家可以参考这张图来阅读本文。 深入理解Vue实例生命周期

    初始化

    执行new Vue()后,将进入实例初始化阶段。这个阶段会触发两个钩子:beforeCreatecreated。看下主要代码:

     // Vue构造函数
    function Vue (options) {
      // 实例初始化
      this._init(options)
    }
    
     Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid 为每个vue实例分配一个唯一的id
        vm._uid = uid++
        vm._isVue = true
        
        //...省略部分代码
        
        if (options && options._isComponent) {
          // 合并组件选项对象
          initInternalComponent(vm, options)
        } else {
          // 合并vue选项对象
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
        
        vm._self = vm
        initLifecycle(vm)
        initEvents(vm)
        initRender(vm)
        callHook(vm, 'beforeCreate')
        initInjections(vm) // resolve injections before data/props
        initState(vm)
        initProvide(vm) // resolve provide after data/props
        // 初始化完成后,调用created钩子函数
        callHook(vm, 'created')
        
        //...省略部分代码
    
        // 如果指定了挂载元素,则执行挂载逻辑
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    

    beforeCreate

    _init方法中可以看到,vue会将用户所定义的选项对象与vue构造函数上所预先定义的静态选项对象进行合并。

    调用initLifecycle函数为实例初始化生命周期有关的属性。例如初始化vue实例_isMounted_isDestroyed_inactive等属性的值。

    调用 initEvents 函数为实例初始化事件相关的属性。例如初始化vue实例的_events_hasHookEvent等属性的值。

    调用initRender函数为实例初始化渲染相关的属性。例如初始化vue实例的 _vnode_c()$slots等属性的值。

    由此可知,beforeCreate钩子被调用时,已经初始化完成了lifecycle、render、event;但尚未处理data、props、methods、provide/inject、watch、computed等。

    created

    从代码中能看到,created 钩子被调用时,先后执行了initInjectionsinitStateinitProvide。这几个函数主要是将data、props、inject、methods、computed、watch中所定义的属性使用defineProperty代理到vue实例上。同时会把data、props、inject、computed、watch中的属性全部转换成响应式的。经过这些处理,后面当我们在实例中修改属性值的时候,就会自动触发页面的重绘了。

    由此可知,created钩子被调用时,完成了实例的初始化,实例属性也具备了响应式的能力。但尚未开始DOM元素的挂载。一般在钩子里常见的操作是异步向后端获取数据。

    DOM元素挂载

    实例初始化完成后,会调用$mount开始DOM元素挂载。这个阶段会触发两个钩子函数:beforeMountmounted

    // src/platforms/web/entry-runtime-with-compiler.js
    
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    
    // src/platforms/web/runtime/index.js
    // 覆写$mount方法
    const mount = Vue.prototype.$mount
    
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && query(el)
    
      const options = this.$options
      if (!options.render) {
        
        let template = options.template
        // 选项中指定了template
        if (template) {
          if (typeof template === 'string') {
            // 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板
            if (template.charAt(0) === '#') {
              template = idToTemplate(template)
              if (process.env.NODE_ENV !== 'production' && !template) {
                warn(
                  `Template element not found or is empty: ${options.template}`,
                  this
                )
              }
            }
          } else if (template.nodeType) {
            // 虽然在文档中并未说明,但template还可以指定一个DOM元素作为模板
            template = template.innerHTML
          } else {
            if (process.env.NODE_ENV !== 'production') {
              warn('invalid template option:' + template, this)
            }
            return this
          }
        } else if (el) {
          // 选项中指定了el
          template = getOuterHTML(el)
        }
    
        // 将模板解析成render函数
        if (template) {
          const { render, staticRenderFns } = compileToFunctions(template, {
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          options.render = render
          options.staticRenderFns = staticRenderFns
        }
      }
      
      // ...省略部分代码
      
      return mount.call(this, el, hydrating)
    }
    
    // src/core/instance/lifecycle.js
    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      // 将对DOM元素的引用保存到$el
      vm.$el = el
      
      // ...省略部分代码
    
      // 调用beforeMount前需要执行模板编译逻辑
      callHook(vm, 'beforeMount')
    
      let updateComponent = () => {
        vm._update(vm._render(), hydrating)
      }
      
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      
      hydrating = false
    
      if (vm.$vnode == null) {
        // 标记为已挂载
        vm._isMounted = true
        
        // 触发mounted事件
        callHook(vm, 'mounted')
      }
      return vm
    }
    

    beforeMount

    我们知道vue需要render函数来生成vnode。但是在实际开发中,基本都是通过templateel来指定模板,很少直接提供一个render函数。因此在触发beforeMount前,vue最重要的一个工作就是将HTML模板编译成render函数。beforeMount钩子函数被调用时,我们尚不能访问DOM元素。

    mounted

    每个vue实例都会对应一个render watcherrender watcher 会创建vnode(通过_render方法),并对vnode进行diff后,创建或者更新DOM元素(通过_update方法)。对于初次渲染来说,当创建完DOM元素后,把DOM树的根元素插入到body中,然后触发mounted钩子函数。此时,在钩子函数中可以对DOM元素进行操作了。

    更新

    实例完成初始化和挂载之后,如果由于用户的交互导致实例的状态发生了变化,实例将进入更新阶段。例如在代码中执行 this.msg = 'update msg',vue实例需要更新DOM元素。

    实例的更新是异步的。前面提到过,render watcher 会负责调度程序创建vnode、创建更新DOM元素。当数据发生变化后,vue不会立即启动DOM的更新,而是先把实例对应的render watcher添加到一个队列中。然后在下一个事件循环中,统一执行DOM更新,清空队列。也就是调用下面代码中的flushSchedulerQueue函数。

    此阶段会触发的钩子是:beforeUpdateupdated

    /**
     * 清空所有的队列并执行watcher的更新逻辑
     */
    function flushSchedulerQueue () {
      flushing = true
      let watcher, id
    
      // 队列按照watcher的id升序排序,目的是确保:
      // 1. 组件总是从父向子进行更新
      // 2. 用户创建的watcher先于渲染watcher更新
      // 3. 如果组件在父组件的watcher运行时被销毁,该组件的watcher可以跳过处理
      queue.sort((a, b) => a.id - b.id)
    
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        // 调用watcher.before,触发beforeUpdate钩子
        if (watcher.before) {
          watcher.before()
        }
        id = watcher.id
        has[id] = null
        // 更新dom
        watcher.run()
      }
    
      const activatedQueue = activatedChildren.slice()
      const updatedQueue = queue.slice()
    
      resetSchedulerState()
    
      // 触发activated钩子
      callActivatedHooks(activatedQueue)
      // 触发updated钩子
      callUpdatedHooks(updatedQueue)
    }
    
    // 触发updated钩子
    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) {
          callHook(vm, 'updated')
        }
      }
    }
    
    

    beforeUpdate

    在初始化阶段,vue实例中的数据(data/props/computed/watch)已经被处理成响应式的了。任何对数据的访问(getter)都会被watcher添加为依赖,任何对数据的变更,都将触发对数据有依赖的watcher的更新。watcher在更新之前会调用watcher.before方法,该方法是在挂载阶段创建watcher实例的时候定义的。watcher.before中会触发beforeUpdate钩子。此时vue实例只是确定了最终需要更新的数据,尚未真正开始更新。

    updated

    从代码中可以看到,在触发updated钩子前,vue实例需要对DOM元素进行更新。更新的过程是异步的。具体方式通过实例的render watcher执行run方法。该方法会去调用我们在挂载阶段介绍的updateComponent函数。从而重新创建vnode,并进行vnode的diff操作后更新DOM元素。

    我们还注意到,代码中调用了callActivatedHooks函数,该函数用来触发activated钩子。下文我们再做说明,这里不展开。

    销毁

    当vue实例的$destroy方法时,实例将进入销毁阶段。此时触发的钩子是:beforeDestorydestroyed

      // 销毁Vue实例
      Vue.prototype.$destroy = function () {
        const vm: Component = this
        // 避免重复执行销毁操作
        if (vm._isBeingDestroyed) {
          return
        }
        // 触发实例的beforeDestroy钩子
        callHook(vm, 'beforeDestroy')
        
        vm._isBeingDestroyed = true
        // 将实例从其父实例中的$chilren中移除(断开与父实例的联系)
        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()
        }
    
        if (vm._data.__ob__) {
          vm._data.__ob__.vmCount--
        }
        
        vm._isDestroyed = true
        // 销毁指令、ref等
        vm.__patch__(vm._vnode, null)
        // 触发destroyed事件
        callHook(vm, 'destroyed')
        // 移除实例的所有事件监听器
        vm.$off()
        if (vm.$el) {
          vm.$el.__vue__ = null
        }
        // 释放循环引用(#6759)
        if (vm.$vnode) {
          vm.$vnode.parent = null
        }
      }
    }
    

    beforeDestroy

    从代码中可以看到,$destroy被调用时,在执行实际的销毁动作前触发beforeDestroy。此时,由于并未开始执行实际的销毁代码,实例及DOM元素仍可正常访问。

    destroyed

    从代码中可以看到,销毁操作主要包括以下几点:清空所有的watcher、删除所有的指令、删除DOM元素并关闭DOM上的所有的事件、断开与父实例之间的关系、将实例标记成已销毁状态。此时实例已经被销毁,已经无法访问实例的属性和DOM元素了。

    keep-alive包裹下的组件的生命周期钩子

    下面的两个钩子只有当组件包裹在keep-alive时才会触发。

    activated

    HTML标签和组件标签在vue内部实现中都有对应的vnode,组件vnode在设计上与普通的HTML标签的vnode有所不同。例如组件vnode上包含initprepatchinsertdestroy等钩子。这些钩子在组件实例初始化、更新和销毁等不同的阶段进行调用。

    // 组件vnode的钩子
    const componentVNodeHooks = {
        // ...省略其他钩子
      insert (vnode: MountedComponentVNode) {
        const { context, componentInstance } = vnode
        if (!componentInstance._isMounted) {
          componentInstance._isMounted = true
          // 触发组件的mounted钩子
          callHook(componentInstance, 'mounted')
        }
        if (vnode.data.keepAlive) {
          if (context._isMounted) {
            queueActivatedComponent(componentInstance)
          } else {
            activateChildComponent(componentInstance, true /* direct */)
          }
        }
      }
    }
    
    // 触发activated钩子
    export function activateChildComponent (vm: Component, direct?: boolean) {
    
      // ...省略部分代码
      
      if (vm._inactive || vm._inactive === null) {
        vm._inactive = false
        for (let i = 0; i < vm.$children.length; i++) {
          activateChildComponent(vm.$children[i])
        }
        // 调用实例的activated钩子
        callHook(vm, 'activated')
      }
    }
    

    与根实例一样,keep-alive包裹下的组件实例初始化时同样会依次经历初始化阶段、挂载阶段,但在挂载阶段之后会调用组件vnode的insert钩子,insert钩子会触发组件实例的activated钩子。因为insert 是在组件实例挂载完成后调用的,所以mounted的触发早于activated

    当组件切换回来的同时,组件的数据发生了变化,此时组件将进入更新阶段,意味着将会依次触发beforeUpdateupdated钩子。

    那么问题来了,activatedbeforeUpdateupdated钩子哪个先触发呢?

    答案是先触发beforeUpdate,再触发activated,最后触发updated

    我们回顾下前面更新阶段的代码:

    // 触发activated钩子
    callActivatedHooks(activatedQueue)
    // 触发updated钩子
    callUpdatedHooks(updatedQueue)
    
    //...省略部分代码
    
    // 触发activated钩子
    function callActivatedHooks (queue) {
      for (let i = 0; i < queue.length; i++) {
        queue[i]._inactive = true
        activateChildComponent(queue[i], true /* true */)
      }
    }
    
    

    可以看到在函数flushSchedulerQueue中,callActivatedHooks函数用来触发activated。而callActivatedHooks的顺序在callUpdatedHooks前面,所以activated钩子的触发早于updated钩子。

    deactivated

    const componentVNodeHooks = {
      // ... 省略其他钩子
      destroy (vnode: MountedComponentVNode) {
        const { componentInstance } = vnode
        if (!componentInstance._isDestroyed) {
          // 未被keep-alive包裹销毁组件
          if (!vnode.data.keepAlive) {
            componentInstance.$destroy()
          } else {
            // 被keep-alive包裹
            deactivateChildComponent(componentInstance, true /* direct */)
          }
        }
      }
    }
    
    export function deactivateChildComponent (vm: Component, direct?: boolean) {
      if (direct) {
        vm._directInactive = true
        if (isInInactiveTree(vm)) {
          return
        }
      }
      if (!vm._inactive) {
        vm._inactive = true
        for (let i = 0; i < vm.$children.length; i++) {
          deactivateChildComponent(vm.$children[i])
        }
        // 调用实例的deactivated钩子
        callHook(vm, 'deactivated')
      }
    }
    

    当组件被切换到其他的组件时,会调用组件vnode的destroy钩子,在组件被keep-alive包裹的情况下,只会将组件对应的DOM元素从DOM树删除,但不会销毁组件实例,此时会调用deactivateChildComponent 从而触发deactivated钩子。组件在没有被keep-alvie包裹的情况下,才会调用$destroy销毁组件实例,触发beforeDestroydestroyed钩子。

    总结

    关于vue实例的生命周期,官网讲解的其实是比较简单易懂的。本文主要还是希望能从源码的角度,进一步让大家理解每个生命周期做了什么处理。更好地理解钩子的触发时机及先后顺序。欢迎大家留言讨论~

    关注我们

    深入理解Vue实例生命周期


    起源地下载网 » 深入理解Vue实例生命周期

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元