Vue2源码,生命周期篇-initInjections+initState

706 阅读7分钟

initInjections

在日常开发中用的少

provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

从函数名字上来看,该函数是用来初始化实例中的inject选项的。说到inject选项,那必然离不开provide选项,这两个选项都是成对出现的

它们的作用是:允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效

// 父级组件提供 'foo'
var Parent = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

函数分析

既然inject选项和provide选项都是成对出现的,那为什么在初始化的时候不一起初始化呢?为什么在init函数中调用initInjections函数和initProvide函数之间穿插一个initState函数呢?

这里所说的数据就是我们通常所写datapropswatchcomputedmethod,所以inject选项接收到注入的值有可能被以上这些数据所使用到,所以在初始化完inject后需要先初始化这些数据,然后才能再初始化provide

export function initInjections (vm: Component) {
  // 把数组里面的key转化为对象
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    // 告诉defineReactive函数,仅仅是把键值添加到实例上,不需要转为响应式
    toggleObserving(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])
      }
    })
    /* 开关再打开 */
    toggleObserving(true)
  }
}

initState

这个很重要,用来初始化,实例的状态选项

export function initState (vm: Component) {
  /* 
    用来存储当前实例的
    Vue不再对所有数据都进行侦测,
    而是将侦测粒度提高到了组件层面,
    对每个组件进行侦测,所以在每个组件上新增了vm._watchers属性
    ,用来存放这个组件内用到的所有状态的依赖,当其中一个状态发生变化时,
    就会通知到组件,
    然后由组件内部使用虚拟DOM进行数据比对,
    从而降低内存开销,提高性能。
  */
  vm._watchers = []
  /* 
    下面这几个初始化,是有顺序的
    所有在data 里面可以使用props
  */
  const opts = vm.$options
  // 先判断实例中是否有props选项,如果有,就调用props选项初始化函数initProps去初始化props选项
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  /* 
    如果有data就初始化data,
    没有的话,就把data当做空对象,并将其转化成响应式
  */
  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)
  }
}

normalizeProps

normalizeProps 函数,把各种props写法,进行规范

initProps

initProps函数的定义位于源码的src/core/instance/state.js

/* 
  初始化 props
  这个函数,主要是把传过来的props的key,value 放到vm实例上,
  这样在组件里,可以直接this.访问到
*/
function initProps (vm: Component, propsOptions: Object) {
  /* 父组件传入的真实props对象 */
  const propsData = vm.$options.propsData || {}
  // 指向vm._props的指针,所有设置到props变量中的属性都会保存到vm._props中
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  /* 
    指向vm.$options._propKeys的指针,缓存props对象中的key,
    将来更新props时只需遍历vm.$options._propKeys数组即可得到所有props的key
   */
  const keys = vm.$options._propKeys = []
  /* 当前组件是否是根组件 */
  const isRoot = !vm.$parent
  // root instance props should be converted
  /* 
    如果不是根组件,就不需要转化为响应式
  */
  if (!isRoot) {
    toggleObserving(false)
  }
  /* 
    遍历 props
  */
  for (const key in propsOptions) {
    /* _propKeys存入key */
    keys.push(key)
    /* 校验数据类型是否匹配,并且,取出父组件传过来的值 */
    const value = validateProp(key, propsOptions, propsData, vm)

    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      /* 
        把键值放到vm._props中
      */
      defineReactive(props, key, value, () => {
        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
          )
        }
      })
    } else {
      /* 
        把键值放到vm._props中
      */
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    /* 
      判断这个key在当前实例中是否存在
      如果不存在,则调用proxy函数在vm上设置一个以key为属性的代码,
      当使用vm[key]访问数据时,其实访问的是vm._props[key]
    */
    if (!(key in vm)) {
      /* 代理this.key === this._props.key */
      proxy(vm, `_props`, key)
    }
  }
  /* 再把defineReactive开关打开 */
  toggleObserving(true)
}

validateProp

/* 
  校验props的类型是否正确
  并且返回value的值
  key: props的key
  propOptions: 子组件的props对象
  propsData: 父组件的数据对象
  vm: Vue实例
*/
export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  /* 当前key在propOptions中对应的值 */
  const prop = propOptions[key]
  /* 父组件是否传入了该属性 */
  const absent = !hasOwn(propsData, key)
  /* 前key在propsData中对应的值,即父组件对于该属性传入的真实值*/
  let value = propsData[key]


  // boolean casting
  /* 判断是不是布尔类型 */
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      /* 父组件没有传入该prop属性并且该属性也没有默认值的时候 */
      value = false
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  // check default value
  /* 
    只需判断父组件是否传入该属性即可
  */
  if (value === undefined) {
    /* 
      父组件没有传入值的话,获取默认值
      并且转化为响应式
    */
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    // skip validation for weex recycle-list child component props
    !(__WEEX__ && isObject(value) && ('@binding' in value))
  ) {
    assertProp(prop, key, value, vm, absent)
  }
  return value
}

getPropDefaultValue

/* 
  获取props的默认值
*/
function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
  /* 
    没有定义default的话,就直接返回undefined
  */
  if (!hasOwn(prop, 'default')) {
    return undefined
  }
  const def = prop.default
  /* 
    这里default的值,不能直接写成一个对象
    默认对象或者数组,必须从一个工厂函数中获取
  */
  if (process.env.NODE_ENV !== 'production' && isObject(def)) {
    warn(
      'Invalid default value for prop "' + key + '": ' +
      'Props with type Object/Array must use a factory function ' +
      'to return the default value.',
      vm
    )
  }
  if (vm && vm.$options.propsData &&
    vm.$options.propsData[key] === undefined &&
    vm._props[key] !== undefined
  ) {
    return vm._props[key]
  }
  /* 
    判断def是否为函数并且prop.type不为Function,
    如果是的话表明def是一个返回对象或数组的工厂函数,
    那么将函数的返回值作为默认值返回;如果def不是函数,那么则将def作为默认值返回
  */
  return typeof def === 'function' && getType(prop.type) !== 'Function'
    ? def.call(vm)
    : def
}

initMethods

/* 
  初始化方法
*/
function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  /* 开始遍历methods */
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      /* 如果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
        )
      }
      /* methods如果跟props重名,就报错 */
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          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 $.`
        )
      }
    }
    /* 把函数绑定到实例上 */
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

initData

初始化函数定义位于源码的src/core/instance/state.js

/* 
  初始化 data
  1. 判断data 是否合法
  2. 转化成响应式
  3. 绑定到vm实例上,也就是proxy代理
*/
function initData (vm: Component) {

  /* 获取data,建议使用函数的形式,挂载到_data上 */
  let data = vm.$options.data
  /* 
    如果是工厂函数,就执行,获取返回值
  */
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}


  /* 
    判断是不是一个对象,否则就报错
    data函数的返回值需要是一个对象,否则就会报错
    无论传入的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 ,props, 还有methods 不能重名
  */
  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') {

      if (methods && hasOwn(methods, key)) {
        /* 是否跟methods重名 */
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }

    if (props && hasOwn(props, key)) {
      /* 是否跟props重名 */
      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)) {
      /* 判断key不是 _或者$ 开头的属性 */
      /* 
        代理key
        this.key === this._data.key
      */
      proxy(vm, `_data`, key)
    }
  }
  /* 
    观察者 到了
    把data 里面的属性转化为响应式
  */
  observe(data, true /* asRootData */)
}
/* 
  getData 执行传入的函数,返回值
*/
export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

initComputed

/* 
  计算属性的初始化
  计算属性的结果会被缓存
 */
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
  /* 
    开始遍历computed里面的每一项属性
  */
  for (const key in computed) {
    // 拿到key对应的值
    const userDef = computed[key]
    // 判断是不是函数,当然也可以是对象,平时写函数是比较多的
    /* 如果不是函数,就吧对象里的get返回 */
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    /* 这里正常来说getter 是个undefined,但是用了两个等号,所以undefined == null 为TRUE */
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }
    // 这个位置是关键
    if (!isSSR) {
      // create internal watcher for the computed property.
      /* 
        创建一个watcher实例
        并且放到对象watchers上面
      */
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    // 这里的意思是computed里面定义的key的名字是不能跟data或者props里面的key冲突的
    /* 
      判断这个key在不在实例上面
    */
    if (!(key in vm)) {
      // 下面进入关键代码
      // 为实例vm上设置计算属性
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      /* 下面三个判断就是判断在具体的哪个地方定义了相同的名字 */
      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)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}

defineComputed

// 作用是为target上定义一个属性key,并且属性key的getter和setter根据userDef的值来设置
/* 
  给实例vm上设置计算属性
  target , vm 实例
  key: computed的属性
  userDef: computed的值
*/
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 判断是否应该有缓存,只有在非服务器下,才是TRUE
  /* 只有在非服务器环境下才会有缓存 */
  const shouldCache = !isServerRendering()

  if (typeof userDef === 'function') {
    // sharedPropertyDefinition 是一个默认的属性描述符
    // 也就是说,在浏览器环境下,走了 createComputedGetter 函数
    // 因为userDef只是一个普通的getter,它并没有缓存功能,
    // 所以我们需要额外创建一个具有缓存功能的getter, createComputedGetter
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)

    sharedPropertyDefinition.set = noop

  } else {
    /* userDef为对象 */
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  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
      )
    }
  }
  // 属性key绑定到target上,其中的属性描述符就是上面设置的sharedPropertyDefinition。
  // 如此以来,就将 计算属性 绑定到实例 vm 上了
  // 属性key绑定到target上
  Object.defineProperty(target, key, sharedPropertyDefinition)
}