美文网首页
vue生命周期图(探究源码之路)

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

作者: scrollHeart | 来源:发表于2021-03-30 00:33 被阅读0次

    学习主线:从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

    相关文章

      网友评论

          本文标题:vue生命周期图(探究源码之路)

          本文链接:https://www.haomeiwen.com/subject/egzihltx.html