初始化阶段主要初始化一些属性、事件以及响应式数据,如props、methods、data、computed、watch、provide和inject等。

模板编译阶段,主要是将模板编译成渲染函数,只存在完整版本中。

挂载阶段Vue.js会开启Watcher来持续追踪依赖的变化,当数据(状态)发生变化时,Watcher会通知虚拟DOM重新渲染视图,并且会在渲染视图前触发beforeUpdate钩子函数,渲染完毕后触发updated钩子函数。

卸载阶段,Vue.js会将自身从父组件中删除,取消实例上所有依赖的追踪并且移除所有的事件监听器。

当调用new Vue()时会执行一些初始化操作

import { initMixin } from './init'

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

// Vue.js通过调用initMixin方法将 _init挂载到Vue构造函数的原型上
initMixin(Vue)

export default Vue

initMixin方法

export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    // ...
  }
}

_init方法

Vue.prototype._init = function (options) {
  // 将用户传递的options选项与当前构造函数的options属性及其父级实例构造函数的options属性,合并生成一个新的options并赋值给 $options属性
  vm.$options = mergeOptions(
    // resolveConstructorOptions函数的作用就是获取当前实例中构造函数的options选项及其所有父级的构造函数的options
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )

  // 在初始化的过程中,首先初始化事件与属性
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')

  initInjections(vm) // 在data/props前初始化inject,让用户可以在data/props中使用inject所注入的内容
  initState(vm)
  initProvide(vm) // 在data/props后初始化provide
  
  callHook(vm, 'created')

  // 如果用户在实例化Vue.js时传递了el选项,则自动开启模板编译阶段与挂载阶段
  // 如果没有传递el选项,则不进入下一个生命周期流程
  // 用户需要执行vm.$mount方法,手动开启模板编译阶段与挂载阶段
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

Vue.js会在初始化流程的不同时期通过callHook函数触发生命周期钩子。那么callHook函数的内部是怎么实现的呢?

export function callHook (vm, hook) {
  // 获取生命周期钩子列表
  const handlers = vm.$options[hook]
  // 遍历生命周期钩子列表依次执行
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      // 使用try...catch捕获钩子函数内发生的错误
      try {
        handlers[i].call(vm)
      } catch (e) {
        // handleError会依次执行父组件的errorCaptured钩子函数与全局的config.errorHandler,这也是为什么生命周期钩子errorCaptured可以捕获子孙组件的错误。
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
}

上述代码我们可能会有两个疑问?

  • 为什么能通过vm.options获取对应的生命周期函数 用户设置的生命周期钩子会在执行new Vue()时通过参数传递给Vue.js,我们可以在Vue.js的构造函数中通过options参数得到用户设置的生命周期钩子。用户传入的options参数最终会与构造函数的options属性合并生成新的options并赋值到vm.$options属性中,所以我们可以通过vm.$options得到用户设置的生命周期函数。
  • 获取到的生命周期钩子为什么是列表呢 Vue.js在合并options的过程中会找出options中所有key是钩子函数的名字,并将它转换成数组。因为Vue.mixin会将选项写入Vue.optioins,会影响之后创建的所有实例。Vue.js在初始化时会将构造函数中的options和用户传入的options选项合并成一个新的选项并赋值给vm.$options。Vue.mixin和用户在实例化Vue.js时,如果设置了同一个生命周期钩子,那么在触发生命周期时,需要同时触发这两个函数。转换成数组后,可以在同一个生命周期钩子列表中保存多个生命周期钩子。

initLifecycle函数用于初始化实例属性向实例中挂载属性,以 $ 开头的属性是提供给用户使用的外部属性,以 _ 开头的属性是提供给内部使用的内部属性。该函数接收Vue.js实例作为参数

// 主要功能就是向Vue.js实例上设置一些属性并提供一个默认值
export function initLifecycle (vm) {
  const options = vm.$options

  // 找出第一个非抽象父类
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 将当前实例添加到父组件实例的 $children属性中
    parent.$children.push(vm)
  }

  vm.$parent = parent
  // 当前组件树的根Vue.js实例
  vm.$root = parent ? parent.$root : vm

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

  vm._watcher = null
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

初始化事件是将父组件在模板中使用的v-on注册的事件添加到子组件的事件系统(Vue.js的事件系统)中

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})

new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

上面的例子,vm.$options._parentListeners

{increment: function () {}}

如果v-on写在组件标签上,那么这个事件会注册到子组件Vue.js事件系统中;如果是写在平台标签上,例如div,那么事件会被注册到浏览器事件中。

在模板编译阶段,可以得到某个标签上的所有属性,其中就包括使用v-on或@注册的事件。在模板编译阶段,我们会将整个模板编译成渲染函数,而渲染函数其实就是一些嵌套在一起的创建元素节点的函数。创建元素节点的函数是这样的:_c(tagName, data, children)。当渲染流程启动时,渲染函数会被执行并生成一份VNode,随后虚拟DOM会使用VNode进行对比与渲染。在这个过程中会创建一些元素,但此时会判断当前这个标签究竟是真的标签还是一个组件:如果是组件标签,那么会将子组件实例化并给它传递一些参数,其中就包括父组件在模板中使用v-on注册在子组件标签上的事件;如果是平台标签,则创建元素并插入到DOM中,同时会将标签上使用v-on注册的事件注册到浏览器事件中。

initEvents函数来执行初始化事件相关的逻辑

export function initEvents (vm) {
  // 存储事件,使用vm.$on注册的事件监听器都会保存到vm._events属性中
  vm._events = Object.create(null)
  // 初始化父组件附加的事件
  // 在模板编译阶段,当模板解析到组件标签时,会实例化子组件,同时将标签上注册的事件解析成object并通过参数传递给子组件。所以当子组件被实例化时,可以在参数中获取父组件向自己注册的事件,这些事件最终会被保存在vm.$options._parentListeners中。
  const listeners = vm.$options._parentListeners
  // 如果vm.$options._parentListeners不为空,则调用updateComponentListeners方法,将父组件向子组件注册的事件注册到子组件实例中。
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}


let target

// 新增事件
function add (event, fn, once) {
  if (once) {
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}

// 删除事件
function remove (event, fn) {
  target.$off(event, fn)
}

// 循环vm.$options._parentListeners并使用vm.$on把事件都注册到this._events中
export function updateComponentListeners (vm, listeners, oldListeners) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, vm)
}

// 对比listeners和oldListeners的不同,并调用add和remove进行相应的注册事件和卸载事件的操作
export function updateListeners (on, oldOn, add, remove, vm) {
  let name, cur, old, event

  // 循环on判断哪些事件在oldOn中不存在,调用add注册这些事件
  for (name in on) {
    cur = on[name]
    old = oldOn[name]
    // 将事件修饰符解析出来
    event = normalizeEvent(name)

    // 判断事件名对应的值是否是undefined或null,如果是,则在控制台触发警告
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      // 判断该事件名在oldOn中是否存在,如果不存在,则调用add注册事件
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur)
      }
      add(event.name, cur, event.once, event.capture, event.passive)
    } else if (cur !== old) {
      // 事件名在on和oldOn中都存在,但是它们并不相同,则将事件回调替换成on中的回调,并且把on中的回调引用指向真实的事件系统中注册的事件,也就是oldOn中对应的事件。
      old.fns = cur
      on[name] = old
    }
  }
  // 循环oldOn判断哪些事件在oldOn中不存在,调用add注册这些事件
  for (name in oldOn) {
    if (isUndef(on[name])) {
      // 将事件修饰符解析出来
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

// 将事件修饰符解析出来
const normalizeEvent = name => {
  // 事件有修饰符,则会将它截取出来
  const passive = name.charAt(0) === '&'
  name = passive ? name.slice(1) : name
  const once = name.charAt(0) === '~'
  name = once ? name.slice(1) : name
  const capture = name.charAt(0) === '!'
  name = capture ? name.slice(1) : name

  // 保存了事件名以及一些事件修饰符,这些修饰符为true说明事件使用了此事件修饰符
  return {
    name,
    once,
    capture,
    passive
  }
}

Vue支持事件修饰符,如capture、once和passive,如果我们在模板中注册事件时使用了事件修饰符,那么在模板编译阶段解析标签上的属性时,会将这些修饰符改成对应的符号加在事件名的前面

<child v-on:increment.once="a"></child>

vm.$options._parentListeners结果如下

{~increment: function () {}}

事件名的前面新增了一个~符号,这说明该事件的事件修饰符是once。上面代码中normalizeEvent函数的作用就是解析事件修饰符的。

# 初始化inject

# provide/inject的使用方式

inject和provide选项需要一起使用,它们允许祖先组件向其所有子孙后代注入依赖,并在其上下游关系成立的时间里始终生效(不论组件层次有多深)。

provide选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性,你可以使用ES2015 Symbol作为key,但是这只在原生支持Symbol和Reflect.ownKeys的环境下可工作。

inject选项应该是一个字符串数组或对象,其中对象的key是本地的绑定名,value是一个key(字符串或Symbol)或对象,用来在可用的注入内容中搜索。

  • name:它是在可用的注入内容中用来搜索的key(字符串或 Symbol)。
  • default:它是在降级情况下使用的value。

可用的注入内容指的是祖先组件通过provide注入了内容,子孙组件可以通过inject获取祖先组件注入的内容。

var Provider = {
  provide: {
    foo: 'bar'
  },
  // ……
}

var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ……
}
// 或
const s = Symbol()

const Provider = {
  provide () {
    return {
      [s]: 'foo'
    }
  }
}

const Child = {
  inject: { s },
  // ……
}

可以在data/props中访问注入的值

// 在props访问注入的值
const Child = {
  inject: ['foo'],
  props: {
    bar: {
      default () {
        // 作为props默认值
        return this.foo
      }
    }
  }
}

// 在data中访问注入的值
const Child = {
  inject: ['foo'],
  data () {
    return {
      // 作为数据入口
      bar: this.foo
    }
  }
}

// Vue.js 2.5.0+ 设置inject的默认值
const Child = {
  inject: {
    foo: { default: 'foo' }
  }
}

const Child = {
  inject: {
    foo: {
      // 用from 来表示其源属性
      from: 'bar',
      default: 'foo'
    }
  }
}

// inject的默认值与props的默认值类似,我们需要对非原始值使用一个工厂方法
const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: () => [1, 2, 3]
    }
  }
}

# inject的内部原理

initInjections方法用于初始化inject,初始化inject,就是使用inject配置的key从当前组件读取内容,读不到则读取它的父组件,以此类推。它是一个自底向上获取内容的过程,最终将找到的内容保存到实例(this)中,这样就可以直接在this上读取通过inject导入的注入内容。

export function initInjections (vm) {
  // 通过用户配置的inject,自底向上搜索可用的注入内容,并将搜索结果返回
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    // 通知defineReactive函数不要将内容转换成响应式
    observerState.shouldConvert = false
    Object.keys(result).forEach(key => {
      defineReactive(vm, key, result[key])
    })
    observerState.shouldConvert = true
  }
}

export function resolveInject (inject, vm) {
  if (inject) {
    const result = Object.create(null)
    // 浏览器原生支持Symbol,那么使用Reflect.ownKeys读取出inject的所有key;如果浏览器原生不支持Symbol,那么使用Object.keys获取key。区别是Reflect.ownKeys可以读取Symbol类型的属性,而Object.keys读不出来。
    // Reflect.ownKeys有一个特点,它可以返回所有自有属性的键名,其中字符串类型和Symbol类型都包含在内。而Object.getOwnPropertyNames和Object.keys返回的结果不会包含Symbol类型的属性名,Object.getOwnPropertySymbols方法又只返回Symbol类型的属性。
    // 获取inject的key
    const keys = hasSymbol
      ? Reflect.ownKeys(inject).filter(key => { // 通过Reflect.ownKeys读出的key包括不可枚举的属性,要将不可枚举的属性过滤掉
        return Object.getOwnPropertyDescriptor(inject, key).enumerable
      })
      : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // 获取provide源属性,inject还支持数组的形式,如果用户将inject的值设置为数组,那么inject中是没有from属性的。Vue.js在实例化的第一步是规格化用户传入的数据,如果inject传递的内容是数组,那么数组会被规格化成对象并存放在from属性中。不论是数组形式还是对象中使用from属性的形式,本质上其实是让用户设置原属性名与当前组件中的属性名。如果用户设置的是数组,那么就认为用户是让两个属性名保持一致。
      /* 
        假设用户设置的inject是这样的
        {
          inject:[foo]
        }
        被规格化后
        {
          inject: {
            foo: {
              from: 'foo'
            }
          }
        }
      */
      const provideKey = inject[key].from
      // 最开始source等于当前组件实例
      let source = vm
      // 当使用provide注入内容时,其实是将内容注入到当前组件实例的 _provide中,所以inject可以从父组件实例的 _provide中获取注入的内容。通过这样的方式,最终会在祖先组件中搜索到inject中设置的所有属性的内容。
      // 通过源属性使用while循环来搜索内容 
      while (source) {
        if (source._provided && provideKey in source._provided) {
          // 如果原始属性在source的 _provided中能找到对应的值,那么将其设置到result中,并使用break跳出循环
          result[key] = source._provided[provideKey]
          break
        }
        // 将source设置为父组件实例进行下一轮循环
        source = source.$parent
      }
      if (!source) {
        // 是否存在默认值
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          // 默认值支持函数,要判断默认值的类型是不是函数,是则执行函数,将函数的返回值设置给result[key]
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          // 如果inject[key] 中不存在default属性,那么会在非生产环境下的控制台中打印警告
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }
}

# 初始化状态

initState用于初始化状态,初始化只初始化用户使用到的状态不会都初始化

export function initState (vm) {
  // 保存当前组件中所有的watcher实例
  vm._watchers = []
  const opts = vm.$options
  // 初始化props
  if (opts.props) initProps(vm, opts.props)
  // 初始化methods
  if (opts.methods) initMethods(vm, opts.methods)

  if (opts.data) {
    // 初始化data
    initData(vm)
  } else {
    // 观察空对象
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化watch,当用户设置了watch选项并且watch选项不等于浏览器原生的watch时,初始化watch。因为Firefox浏览器中的Object.prototype上有一个watch方法。当用户没有设置watch时,在Firefox浏览器下的opts.watch将是Object.prototype.watch函数
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

# 初始化props

Vue组件系统的运作原理

Vue.js中的所有组件都是Vue.js实例,组件在进行模板解析时,会将标签上的属性解析成数据,最终生成渲染函数。而渲染函数被执行时,会生成真实的DOM节点并渲染到视图中。如果某个节点是组件节点,那么在虚拟DOM渲染的过程中会将子组件实例化,这会将模板解析时从标签属性上解析出的数据当作参数传递给子组件,其中就包含props数据。

  1. 规格化props

props可以通过数组指定需要哪些属性。但在Vue.js内部,数组格式的props将被规格化成对象格式。

function normalizeProps (options, vm) {
  const props = options.props
  // 判断是否有props属性
  if (!props) return
  // 保存规格化后的结果
  const res = {}
  let i, val, name
  // 检查props是否为一个数组,主要作用就是将Array类型的props规格化成Object类型
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      // props名称的类型是否是String类型
      if (typeof val === 'string') {
        // 调用camelize函数将props名称驼峰化,如将news-id转换成newsId
        name = camelize(val)
        // 将props名当作属性,设置到res中
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') { // 在非生产环境下在控制台中打印警告
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) { // 检测props是否为对象类型
    /* 
      如果props值是对象的话有可能是下面三种格式
      1.基础的类型函数
      {
        propA: Number
      }
      2.数组
      {
        propB: [String, Number]
      }
      3.对象类型的高级选项
      {
        propC: {
          type: String,
          required: true
        }
      }
    */
    for (const key in props) {
      val = props[key]
      // 调用camelize函数将props名称驼峰化
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') { // 在非生产环境下在控制台中打印警告
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}
  1. 初始化props

通过规格化之后的props从其父组件传入的props数据中或从使用new创建实例时传入的propsData参数中,筛选出需要的数据保存在vm._props中,然后在vm上设置一个代理,实现通过vm.x访问vm._props.x的目的

/* 
vm: Vue实例
propsOptions:规格化的props
*/
function initProps (vm, propsOptions) {
  // propsData中保存的是通过父组件传入或用户通过propsData传入的真实props数据
  const propsData = vm.$options.propsData || {}
  // 变量props是指向vm._props的指针,也就是所有设置到props变量中的属性最终都会保存到vm._props中
  const props = vm._props = {}
  // 缓存props的key,将来更新props时只需要遍历vm.$options._propKeys数组即可得到所有props的key
  const keys = vm.$options._propKeys = []
  // 判断当前组件是否是根组件
  const isRoot = !vm.$parent
  // root实例的props属性应该被转换成响应式数据
  if (!isRoot) {
    // 作用是确定并控制defineReactive函数调用时所传入的value参数是否需要转换成响应式的。
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    // 将key添加到keys中
    keys.push(key)
    // 将得到的props数据通过defineReactive函数设置到vm._props中。
    const value = validateProp(key, propsOptions, propsData, vm)
    defineReactive(props, key, value)
    // 判断这个key在vm中是否存在
    if (!(key in vm)) {
      // 如果不存在,则调用proxy,在vm上设置一个以key为属性的代理,当使用vm[key] 访问数据时,其实访问的是vm._props[key]
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}


/* 
  key:propOptions中的属性名。
  propOptions:子组件用户设置的props选项。
  propsData:父组件或用户提供的props数据。
  vm:Vue.js实例上下文,this的别名。
*/
export function validateProp (key, propOptions, propsData, vm) {
  // 保存当前这个key的prop选项
  const prop = propOptions[key]
  // 表示当前的key在用户提供的props选项中是否存在
  const absent = !hasOwn(propsData, key)
  // 获取用户提供的props选项中的数据
  let value = propsData[key]
  // 处理布尔类型的props,isType方法判断prop的type属性是否是布尔值
  if (isType(Boolean, prop.type)) {
    // 父组件或用户没有提供这个数据,key不存在且props选项中也没有设置默认值
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) { // key存在,value是空字符串或者value和key相等。hyphenate函数会将key进行驼峰转换,如userName -> user-name。所以属性为userName的值如果是user-name,那么也会将value设置为true。
      value = true
    }
  }
  /* 
    下面这种情况子组件的prop都将设置为true
    <child name></child>  
    <child name="name"></child>  
    <child userName="user-name"></child>
  */


  // 其他类型的prop只需处理子组件通过props选项设置的key在props数据中并不存在,检查默认值将默认值转换成响应式数据
  if (value === undefined) {
    // 获取prop的默认值
    value = getPropDefaultValue(vm, prop, key)
    // 因为默认值是新的数据,所以需要将它转换成响应式的
    const prevShouldConvert = observerState.shouldConvert
    observerState.shouldConvert = true
    // 将获取的默认值转换成响应式的
    observe(value)
    // 将状态恢复成最初的状态
    observerState.shouldConvert = prevShouldConvert
  }
  if (process.env.NODE_ENV !== 'production') {
    // 调用assertProp来断言prop是否有效
    assertProp(prop, key, value, vm, absent)
  }
  return value
}


/* 
  prop: prop选项
  name: props中prop选项的key
  value: prop数据(propData)
  vm:上下文(this)
  absent:prop数据中不存在key属性
*/
function assertProp (prop, name, value, vm, absent) {
  // 处理prop中设置了必填项且prop数据中没有这个key属性这种情况
  if (prop.required && absent) {
    warn(
      'Missing required prop: "' + name + '"',
      vm
    )
    return
  }
  // 不是必填项且value不存在
  if (value == null && !prop.required) {
    return
  }
  // prop中用来校验的类型
  let type = prop.type
  // valid表示是否校验成功, type是一个原生构造函数或一个数组,或者用户没提供type
  let valid = !type || type === true
  // 保存type的列表
  const expectedTypes = []
  if (type) {
    // 判断type是否是一个数组
    if (!Array.isArray(type)) {
      // type不是数组,将type转换为数组
      type = [type]
    }
    // !valid表示type列表中只要有一个校验成功,循环就结束,认为是成功了
    for (let i = 0; i < type.length && !valid; i++) {
      // assertedType函数校验value
      const assertedType = assertType(value, type[i])
      // assertedType.expectedType表示类型,如{valid: true, expectedType: "Boolean"}
      expectedTypes.push(assertedType.expectedType || '')
      // assertedType.valid:是否校验成功
      valid = assertedType.valid
    }
  }
  if (!valid) {
    warn(
      `Invalid prop: type check failed for prop "${name}".` +
      ` Expected ${expectedTypes.map(capitalize).join(', ')}` +
      `, got ${toRawType(value)}.`,
      vm
    )
    return
  }
  const validator = prop.validator
  // 判断用户是否设置了validator(自定义验证函数)
  if (validator) {
    if (!validator(value)) {
      warn(
        'Invalid prop: custom validator check failed for prop "' + name + '".',
        vm
      )
    }
  }
}

# 初始化methods

initMethods方法用于初始化methods,循环选项中的methods对象,并将每个属性依次挂载到vm上

function initMethods (vm, methods) {
  // 判断methods中的方法是否和props发生了重复
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      // 处理方法不存在的情况
      if (methods[key] == null) {
        warn(
          `Method "${key}" has an undefined value in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      // methods中的方法和props发生了重复
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      // methods中的某个方法已存在于vm中,并且方法名是以 $ 或 _ 开头的,在控制台发出警告
      // isReserved函数的作用是判断字符串是否是以 $ 或 _ 开头
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    // 判断方法是否存在:如果不存在,则将noop赋值到vm[key] 中;如果存在,则将该方法通过bind改写它的this后,再赋值到vm[key] 中
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}

# 初始化data

data初始化的过程:data中的数据最终会保存到vm._data中。然后在vm上设置一个代理,使得通过vm.x可以访问到vm._data中的x属性。最后由于这些数据并不是响应式数据,所以需要调用observe函数将data转换成响应式数据。

function initData (vm) {
  // 获取data
  let data = vm.$options.data

  // 如果data是函数,则执行函数将函数返回值赋值给变量data和vm._data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  // 如果data不是对象类型
  if (!isPlainObject(data)) {
    // 给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
    )
  }
  // 将data代理到Vue.js实例上
  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') {
      // 判断当前循环的key是否存在于methods中,如果存在说明重复了,在控制台输出警告
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    // 判断props中是否存在某个属性与key相同
    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)) { // 属性名不能以 $ 或 _ 开头
      proxy(vm, `_data`, key) // 将属性代理到实例上
    }
  }
  // 执行observe函数将数据转换成响应式的
  observe(data, true /* asRootData */)
}

// proxy

// 默认属性描述符
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

// 将vm._data中的方法代理到vm上。
export function proxy (target, sourceKey, key) {
  // 设置get 相当于给属性提供了getter方法
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  // 设置set 相当于给属性提供了settr方法
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  // 给target定义一个属性,属性名为key,属性描述符为sharedPropertyDefinition
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

# 初始化computed

// Watcher选项,实例化Watcher时,通过参数告诉Watcher类应该生成一个供计算属性使用的watcher实例
const computedWatcherOptions = { lazy: true }

/*
初始化计算属性 
vm:Vue.js实例上下文(this)
computed:计算属性对象
*/
function initComputed (vm, computed) {
  // _computedWatchers属性用来保存所有计算属性的watcher实例
  // Object.create(null)创建出来的对象没有原型,它不存在 __proto__ 属性。
  const watchers = vm._computedWatchers = Object.create(null)
  // 计算属性在SSR环境中,只是一个普通的getter方法
  // 判断当前运行环境是否是服务端渲染,isServerRendering工具函数执行后,会返回一个布尔值用于判断是否是服务端渲染环境。
  const isSSR = isServerRendering()

  // 依次初始化每个计算属性
  for (const key in computed) {
    // 保存用户设置的计算属性定义
    const userDef = computed[key]
    // 获取getter函数
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 传入的计算属性既不是函数也不是对象,或者对象没有get方法,在控制台打印警告以提示用户
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    // 在非SSR环境中,为计算属性创建内部观察器
    // 第二个参数的getter是用户设置的计算属性的get函数
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 判断当前循环到的计算属性的名字是否已经存在于vm中
    if (!(key in vm)) {
      // 使用defineComputed函数在vm上设置一个计算属性
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 当计算属性的名字已经存在于vm中时,说明已经有了一个重名的data或者props,也有可能是与methods重名,这时候不会在vm上定义计算属性,如果与methods重名,并不会在控制台打印警告。所以如果与methods重名,计算属性会悄悄失效
      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)
      }
    }
  }
}

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

// 在target上定义一个key属性,属性的getter和setter根据userDef的值来设置
export function defineComputed (target, key, userDef) {
  // 判断computed是否应该有缓存,isServerRendering判断当前环境是否是服务端渲染环境
  const shouldCache = !isServerRendering()

  // 判断userDef的类型,如果是函数,则将函数理解为getter函数。如果是对象,则将对象的get方法作为getter方法,set方法作为setter方法。
  /* 
    为什么要判断userDef的类型呢?

    因为Vue.js支持用户设置两种类型的计算属性:函数和对象
    var vm = new Vue({
      data: { a: 1 },
      computed: {
        // 仅读取
        aDouble: function () {
          return this.a * 2
        },
        // 读取和设置
        aPlus: {
          get: function () {
            return this.a + 1
          },
          set: function (v) {
            this.a = v - 1
          }
        }
      }
    })
  */
  if (typeof userDef === 'function') {
    /* 
      通过判断shouldCache来选择将get设置成userDef这种普通的getter函数,还是设置为计算属性的getter函数。其区别是如果将sharedPropertyDefinition.get设置为userDef函数,那么这个计算属性只是一个普通的getter方法,没有缓存。当计算属性中所使用的数据发生变化时,计算属性的Watcher也不会得到任何通知,使用计算属性的Watcher也不会得到任何通知。它就是一个普通的getter,每次读取操作都会执行一遍函数。这种情况通常在服务端渲染环境下生效,因为数据响应式的过程在服务器上是多余的。

      用户并没有设置setter函数,将sharedPropertyDefinition.set设置为noop,而noop是一个空函数 
    */
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop 
  } else {
    // 判断userDef.get是否存在,如果不存在,则将sharedPropertyDefinition.get设置成noop。如果存在,那么逻辑和前面介绍的相同,如果shouldCache为true并且用户没有明确地将userDef.cache设置为false,则调用createComputedGetter函数将sharedPropertyDefinition.get设置成计算属性的getter函数,否则将sharedPropertyDefinition.get设置成普通的getter函数userDef.get。
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    // 只需要判断userDef.set是否存在,如果存在,则将sharedPropertyDefinition.set设置为userDef.set,否则设置为noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }

  // 如果用户没有设置setter函数,那么为计算属性设置一个默认的setter函数,并且当函数执行时,打印出警告
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  // 调用Object.defineProperty方法在target对象上设置key属性,其中属性描述符为前面我们设置的sharedPropertyDefinition。
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// 计算属性的缓存与响应式功能主要在于是否将getter方法设置为createComputedGetter函数执行后的返回结果
function createComputedGetter (key) {
  // 被设置到getter方法中的函数是被返回的computedGetter函数,在非服务端渲染环境下,每当计算属性被读取时,computedGetter函数都会被执行
  return function computedGetter () {
    // 读出watcher并赋值给变量watcher,this._computedWatchers属性保存了所有计算属性的watcher实例
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // watcher存在
    if (watcher) {
      // 判断watcher.dirty是否为true,watcher.dirty属性用于标识计算属性的返回值是否有变化
      if (watcher.dirty) {
        watcher.evaluate()
      }

      // 判断Dep.target是否存在,如果存在,则调用watcher.depend方法
      if (Dep.target) {
        // 将读取计算属性的那个Watcher添加到计算属性所依赖的所有状态的依赖列表中,换句话说,读取计算属性的那个Watcher持续观察计算属性所依赖的状态的变化

        /* 
          使用计算属性的同学大多会有一个疑问:为什么我在模板里只使用了一个计算属性,但是把计算属性中用到的另一个状态给改了,模板会重新渲染,它是怎么知道自己需要重新渲染的呢?

          这是因为组件的Watcher观察了计算属性中所依赖的所有状态的变化。当计算属性中所依赖的状态发生变化时,组件的Watcher会得到通知,然后就会执行重新渲染操作。
        */
        watcher.depend()
      }
      return watcher.value
    }
  }
}

Watcher中定义了depend与evaluate方法专门用于实现计算属性相关的功能

export default class Watcher {
  constructor (vm, expOrFn, cb, options) {
    // 隐藏无关代码

    if (options) {
      this.lazy = !!options.lazy
    } else {
      this.lazy = false
    }

    this.dirty = this.lazy

    this.value = this.lazy
      ? undefined
      : this.get()
  }

  evaluate () {
    // 执行this.get方法重新计算一下值
    this.value = this.get()
    // 将this.dirty设置为false
    this.dirty = false
  }

  depend () {
    let i = this.deps.length
    // 遍历this.deps属性(该属性中保存了计算属性用到的所有状态的dep实例,而每个属性的dep实例中保存了它的所有依赖),并依次执行dep实例的depend方法。
    while (i--) {
      // 执行dep实例的depend方法可以将组件的watcher实例添加到dep实例的依赖列表中。换句话说,this.deps是计算属性中用到的所有状态的dep实例,而依次执行了dep实例的depend方法就是将组件的Watcher依次加入到这些dep实例的依赖列表中,这就实现了让组件的Watcher观察计算属性中用到的所有状态的变化。当这些状态发生变化时,组件的Watcher会收到通知,从而进行重新渲染操作。
      this.deps[i].depend()
    }
  }
}

如果计算属性中用到的状态发生了变化,但最终计算属性的返回值并没有变,这时计算属性依然会认为自己的返回值变了,组件也会重新走一遍渲染流程。只不过最终由于虚拟DOM的Diff中发现没有变化,所以在视觉上并不会发现UI有变化,其实渲染函数会被执行。计算属性只是观察它所用到的所有数据是否发生了变化,但并没有真正去校验它自身的返回值是否有变化,所以当它所使用的数据发生变化后,它就认为自己的返回值也会有变化,但事实并不总是这样。

Vue.js在2.5.17对计算属性的实现方式做了一个改动,改动后的逻辑:组件的Watcher不再观察计算属性用到的数据的变化,而是让计算属性的Watcher得到通知后,计算一次计算属性的值,如果发现这一次计算出来的值与上一次计算出来的值不一样,再去主动通知组件的Watcher进行重新渲染操作。这样就可以解决前面提到的问题,只有计算属性的返回值真的变了,才会重新执行渲染函数。

这样计算属性的getter被触发后:

  • 使用组件的Watcher观察计算属性的Watcher,也就是把组件的Watcher添加到计算属性的Watcher的依赖列表中,让计算属性的Watcher向组件的Watcher发送通知。
  • 使用计算属性的Watcher观察计算属性函数中用到的所有数据,当这些数据发生变化时,向计算属性的Watcher发送通知。

如果是在模板中读取计算属性,那么使用组件的Watcher观察计算属性的Watcher;如果是用户使用vm.$watch定义的Watcher,那么其实是使用用户定义的Watcher观察计算属性的Watcher。其区别是当计算属性通过计算发现自己的返回值发生变化后,计算属性的Watcher向谁发送通知。

function createComputedGetter (key) {
  return function computedGetter () {
    // 从this._computedWatchers中读出watcher并赋值给变量watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]

    // 判断watcher是否存在
    if (watcher) {
      // depend方法被执行后,将读取计算属性的那个Watcher添加到计算属性的Watcher的依赖列表中,这可以让计算属性的Watcher向使用计算属性的Watcher发送通知。
      watcher.depend()
      // 将watcher.evaluate()的返回值当作计算属性函数的计算结果返回出去
      return watcher.evaluate()
    }
  }
}

修改后的Watcher

export default class Watcher {
  constructor (vm, expOrFn, cb, options) {
    // ...

    if (options) {
      this.computed = !!options.computed
    } else {
      this.computed = false
    }

    this.dirty = this.computed

    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }

  // 当计算属性中用到的数据发生变化时,计算属性的Watcher的update方法会被执行,此时会判断当前Watcher是不是计算属性的Watcher,如果是,那么有两种模式,一种是主动发送通知,另一种是将dirty设置为true。行业术语中,这两种方式分别叫作activated和lazy。
  update () {
    if (this.computed) {
      if (this.dep.subs.length === 0) {
        this.dirty = true
      } else {
        // activated模式要求至少有一个依赖
        // getAndInvoke方法对比计算属性的返回值,只有计算属性的返回值真的发生了变化,才会执行回调,从而主动发送通知让组件的Watcher去执行重新渲染逻辑
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    }
    // ...
  }

  getAndInvoke (cb) {
    const value = this.get()
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

  evaluate () {
    // 判断返回值是否发生了变化
    if (this.dirty) {
      // 如果发生了变化执行get方法重新计算一次
      this.value = this.get()
      // 将dirty属性设置为false,表示数据已经是最新的,不需要重新计算
      this.dirty = false
    }
    // 返回本次计算的结果
    return this.value
  }

  // 不再是将Dep.target添加到计算属性所用到的所有数据的依赖列表中,而是改成了将Dep.target添加到计算属性的依赖列表中。
  depend () {
    if (this.dep && Dep.target) {
      this.dep.depend()
    }
  }
}

# 初始化watch

watch的使用方式

  • 类型:{ [key: string]: string | Function | Object | Array }
  • 介绍:一个对象,其中键是需要观察的表达式,值是对应的回调函数,也可以是方法名或者包含选项的对象。Vue.js实例将会在实例化时调用vm.$watch() 遍历watch对象的每一个属性。

示例:

var vm = new Vue({
  data: {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: {
      f: {
        g: 5
      }
    }
  },
  watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
    // 方法名
    b: 'someMethod',
    // 深度watcher
    c: {
      handler: function (val, oldVal) { /* ... */ },
      deep: true
    },
    // 该回调将会在侦听开始之后被立即调用
    d: {
      handler: function (val, oldVal) { /* ... */ },
      immediate: true
    },
    e: [
      function handle1 (val, oldVal) { /* ... */ },
      function handle2 (val, oldVal) { /* ... */ }
    ],
    // watch vm.e.f's value: {g: 5}
    'e.f': function (val, oldVal) { /* ... */ }
  }
})
vm.a = 2 // => new: 2, old: 1

initWatch方法

function initWatch (vm, watch) {
  for (const key in watch) {
    const handler = watch[key]
    // 如果handler的类型是数组,那么遍历数组并将数组中的每一项依次调用createWatcher函数来创建Watcher
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      // 如果不是数组,那么直接调用createWatcher函数创建一个Watcher
      createWatcher(vm, key, handler)
    }
  }
}

/* 
vm:Vue.js实例上下文(this)。
expOrFn:表达式或计算属性函数。
handler:watch对象的值。
options:用于传递给vm.$watch的选项对象。

createWatcher函数主要负责处理其他类型的handler并调用vm.$watch创建Watcher观察表达式
*/
function createWatcher (vm, expOrFn, handler, options) {
  // 如果是对象,那么说明用户设置了一个包含选项的对象,因此将options的值设置为handler,并且将变量handler设置为handler对象的handler方法。
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 如果handler的类型是字符串,那么从vm中取出方法,将它赋值给handler变量即可
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

# 初始化provide

// provide选项是一个对象或者是返回一个对象的函数
export function initProvide (vm) {
  const provide = vm.$options.provide
  if (provide) {
    // 判断provide的类型是否是函数,如果是,则执行函数,将返回值赋值给vm._provided,否则直接将变量provide赋值给vm._provided
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

# errorCaptured与错误处理

errorCaptured函数用来捕获子孙组件的错误,如果该函数返回false,表示阻止错误继续向上传播。

Vue.js会使用try...catch来捕获错误,当错误被捕获到后,Vue.js会使用handleError函数来处理错误,该函数会依次触发父组件链路上的每一个父组件中定义的errorCaptured钩子函数。

handleError函数的实现:

export function handleError (err, vm, info) {
  if (vm) {
    let cur = vm
    // 如果一个组件继承的链路或其父级从属链路中存在多个errorCaptured钩子函数,则它们将会被相同的错误逐个唤起
    while ((cur = cur.$parent)) {
      // 读取errorCaptured钩子函数列表
      const hooks = cur.$options.errorCaptured
      if (hooks) {
        // 遍历钩子函数列表依次执行列表中的每一个errorCaptured钩子函数
        for (let i = 0; i < hooks.length; i++) {
          try {
            const capture = hooks[i].call(cur, err, vm, info) === false
            // 如果某个errorCaptured钩子函数能够返回false,阻止错误继续向上传播,错误向上传递和全局的config.errorHandler会被停止
            if (capture) return
          } catch (e) {
            // 如果errorCaptured钩子函数自身抛出了一个错误,那么这个新错误和原本被捕获的错误发送给全局的config.errorHandler
            globalHandleError(e, cur, 'errorCaptured hook')
          }
        }
      }
    }
  }
  globalHandleError(err, vm, info)
}

// 将错误发送给config.errorHandler
function globalHandleError (err, vm, info) {
  // 先判断Vue.config.errorHandler是否存在
  if (config.errorHandler) {
    try {
      // 将错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串通过参数的方式传递给config.errorHandler
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      // 全局错误处理的函数发生报错,在控制台打印其中抛出的错误
      logError(e)
    }
  }
  // 即便没使用Vue.config.errorHandler捕获错误,也会将错误信息打印在控制台
  logError(err)
}

function logError (err) {
  console.error(err)
}

# 总结

Vue.js的整体生命周期可以分为4个阶段:初始化阶段、模板编译阶段、挂载阶段和卸载阶段。初始化阶段结束后,会触发created钩子函数。在created钩子函数与beforeMount钩子函数之间的这个阶段是模板编译阶段,这个阶段在不同的构建版本中不一定存在。挂载阶段在beforeMount钩子函数与mounted期间。挂载完毕后,Vue.js处于已挂载阶段。已挂载阶段会持续追踪状态的变化,当数据(状态)发生变化时,Watcher会通知虚拟DOM重新渲染视图。在渲染视图前触发beforeUpdate钩子函数,渲染完毕后触发updated钩子函数。当vm.$destroy被调用时,组件进入卸载阶段。卸载前会触发beforeDestroy钩子函数,卸载后会触发destroyed钩子函数。

new Vue()被执行后,Vue.js进入初始化阶段,然后选择性进入模板编译与挂载阶段。

在初始化阶段,会分别初始化实例属性、事件、provide/inject以及状态等,其中状态又包含props、methods、data、computed与watch。