从源码了解Vue生命周期

991 阅读3分钟

从源码了解Vue生命周期

渣新一枚,近期打算离职,去好点的城市发展,然后发现写了不少业务代码,真正有用的又一问三不知。 虽然一直都在用Vue,但总觉得好像只是背了背文档,成了一个Api调用师。基于职业发展焦虑, 打算好好学习学习,把看到的东西总结下,方便以后复习,也分享给大家,有错还请指正,写的不好望海涵。

生命周期概览[2.x]

  1. beforeCreate // 调用该生命周期前已初始化生命周期,事件和渲染函数,不能访问到props等属性
  2. created // 调用该生命周期前已顺序初始化具体的数据—— injections => props => methods => data => computed => watch => initProvide
  3. beforeMount // 调用该生命周期前已初始化渲染函数$options.render
  4. mounted // 调用该生命周期前已渲染真实节点
  5. beforeUpdate // 状态改变时,会在nextTick中更新视图前调用
  6. updated // 已调用render函数重新渲染
  7. activated // keep-alive缓存组件渲染时调用 [首次加载时在 mounted 之后]
  8. deactivated // keep-alive缓存组件销毁后调用
  9. beforeDestroy // 实例销毁前调用
  10. destroyed // 实例销毁后调用
  11. errorCaptured

我们再来看看源码中是如何一步一步创建一个Vue实例的

new Vue(options)

new Vue(options)显示创建一个Vue实例,传入实例配置属性options,我们就来看看Vue构造函数做了什么?

  function Vue ( options ) {
    if ( !( this instanceof Vue ) ) {
      warn( 'Vue is a constructor and should be called with the `new` keyword' );
    }
    this._init( options );
  }

Vue构造函数就只做了一件事,调用内部方法_init() , _init是混入Vue原型对象上的一个方法,我们继续往下看。

Vue.prototype._init

_init简要代码如下(已省略于本文无关代码),记住_init里调用的方法也就大概能记住Vue实例化的过程了。这一步可以多看看

Vue.prototype._init = function ( options ) {
  var vm = this;
  /////////////////////////////////////
  //  根据合并策略完成配置选项合并   //
  ////////////////////////////////////
  vm.$options = mergeOptions(
    resolveConstructorOptions( vm.constructor ),
    options || {},
    vm
  );

  vm._self = vm;
  initLifecycle( vm );  // 初始化$parent, $root, 生命周期相关属性 
  initEvents( vm );     // 挂载父组件传入的事件监听
  initRender( vm );     // 挂载$createElement, $attrs, $listener
  callHook( vm, 'beforeCreate' );
  initInjections( vm ); // 初始化 injections [在data 和 props 之前]
  initState( vm ); // 初始化 props methods data computed watch 属性
  initProvide( vm ); // 初始化 provide [在data 和 props 之后]
  callHook( vm, 'created' );

  if ( vm.$options.el ) { // 存在el时 主动挂载 否则 手动使用$mount
    vm.$mount( vm.$options.el );
  }
}

_init函数中,mergeOptions会将传入的配置和诸如Mixin配置按照默认或用户自定义的合并策略进行合并。 配置项合并完成后,就可以开始初始化组件数据了。

initLifecycle

  function initLifecycle ( vm ) {
    var options = vm.$options;

    // locate first non-abstract parent
    var 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;

    vm.$children = [];
    vm.$refs = {};

    vm._watcher = null;
    vm._inactive = null;
    vm._directInactive = false;
    vm._isMounted = false;
    vm._isDestroyed = false;
    vm._isBeingDestroyed = false;
  }

initLifecycle中首先循环查找当前组件的父级(非抽象),并挂在到$parent属性上,然后初始化了其他的一些属性的默认值。 说明beforeCreate此时已经可以访问$parent$root

initEvents

 function initEvents ( vm ) {
  vm._events = Object.create( null );
  vm._hasHookEvent = false;
  // init parent attached events
  var listeners = vm.$options._parentListeners;
  if ( listeners ) {
    updateComponentListeners( vm, listeners );
  }
}

initEvents也不复杂,初始化内部属性并获取父组件注册到该组件上的事件并初始化(updateComponentListeners)。 其中_parentListeners是父组件传入的事件监听,_parentListeners存在时将会在子组件内进行事件注册。

这里需要注意的是,initEvents初始化的是父组件传入的事件监听。

initRender

function initRender ( vm ) {
    var options = vm.$options;
    var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree
    var renderContext = parentVnode && parentVnode.context;
    vm.$slots = resolveSlots( options._renderChildren, renderContext );
    vm.$scopedSlots = emptyObject;

    vm._c = function ( a, b, c, d ) { return createElement( vm, a, b, c, d, false ); };
    vm.$createElement = function ( a, b, c, d ) { return createElement( vm, a, b, c, d, true ); };

    // $attrs & $listeners are exposed for easier HOC creation.
    // they need to be reactive so that HOCs using them are always updated
    var parentData = parentVnode && parentVnode.data;
    {
      defineReactive$1( vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {
        !isUpdatingChildComponent && warn( "$attrs is readonly.", vm );
      }, true );
      defineReactive$1( vm, '$listeners', options._parentListeners || emptyObject, function () {
        !isUpdatingChildComponent && warn( "$listeners is readonly.", vm );
      }, true );
    }
  }

这里可以看到,initRender初始化一些属性,其中主要的函数$createElement已经能在实例中访问。代码的最后几行中, 还将父组件的attrslisteners(都做了响应式处理)挂载到了子组件实例中,主要是为了高阶组件的使用。

callHook( vm, 'beforeCreate' )

在调用beforeCreate前,vue实例已经能访问$parent, $root, $listeners, $attrs, $createElement, $slots属性了。 这些属性大部分都是从父组件中传入的。

initInjections(vm)

function initInjections ( vm ) {
  var result = resolveInject( vm.$options.inject, vm );
  if ( result ) {
    toggleObserving( false );
    Object.keys( result ).forEach( function ( key ) {
      /* istanbul ignore else */
      {
        defineReactive$1( vm, key, result[key], function () {
          // ...
        } );
      }
    } );
    toggleObserving( true );
  }
}

initInjections仅仅对inject选项做处理,resolveInject函数内部会层层遍历父节点,查找所有注入的属性并将inject相关属性转换成键值对的形式。 拿到键值对后,再逐一将这些注入属性挂载到当前实例下。

需要注意的是,挂载前执行了toggleObserving函数,传入false时,后续绑定的属性将不会主动设置为响应式,也就是说,inject属性通常都并非响应的(除非它本身就是响应式)。 inject初始化后再恢复响应式绑定 => toggleObserving( true )

initState

  function initState ( vm ) {
    vm._watchers = [];
    var 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 );
    }
  }

initState(在created前)中初始化了许多当前实例的属性props methods data computed watchcreated钩子中自然能够访问这些属性了。 值得一提的是props是首个完成初始化的,所以我们在data函数中能够访问到props

initProvide

function initProvide ( vm ) {
    var provide = vm.$options.provide;
    if ( provide ) {
      vm._provided = typeof provide === 'function'
        ? provide.call( vm )
        : provide;
    }
  }

initProvide只做了一件事,将组件提供的Provide选项保存到私有属性_provided中。 这里我们而可能会有疑问,injectionprovide 为什么没有成双成对的出现,而在其中穿插了initState呢?

injection 属性是从父组件注入的,data, computed等属性很有可能会引用,自然放在initState之前。 provide是将自身属性提供给子组件,也就需要data属性都初始化完成后才能执行。 这也印证了父->子->孙的关系

callHook( vm, 'created' )

至此,我们已经能访问绝大部分的组件属性了,如data, props, methods, inject

vm.$mount

_init函数最后一步就是挂载实例到DOM中,用到的就是$mount函数。

$mount涉及到的代码可能有些多,篇幅问题,我们就看看主要步骤。

Vue.prototype.$mount = function (el, hydrating) {
if ( !options.render ) {
      var template = options.template;
      if ( template ) {
        // 对template模板进行校验处理
      } else if ( el ) {
        template = getOuterHTML( el );
      }
      if ( template ) {
        var ref = compileToFunctions( template, {
          outputSourceRange: "development" !== 'production',
          shouldDecodeNewlines: shouldDecodeNewlines,
          shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        }, this );
        var render = ref.render;
        var staticRenderFns = ref.staticRenderFns;
        options.render = render;
        options.staticRenderFns = staticRenderFns;
      }
    }
    return mount.call( this, el, hydrating )
  };

$mount根据手写render函数template选项和el选项决定如何渲染挂在实例。 其中比较主要的就是compileToFunctions函数。 我们看到该函数传入了template并返回了render,staticRenderFns等函数。 很容易猜到Vue在这里对模板进行了解析。返回的render也就是我们常说的渲染函数。

模板转换为渲染函数有三个步骤:

  1. 模板解析 template => AST
  2. 优化 => 标记静态节点
  3. 转化为渲染函数

有渲染函数后,我们就能挂在到真实节点上了mount.call( this, el, hydrating )

mount

mount function ( el, hydrating ) {
  el = el && inBrowser ? query( el ) : undefined;
  return mountComponent( this, el, hydrating )
};

function mountComponent ( vm,  el, hydrating ) {
   vm.$el = el;
   if ( !vm.$options.render ) {
     vm.$options.render = createEmptyVNode;
   }
   callHook( vm, 'beforeMount' );
   ...
 }

mountComponent 函数中,确保了render函数存在。紧随其后调用了beforeMount生命周期。 我们回顾以下createdbeforeMount,它们之间进行了模板编译,优化,转换为渲染函数。

// 紧接刚才的代码
  var updateComponent;
  updateComponent = function () {
    vm._update( vm._render(), hydrating );
  };
  new Watcher( vm, updateComponent, noop, {
    before: function before () {
      if ( vm._isMounted && !vm._isDestroyed ) {
        callHook( vm, 'beforeUpdate' );
      }
    }
  }, true /* isRenderWatcher */ );
  hydrating = false;

  if ( vm.$vnode == null ) {
    vm._isMounted = true;
    callHook( vm, 'mounted' );
  }
  return vm

这里主要是初始化了Watcher实例,实例中传入了updateComponent函数。 该函数后续会对比虚拟Dom并更新视图。初始化了Watcher时就会执行一次updateComponent。 然后callHook( vm, 'mounted' )。 所以mounted阶段已经能访问真实DOM了。

beforeUpdate updated

new Watcher( vm, updateComponent, noop, {
    before: function before () {
      if ( vm._isMounted && !vm._isDestroyed ) {
        callHook( vm, 'beforeUpdate' );
      }
    }
  }, true /* isRenderWatcher */ );

new Watchebefore中调用了beforeUpdate,顺着before,我找到了flushSchedulerQueue函数。

  function flushSchedulerQueue () {
    currentFlushTimestamp = getNow();
    flushing = true;
    var watcher, id;
    ...
    // do not cache length because more watchers might be pushed
    // as we run existing watchers
    for ( index = 0; index < queue.length; index++ ) {
      watcher = queue[index];
      if ( watcher.before ) {
        watcher.before();
      }
      ...
      watcher.run();
      ...
    }
    ... 
    // call component updated and activated hooks
    callActivatedHooks( activatedQueue );
    callUpdatedHooks( updatedQueue );
    ...
  }

当需要更新依赖/状态时($forceUpdate, 数据读取变动),flushSchedulerQueue就会被触发。

flushSchedulerQueue先遍历了watcher,调用了before(也就是生命周期beforeUpdate), 然后执行callActivatedHook函数,该函数调用其子组件activated钩子, 最后再调用callUpdatedHooks也就是updated钩子。

activated

来看看activated, keep-alive缓存的组件激活时调用

componentVNodeHooks: {
  insert: function insert ( vnode ) {
    var context = vnode.context;
    var componentInstance = vnode.componentInstance;
    if ( !componentInstance._isMounted ) {
      componentInstance._isMounted = true;
      callHook( componentInstance, 'mounted' );
    }
    if ( vnode.data.keepAlive ) {
      if ( context._isMounted ) {
        queueActivatedComponent( componentInstance );
      } else {
        activateChildComponent( componentInstance, true /* direct */ );
      }
    }
  },
...
}

以上代码可以看到,mounted调用后会判断是否为缓存组件并调用activated。 所以首次加载activated钩子是在mounted之后调用的。

一般组件失去活性(切换页面等)会直接$destory, keep-alive组件却调用

beforeDestroy destoryed deactivated

componentVNodeHooks: {
destroy: function destroy ( vnode ) {
    var componentInstance = vnode.componentInstance;
    if ( !componentInstance._isDestroyed ) {
      if ( !vnode.data.keepAlive ) {
        componentInstance.$destroy();
      } else {
        deactivateChildComponent( componentInstance, true /* direct */ );
      }
    }
  }
}

对虚拟DOM(VNode)的对比中,如果判定该组件不存在,普通组件将会调用$destroy(),完全销毁示例, keep-alive组件却会缓存起来, 下一次添加该实例时,keep-alive组件的_isMounted属性仍为true, 就会跳过$mount阶段,直接调用activateChildComponent进入激活态。

非首次加载时,keep-alive缓存的组件会跳过mounted直接调用activated钩子

好记性不如烂笔头,看了很快就忘了,还是记一记能有个好的思路,也方便复习。

本文使用 mdnice 排版