解析vue2.x源码之组件的生命周期

基本概念介绍:

在日常开发中,我们会在组件的生命周期函数内编写代码,等待组件在适当的时机调用。

生命周期函数有: beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestory、destoryd。

那么组件是在什么时候调用这些生命周期函数的?在调用生命周期函数时又分别做了什么呢?

created:

在vue实例被创建时,vue构造函数会调用_init方法,进行一系列初始化操作,并调用beforeCreate、created函数

function Vue (options) {
  // 限制Vue构造函数只能用 new Vue(options) 方式调用
  // 此时 this 是 Vue构造函数的实例,(this instanceof Vue) 为true
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 进行Vue实例的初始化
  this._init(options)
}
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created') // 调用created钩子

    // 如果用户在实例化vue时传入了el选项,则自动调用vm.$mount方法,开启模板编译与挂载
    // 如果没有传el,则需要等用户自己手动调用vm.$mount方法,开启模板编译与挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

在源码中可看到,在beforeCreate被调用前,执行了函数 initLifecycle(vm),initEvents(vm),initRender(vm).

这里重点 看一下initLifecycle,initEvents

initLifecycle:

export function initLifecycle (vm: Component) {
  const options = vm.$options

  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

  // 初始化一系列实例属性
  vm.$children = []
  vm.$refs = {}
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

initEvents:

initEvents主要是将父组件监听子组件的事件加到子组件的_event属性上,等子组件$emit时,取出父组件的回调函数进行调用。

function initEvents (vm) {
  vm._events = Object.create(null);
  vm._hasHookEvent = false;
  // vm.$options._parentListeners 是父组件监听子组件的事件
  var listeners = vm.$options._parentListeners;
  if (listeners) {
    // 将父组件监听事件添加到子组件的_event属性
    updateComponentListeners(vm, listeners);
  }
}

在created被调用前,执行了函数initInjections(vm),initState(vm),initProvide(vm)

这里重点 看一下initState, initState初始化了vm实例的 props、methods、data、computed、watch

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);
  }
}

initProps:

initProps中将父组件传入的props数据,值赋值给vm._props 。

并且做了代理,使我们可以通过vm.key访问到vm._props.key

function initProps (vm, propsOptions) {
  // propsData 为父组件中传入的 prop数据
  var propsData = vm.$options.propsData || {};
  var props = vm._props = {};
  var keys = vm.$options._propKeys = [];
  var isRoot = !vm.$parent;
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false);
  }
  var loop = function ( key ) {
    keys.push(key);
    // 从propsData 中取出当前key对应的值
    var value = validateProp(key, propsOptions, propsData, vm);
    /* istanbul ignore else */
    {
      var hyphenatedKey = hyphenate(key);
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      // 将父组件传入的prop值赋值给vm._props
      defineReactive$$1(props, key, value, function () {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            "Avoid mutating a prop directly since the value will be " +
            "overwritten whenever the parent component re-renders. " +
            "Instead, use a data or computed property based on the prop's " +
            "value. Prop being mutated: \"" + key + "\"",
            vm
          );
        }
      });
    }
    // 代理 访问vm.key时,返回vm._props.key
    if (!(key in vm)) {
      proxy(vm, "_props", key);
    }
  };

  for (var key in propsOptions) loop( key );
  toggleObserving(true);
}

initMethods:

initMethods对函数名称做了校验,并且将methods,赋值到vm上,使得我们可以通过vm.methods直接访问

function initMethods (vm, methods) {
  var props = vm.$options.props;
  // 校验
  for (var key in methods) {
    {
      // 不是函数
      if (typeof methods[key] !== 'function') {
        warn(
          "Method \"" + key + "\" has type \"" + (typeof methods[key]) + 
          "\" in the component definition. " +
          "Did you reference the function correctly?",
          vm
        );
      }
      // 与prop中的属性有重名
      if (props && hasOwn(props, key)) {
        warn(
          ("Method \"" + key + "\" has already been defined as a prop."),
          vm
        );
      }
      // 与现有的vue实例方法有重名
      if ((key in vm) && isReserved(key)) {
        warn(
          "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
          "Avoid defining component methods that start with _ or $."
        );
      }
    }
    // 将方法赋值给vm
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
  }
}

initData:

initMethods先是对data做了校验,然后对data做了代理,使得我们可以通过vm.data直接访问,最后将data转为响应式数据

function initData (vm) {
  //取出data
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
    warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    );
  }
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  // 遍历data
  while (i--) {
    var key = keys[i];
    {
      // 如果methods中有重名属性
      if (methods && hasOwn(methods, key)) {
        warn(
          ("Method \"" + key + "\" has already been defined as a data property."),
          vm
        );
      }
    }
    // 如果props中有重名属性
    if (props && hasOwn(props, key)) {
      warn(
        "The data property \"" + key + "\" is already declared as a prop. " +
        "Use prop default value instead.",
        vm
      );
    } else if (!isReserved(key)) {
      // 如果与实例方法名没冲突,则代理当前key,使vm.key返回 vm._data.key
      proxy(vm, "_data", key);
    }
  }
  // 将data转为响应式数据
  observe(data, true /* asRootData */);
}

initComputed:

我们都知道computed与watch最大的区别在于computed具有缓存性,当computed所依赖的状态不发生改变时,模板重新渲染,computed会使用上次缓存的值,而不会重新计算。

其实computed底层也是使用了watcher实现,只不过他是一个懒watcher(lazy属性为true)。


// 用于实例化lazy属性为true的watcher
var computedWatcherOptions = { lazy: true };

function initComputed (vm, computed) {
  // 取出computed的watcher数组
  var watchers = vm._computedWatchers = Object.create(null);
  // 判断是否为服务端渲染,如果是服务端渲染则不缓存
  var isSSR = isServerRendering();

  // 遍历computed
  for (var key in computed) {
    var userDef = computed[key];
    // 取computed得getter
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
    if (getter == null) {
      warn(
        ("Getter is missing for computed property \"" + key + "\"."),
        vm
      );
    }
    // 不是服务端渲染,需要缓存
    if (!isSSR) {
      // 实例化watcher,使computed监听getter中的变量
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
    }

    // 代理computed
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    } else {
      if (key in vm.$data) {
        warn(("The computed property \"" + key + "\" is already defined in data."), vm);
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(("The computed property \"" + key + "\" is already defined 
        as a prop."), vm);
      }
    }
  }
}

function defineComputed (
  target,
  key,
  userDef
) {
  var shouldCache = !isServerRendering();
  if (typeof userDef === 'function') {
    // 重写computed的getter
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  } else {
    // 重写computed的getter
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop;
    sharedPropertyDefinition.set = userDef.set || noop;
  }
  if (sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        ("Computed property \"" + key + "\" was assigned to but it has no setter."),
        this
      );
    };
  }
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

function createComputedGetter (key) {
  return function computedGetter () {
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      // 当getter被触发时,判断dirty是否为true
      // true则重写计算,false则无需计算
      if (watcher.dirty) {
        watcher.evaluate();
      }

      // 如果当前有正在处理的watcher
      // 有的话一般都是 组件的watcher,让组件watcher也监听 computed的watcher所监听的状态
      // 这样当状态发生改变时,会想组件watcher与computed的watcher都发送通知
      if (Dep.target) {
        watcher.depend();
      }
      // 返回值
      return watcher.value
    }
  }
}

function createGetterInvoker(fn) {
  return function computedGetter () {
    // 调用fn
    return fn.call(this, this)
  }
}

// 相关代码
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    // 当computed依赖的状态变化时,update方法被调用,dirty 属性会设为true
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

// 等待组件读取computed, 触发getter
// getter判断,如果dirty为true,则调用evaluate重新计算值,并将dirty设为false
Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};

从源码中可以看出computed是如何实现缓存功能的,重点在于computed的getter,computed的getter中使组件的watcher监听 computed的watcher所监听的状态,所以当状态发生变化时会给组件和computed发送通知,此时computed将watcher的dirty设为true,而组件收到通知触发重新渲染,读取computed的getter,发现computed的的dirty为true,触发重新计算,并将watcher的dirty设为false。如果状态没有发生变化,组件重新渲染,computed的watcher的dirty设为false,则直接返回缓存的值。

initWatch:

initWatch其实是用了$watch实现,底层也是实例化了一个watcher。

function initWatch (vm, watch) {
  for (var key in watch) {
    var handler = watch[key];
    // 回调函数是数组,则遍历调用createWatcher
    if (Array.isArray(handler)) {
      for (var i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}

// 调用$watch实现监听
function createWatcher (
  vm,
  expOrFn,
  handler,
  options
) {
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === 'string') {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options)
}

初始化完这一系列状态后,vue会调用created钩子。

如果用户在实例化vue时传入了el选项,则自动调用vm.$mount方法,进入mounted阶段,如果没有传el,则需要等用户自己手动调用vm.$mount方法。

mounted:

vue生命周期挂载阶段做了些什么事情呢?这里得分为vue的完整版与运行时版本。

完整版与运行时版本的区别在于,完整版包括模板编译 即template转render函数 的过程,而运行时版本没有。

如果你在项目里使用了vue-loader或vueify时,vue文件中的模板会在构建时编译成render函数,用运行时版本即可。

由于完整版功能包括了运行时版本,这里先看一下运行时版本的挂载阶段源码

// 当调用vm.$mount时进入挂载阶段
Vue.prototype.$mount = function (
  el,
  hydrating
) {
  // 找到模板要挂载的DOM节点
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

function query (el) {
  if (typeof el === 'string') {
    var selected = document.querySelector(el);
    if (!selected) {
      warn(
        'Cannot find element: ' + el
      );
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

function mountComponent (
  vm,
  el,
  hydrating
) {
  vm.$el = el;
  // 如果没有render函数,为了代码的健壮性,给render一个生产空vnode的函数
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
    {
      /* 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'); // 调用beforeMount钩子


  // 定义组件的watcher,当模板中依赖的状态改变时,触发vm_update重新渲染
  new Watcher(vm, function () {
      vm._update(vm._render(), hydrating);
    }, 
    noop, {
    // 状态变化时触发回调,在这之前调用beforeUpdate钩子
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
  hydrating = false;

  callHook(vm, 'mounted'); // 调用mounted钩子
  return vm
}

可以看到,在beforeMount与mounted之间,定义了一个组件的watcher,用于监听组件模板中所依赖的变量,变量发生变化时,模板会重新渲染。

了解运行时挂载阶段的过程后,我们再看看完整版的挂载阶段。

完整版中对原来的$mount做了函数劫持,在挂载之前加了模板编译的过程。

// 保存原有$mount
var mount = Vue.prototype.$mount;
// 重写$mount 方法
Vue.prototype.$mount = function (
  el,
  hydrating
) {
  el = el && query(el);

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    warn(
      "Do not mount Vue to  or  - mount to normal elements instead."
    );
    return this
  }

  var options = this.$options;
  // 如果没有render函数,则需要vue进行模板编译
  if (!options.render) {
    // 下面一大串代码都是为了取到模板template
    var template = options.template;
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // 如果是id,就找到DOM,再取DOM的innerHtml
          template = idToTemplate(template);
          /* istanbul ignore if */
          if (!template) {
            warn(
              ("Template element not found or is empty: " + (options.template)),
              this
            );
          }
        }
      } else if (template.nodeType) {
        // DOM类型
        template = template.innerHTML;
      } else {
        {
          warn('invalid template option:' + template, this);
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el);
    }
    if (template) {
      /* istanbul ignore if */
      if (config.performance && mark) {
        mark('compile');
      }
      // 调用compileToFunctions将模板转为render函数
      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;

      /* istanbul ignore if */
      if (config.performance && mark) {
        mark('compile end');
        measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
      }
    }
  }
  // 调用原有$mount,完成函数劫持
  return mount.call(this, el, hydrating)
};

具体编译过程可看:解析vue2.x源码之模板编译

从源码中可以看到,完整版的版本中,在created 与 beforeMount 之间,经历了模板编译的阶段,将template转化成可生产vnode的render函数。

updated:

在上面的挂载阶段源码,其实我们已经提到了updated钩子函数的调用

  // 定义组件的watcher,当模板中依赖的状态改变时,触发vm_update重新渲染
  new Watcher(vm, function () {
      vm._update(vm._render(), hydrating);
    }, 
    noop, {
    // 状态变化时触发回调,在这之前调用beforeUpdate钩子
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);

每次组件接收到状态变化的通知,都会先调用beforeUpdate钩子函数,再调用watcher实例的update方法,执行回调函数。

Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    // queueWatcher方法会执行回调函数,并触发updated钩子
    queueWatcher(this);
  }
};

destoryd:

当$destory被调用时,组件开始销毁,调用destoryd相关钩子函数。

  Vue.prototype.$destroy = function () {
    const vm: Component = this
    // 如果已经在销毁过程中,则return,以免多次调用
    if (vm._isBeingDestroyed) {
      return
    }
    // 调用组件beforeDestroy钩子中方法
    callHook(vm, 'beforeDestroy')
    // 标识组件正在销毁中
    vm._isBeingDestroyed = true
    // 讲组件从父组件中移除
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // vm._watcher 是当前组件渲染函数的watcher
    // vm._watchers 是用户自定义的watcher
    // 详情可见watcher构造函数代码

    // 释放组件中的状态所收集的依赖
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    // 释放用户所定义的watcher中状态收集的依赖
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }

    // 标识已销毁
    vm._isDestroyed = true
    // 调用__patch__,发现新vnode为null
    // 说明组件已销毁,这时触发vnode树的destory钩子函数
    vm.__patch__(vm._vnode, null)
    // 调用组件destroyed钩子中方法
    callHook(vm, 'destroyed')
    // 调用实例方法的$off方法,且不传任何参数,这时会将所有监听都取消,_event清空
    vm.$off()
  }

 

你可能感兴趣的:(vue,前端,源码)