vue源码阅读——vue生命周期

这是官网上一个最简单的例子

{{ message }}

接下来我通过在chrome中一步步运行代码来理清其内部逻辑。
首先当然是调用构造函数

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options) // 主要就是这个步骤,调用原型中的_init方法
}

//下面的这些方法都是给Vue.prototype增加方法和属性的,具体意义看vue官网api就能了解
initMixin(Vue) // 给原型增加了_init()
stateMixin(Vue) // 增加$data,$props,$set(),$delete(),$watch()
eventsMixin(Vue) // 增加$on(),$once(),$off(),$emit()
lifecycleMixin(Vue) // 增加_update(),$forceUpdate(),$destroy()
renderMixin(Vue) // 增加_render()

然后就进入_init()方法了

var uid$1 = 0;

function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    var vm = this; // // 定义一个vm指向Vue实例(这里就是app)
    // a uid
    vm._uid = uid$1++;

    var startTag, endTag;
    /* istanbul ignore if */
    if ("development" !== 'production' && config.performance && mark) {
      startTag = "vue-perf-init:" + (vm._uid);
      endTag = "vue-perf-end:" + (vm._uid);
      mark(startTag);
    }

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) { // 这个例子不是组件,所以先跳过
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options);
    } else {
      // 这个步骤炒鸡重要,给vm增加了一个$options属性,这个属性是合并之后的options。详细内容看¹mergeOptions
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    /* istanbul ignore else */
    {
      initProxy(vm);
    }
    // expose real self
    vm._self = vm;
    // 下面这些都是和vue生命周期有关的一些操作了
    initLifecycle(vm) // ²初始化生命周期
    initEvents(vm) // ³初始化事件
    initRender(vm) // ⁴初始化render
    callHook(vm, 'beforeCreate') // ⁵beforeCreate
    initInjections(vm) // ⁶初始化inject
    initState(vm) // ⁷对data/props/computed/watch等进行监听
    initProvide(vm) // ⁸初始化provide
    callHook(vm, 'created') // ⁹create

    /* istanbul ignore if */
    if ("development" !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(((vm._name) + " init"), startTag, endTag);
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el); // ¹⁰挂载
    }
  };
}

1.mergeOptions
这个方法顾名思义就是合并分支的。那我们先看没合并之前的vm

vue源码阅读——vue生命周期_第1张图片
mergeOptions执行前

再看这个方法运行之后变成啥样了


vue源码阅读——vue生命周期_第2张图片
mergeOptions执行后

明显能注意到的就是,vm.$options比之前的options多了components,directives,filters,_base,然后data变成一个方法了。因此我们大概知道这个方法就是添加vm.$options然后把基础的options和传入的options合并(其实看函数名就差不多知道了)

2.initLifecycle

vue源码阅读——vue生命周期_第3张图片
initLifecycle执行前
vue源码阅读——vue生命周期_第4张图片
initLifecycle执行后

对比很明显,增加了很多属性,来看一眼代码

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

  // 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 // 没有父实例的话把根实例设成自己了

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

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

看样子这个函数是很好理解的,主要目的是为了给vm增加$children、$parent、$refs、$root。然后以下划线开始的这些属性看命名就能才出来是与vue生命周期有关的一些变量(下面我把以$开头的称为实例属性,把_开头的称变量)。这里放一张官网上的生命周期图片,具体的在之后的内容中也会涉及到的

vue源码阅读——vue生命周期_第5张图片
vue生命周期
  1. initEvents


    vue源码阅读——vue生命周期_第6张图片
    initEvents执行前
vue源码阅读——vue生命周期_第7张图片
initEvents执行后

看上去也就是多了几个变量,我们看看具体的函数执行情况吧

initEvents代码.png

这么一张图看上去已经很清晰了, vm.$options._parentListeners未定义,因此之后的步骤也不会执行的,整个函数就新增了两个变量。不过这个函数本来想干的事,看作者注释的话是想添加父实例的事件,这个功能在我之后调试到相应例子之后会再进行扩散的

4.initRender

vue源码阅读——vue生命周期_第8张图片
initRender执行前

vue源码阅读——vue生命周期_第9张图片
initRender执行后

好害怕啊突然变长好多~总结一下就是又多了很多属性( $attrs$listeners、$slots、$scopedSlots、$vnode)和变量,还增加了方法($createElement)。先看一下执行过程

vue源码阅读——vue生命周期_第10张图片
initRender代码

这里大致可以分为三个部分,第一又增加了变量和属性,同样我们在这个例子里看不出什么,比如说$slots这个实例属性,第二是给vm增加了一个$createElement方法,调用这个方法其实就是调用createElement方法,看命名就知道是一个生成元素的方法。第三个是调用了一个defineReactive方法,这个方法是什么意思呢

vue源码阅读——vue生命周期_第11张图片
defineReactive

这些步骤基本是在做初始化

vue源码阅读——vue生命周期_第12张图片
defineReactive

但是最重要的这里,事实上对于get,在我们这个例子中,Dep.target为null,因此get就是返回了value,而set也就是普通的设置了值而已,那么这么长的代码有什么用呢,这其实又和数据绑定有关。关于数据绑定我这里也先不再延伸。所以这第三部分就是在给$attrs、$listeners重写get和set

5.callHook(vm, 'beforeCreate')

function callHook (vm, hook) {
  var handlers = vm.$options[hook];
  if (handlers) {
    for (var i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm); // 执行beforeCreate下的每个函数
      } catch (e) {
        handleError(e, vm, (hook + " hook"));
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook);
  }
}

这个函数很简单,我们这个简单的vue实例也没有传beforeCreate这个选项,因此这步也可以忽略,只需要知道在create之前会调用这样一个东西

6.initInjections

 function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm) //获得vm.$options.inject的所有属性
  if (result) {
    observerState.shouldConvert = 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]) // 把inject监听起来
      }
    })
    observerState.shouldConvert = true
  }
}

瞅瞅这个简单的代码,对于我们这个例子来说,又是可以跳过的。不过我们还需大致看看这个函数,它其实是对inject选项的初始化,这个眼熟的defineReactive,没错就是对inject的每个属性的get,set都重写了一下。

7.initState

vue源码阅读——vue生命周期_第13张图片
initState执行前

这个函数明显是很重要的,因为它处理了props/methods/data/computed/watch等选项,而这些选项又是我们经常需要用到的。那我们从源码看看它怎么处理的

function initState (vm: Component) {
  // 首先在vm上初始化一个_watchers数组,缓存这个vm上的所有watcher
  vm._watchers = []
  // 获取options,包括在new Vue传入的,同时还包括了Vue所继承的options
  const opts = vm.$options
  // 初始化props属性
  if (opts.props) initProps(vm, opts.props)
  // 初始化methods属性
  if (opts.methods) initMethods(vm, opts.methods)
  // 初始化data属性
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化computed属性
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化watch属性
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

这段代码从命名上看是很简单的,没有阅读障碍。那我们看看和我们相关的initData

function initData (vm: Component) {
  // 获取data
  let data = vm.$options.data
  // 因为data有可能是个函数,这里给它转换了一下
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  // 收集data的自身属性名
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  // 遍历每个属性
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      // data的属性不能同时在method中
      if (methods && hasOwn(methods, key)) {
        warn(
          `method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    // data的属性不能同时在props中
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      // 给data里的每个属性重写了get和set
      proxy(vm, `_data`, key)
    }
  }
  // 时刻观察data
  // observe data
  observe(data, true /* asRootData */)
}

这个observe函数实际上做了很多事情。
8.initProvide

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

初始化provide选项。超简单的代码,谁都能看懂,加了个_provided变量,和我这个例子好像也没有什么关系

  1. callHook(vm, 'created')
    和callHook(vm, 'beforeCreate')一样一样的,不再赘述了

10.mount

if (vm.$options.el) {
      vm.$mount(vm.$options.el) //如果传入了el就挂载
    }

像这段代码,和生命周期图结合起来看,就一目了然,如果有el选项的话就挂载

vue源码阅读——vue生命周期_第14张图片
$mount-1
vue源码阅读——vue生命周期_第15张图片
$mount-2
vue源码阅读——vue生命周期_第16张图片
mount
vue源码阅读——vue生命周期_第17张图片
mountComponent核心代码
vue源码阅读——vue生命周期_第18张图片
Wacher-1
vue源码阅读——vue生命周期_第19张图片
Wacher-2
vue源码阅读——vue生命周期_第20张图片
get

那么再来看看updateComponent


updateComponent

这里首先要执行vm._render()

vue源码阅读——vue生命周期_第21张图片
_render-1

回到_update

vue源码阅读——vue生命周期_第22张图片
_update

我们传了旧的和新的node进去


vue源码阅读——vue生命周期_第23张图片
__patch__

这步执行完页面上是这样的

vue源码阅读——vue生命周期_第24张图片
两个

直到下图执行完后,页面才算更新完毕

vue源码阅读——vue生命周期_第25张图片
去掉一个
vue源码阅读——vue生命周期_第26张图片
好了

说的可能还有点不太完整。整个_init方法实际上只是到mount部分为止。真的要销毁的话得手动调用$destroy。还有很多函数没有仔细扩散,等之后调试到相关例子的时候会再补充。

你可能感兴趣的:(vue源码阅读——vue生命周期)