从源码了解Vue生命周期

从源码了解Vue生命周期

渣新一枚,近期打算离职,去好点的城市发展,而后发现写了很多业务代码,真正有用的又一问三不知。 虽然一直都在用Vue,但总以为好像只是背了背文档,成了一个Api调用师。基于职业发展焦虑, 打算好好学习学习,把看到的东西总结下,即使快过期了,面试也可能会用到。既方便之后复习,也分享给你们,有错还请指正,写的很差望海涵。。vue

生命周期概览[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前会渲染一次,每次updated前也会渲染一次)
  8. deactivated // keep-alive缓存组件销毁后调用
  9. beforeDestroy
  10. destroyed
  11. errorCaptured

咱们再来看看源码中是如何一步一步建立一个Vue实例的node

new Vue(options)

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

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

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

Vue.prototype._init

_init简要代码以下(已省略于本文无关代码)面试

Vue.prototype._init = function ( options ) {
 var vm = this;  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 ); // init props methods data computed watch  initProvide( vm ); // resolve provide after data/props  callHook( vm, 'created' );   if ( vm.$options.el ) {  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存在时将会在子组件内进行事件注册。ide

这里须要注意的是,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(都作了响应式处理)挂载到了子组件实例中,主要是为了高阶组件的使用。oop

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 () {  warn(  "Avoid mutating an injected value directly since the changes will be " +  "overwritten whenever the provided component re-renders. " +  "injection being mutated: \"" + key + "\"",  vm  );  } );  }  } );  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 watch属性。

initProvide

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

initProvide只作了一件事,将组件提供的Provide选项保存到私有属性_provided中。

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 Watche 的before中调用了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。

beforeDestroy destoryed deactivated

调用$destory时触发beforeDestroydeactivated,完成数据销毁后调用destoryed

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

本文使用 mdnice 排版

相关文章
相关标签/搜索