(三)小菜鸡的Vue源码阅读——Vue实例创建时initState干了哪些事

326 阅读4分钟

0.前情提要

_init函数中调用initState()

// core/init.js
Vue.prototype._init = function(){
    // ...
    initState(vm)
    // ...
}

// core/instance/state.js
export function initState (vm: Component) {
  vm._watchers = []
  // 这个是已经合并后的options
  const 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)
  }
}

分别初始化了options中的五个属性,增加了_watchers的实例属性

  1. props
  2. methods
  3. data
  4. computed
  5. watch 下面依次看一下源码中是如何对这五个属性初始化的

1.props

1.1 initProps主要干了这么几件事

  1. 在实例上挂载了_props$options._propKeys
  2. 通过propsDataprops声明定义的规则,过滤出真正传入的值
  3. props定义的属性通过defineReactive函数进行响应式,且添加到_props
  4. _props上的属性代理到vm实例上(不允许覆盖本身的属性)
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  
  // $options._propKeys属性在这里被增加
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  
  if (!isRoot) {
    toggleObserving(false)
  }
  
  // 遍历props属性
  for (const key in propsOptions) {
    keys.push(key)
    // 验证传的propsData和声明的props规则,过滤后确定最后拿到的值
    const value = validateProp(key, propsOptions, propsData, vm)
    
    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
        )
      }
      // 第四个参数是自定义setter函数,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)
    }
    
    // 如果vm中本身没有这个属性才代理
    if (!(key in vm)) {
      // 可以通过vue实例直接访问到props上了
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}


export function toggleObserving (value: boolean) {
  shouldObserve = value
}

1.2 validateProp

通过之前配置的props的一些规则,和传入的propsData,返回真正传给实例的值,起到一个过滤转换的功能,主要是对以下几种特殊情况做判断

  1. 传了空字符串
  2. 传的值等于属性名本身
  3. 未传值,需要去获取默认值
export function validateProp (key: string,propOptions: Object,propsData: Object,vm?: Component): any {
  const prop = propOptions[key]
  // propsData是否传了改prop
  const absent = !hasOwn(propsData, key)
  // 获取propsData值
  let value = propsData[key]
  // 该类型是否是Boolean类型
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    // 没传propsData且也没有默认的情况下,值是false
    if (absent && !hasOwn(prop, 'default')) {
      value = false
      // 当值传了,但是是空字符串或则是这个属性名本身的驼峰命名(经常通过这样简写的方式来表示true)
      //那么就要考虑一下props的定义里面有没有别的接受类型了
    } else if (value === '' || value === hyphenate(key)) {
      // 不是string类型,或则boolean类型在数组前面的时候,那么boolean的优先级更高,才可以置true
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  
  // 没有传值
  if (value === undefined) {
    // 获取到默认值,当然也可能没有默认值,那就是undefined了
    value = getPropDefaultValue(vm, prop, key)
    // 对默认值进行一个观测,这个observe算是重头戏,放到后面讲
    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
}


1.3 getPropDefaultValue

根据props的声明规则去获取实际应该拿到的默认值

注意:默认值是对象的情况,需要定义工厂函数来返回

// 获取默认值
function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
  // 没有声明默认值
  if (!hasOwn(prop, 'default')) {
    return undefined
  }
  const def = prop.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]
  }
  // 入股默认值是工厂函数的时候,判断一下类型本省不是函数,那么就执行这个工厂函数把其返回值当做默认值
  return typeof def === 'function' && getType(prop.type) !== 'Function'
    ? def.call(vm)
    : def
}

1.4 assertProp验证prop属性是否有效

无效也不会怎么样,还是会传值,只不过会开发环境下报一些警告,可跳过

function assertProp (prop: PropOptions,name: string,value: any,vm: ?Component,absent: boolean) {
  // 要求传,却没传
  if (prop.required && absent) {
    warn(
      'Missing required prop: "' + name + '"',
      vm
    )
    return
  }
  if (value == null && !prop.required) {
    return
  }
  let type = prop.type
  let valid = !type || type === true
  const expectedTypes = []
  if (type) {
    // 强制转换成数组
    if (!Array.isArray(type)) {
      type = [type]
    }
    for (let i = 0; i < type.length && !valid; i++) {
      const assertedType = assertType(value, type[i], vm)
      // 返回期待的类型,和现在传入的值是否符合有效
      expectedTypes.push(assertedType.expectedType || '')
      valid = assertedType.valid
    }
  }

  // 只有有规定过一种类型以上
  const haveExpectedTypes = expectedTypes.some(t => t)
  // 且最终传入的值和规定的一种都不符合
  if (!valid && haveExpectedTypes) {
    // 发出警告
    warn(
      getInvalidTypeMessage(name, value, expectedTypes),
      vm
    )
    return
  }
  // 自定义的验证器
  const validator = prop.validator
  if (validator) {
    if (!validator(value)) {
      warn(
        'Invalid prop: custom validator check failed for prop "' + name + '".',
        vm
      )
    }
  }
}

1.5 defineReactive

对于props声明的每一个属性进项响应式处理,对于对象需要递归进行响应式处理,这里的递归方式很特别,通过传入的shallow参数来决定是否要深度的监听,如果是,那么就observe继续观测这个对象,观测过程中再调用defineReavtive,来响应式他这个对象的每一个属性。

  1. 每一个属性就生成一个Dep实例,来收集订阅
  2. 这个属性的定义configurable必须是可配置的
  3. 如果原来就有setter,重新刚给他设置一遍(个人理解,待斟酌)
  4. 通过Object.definePrototype来重写setter,getter钩子,实现发布订阅模式
export function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean) { 
  // 每一个属性就生成一个dep实例,来收集订阅
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key
  // 描述符无法改变
  if (property && property.configurable === false) {
    return
  }

  // 原先setter函数存在,且现在也没有自定义的setter,那么value重新取一遍
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  // shallow参数表示是否深度监听
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 模板解析的时候会赋值,由于js单线程解析,所以全局只需要判断Dep.target就行了
      if (Dep.target) {
        // 会把push(Dep.target)
        dep.depend()
        // 深度监听
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // 当数据没有发生变化就不需要发布通知了
      // 后面这个逻辑我怀疑是自定义的getter重写了之后让他的值发生变化,导致死循环渲染,所以干脆不渲染了
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      // 原来这个自定义的sette只在开发环境起作用啊
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // 没看懂,源码好像为了解决一个issue加的
      if (getter && !setter) return
      // 更改设置的值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 新值有可能是对象或者数组,还是要重新观察一下
      childOb = !shallow && observe(newVal)
      // 通知订阅者
      dep.notify()
    }
  })
}


// 如果属性值是一个数组,那么给数组的每一个元素都去加订阅
// 代码较短大家自己看一下
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    // 递归调用一下
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

1.6 Observe

  1. 对于一个属性是对象才会观测,观测结果是给属性值增加一个__ob__属性
  2. 如果是数组的话那么对每个元素循环观测就行了,但还是要符合条件(1)
  3. 数组直接下标修改不会触发响应,源码重写了数组的原型方法,按照是否可以挂载__proto__,分别通过继承和definePrototype挂载重写的方法
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // ...
  
  // 该属性下存在__ob__属性就返回,否则创建一个
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    // 对数组和对象才会观测
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  // ...
  return ob
}

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  // 构造函数
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 挂载__ob__属性
    def(value, '__ob__', this)
    // 改属性值是一个数组
    if (Array.isArray(value)) {
      if (hasProto) {
        // 原型链继承方式
        protoAugment(value, arrayMethods)
      } else {
        // definePrototype声明方式
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 数组每一个元素都需要要观察
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  // 遍历每个属性都给他加响应式处理
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 至此defineReactive和observe两个函数形成了递归调用的关系
      defineReactive(obj, keys[i])
    }
  }

  // 数组元素循环观测
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      // 如果这个值是简单数据的话,那么就不会调用defineReactive了,也就是说数组里面的东西不会再响应式了
      observe(items[i])
    }
  }
}

1.7 Dep

  1. addSub:添加订阅
  2. remove:移除订阅
  3. depend:将全局遍历Dep.target添加订阅
  4. notify:通知订阅者该更新视图了
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    // 维护一个订阅的数组
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // 为了下面排序会打乱,先拿一份拷贝
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    // 触发订阅者的视图更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

简单小结

  1. props每个属性做defineReactive
  2. defineReactive 为每一个属性维护一个Dep实例,且重写这个属性的钩子,并对这个属性值做观察observe
  3. observe只会观察对象或者数组,而不会观察简单数据,观察的时候给属性值增加一个属性__ob__ = new Observe()
  4. Observe实例会重写数组的一些方法来使得数组变化可以响应,对属性值的每一个属性递归调用(1),如果是数组那再对每一个元素循环调用(3)而不是(1)因为数组里的元素可能还是数组

2. methods

2.1 initMethods

  1. 开发环境下的一些警告
  • 必须是函数
  • 不能和props同名
  • 和vue实例中已经定义过的api同名
  1. 生产环境下将不是函数的情况转换为空函数
function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      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)
  }
}

3. data

3.1 initData

  1. 对函数声明和对象声明两种方式的统一
  2. 对最终返回数据类型的容错
  3. 对同名的警告
  4. _data代理到vm实例上
  5. observe(data)使得其响应式

props是将每一个属性defineReactive而data是直接observe(data, true),直接带来的区别是vm._data.__ob__有而vm._props___ob__没有,后续再看看这里为啥要这么高

function initData (vm: Component) {
  let data = vm.$options.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
  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]
    // 和methods同名
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    // 和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代理带vue实例上
      proxy(vm, `_data`, key)
    }
  }
  // 使得data响应式
  observe(data, true /* asRootData */)
}

4. computed

类似于data,他所依赖的响应式数据更新的时候,他也会更新,可以自定义set逻辑

4.1 initComputed

  1. vm挂载_computedWatchers属性
  2. 格式化输入(有两种声明方式),获取getter
  3. new Watcher()挂载到_computedWatchers
  4. defineComputed()该函数做主要工作
  5. 开发环境下同名警告
function initComputed (vm: Component, computed: Object) {
  
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    // computed 有函数和对象两种声明方式
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // 新建watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    if (!(key in 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)
      }
    }
  }
}

4.2 defineComputed

  • computed可以直接声明函数,或者声明对象{get:function(){},set:function(){}}
  • set可以直接赋值给definePrototype中的set
export function defineComputed (target: any,key: string,userDef: Object | Function) {
  const shouldCache = !isServerRendering()
  
  // 看起来比较长,其实就是判断是否有自带的set,然后包装一下get
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    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
      )
    }
  }
  // 把computed属性挂载到vm实例上,并定义了getter和setter钩子
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

4.3 createComputedGettercreateGetterInvoker

类似于defineReactive函数重写gettersetter,在getterdepend提一嘴,这个setter用来v-model的时候挺好用的,特别是如果想直接用vuex里的state来做双向绑定的话

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

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 暂时没看懂,等看到watcher类的时候再回来看看
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

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

5 watch

声明方式:[key:string]: (function|Object)|(function|Object)[]

注意属性可以是一个表达式

5.1 initWatch

适应不同声明方式并调用creatWatcher()

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

5.2 createWatcher

function createWatcher (vm: Component,expOrFn: string | Function,handler: any,options?: Object) {
   // 改声明是一个对象的话,那么整个都可以当做是配置
  if (isPlainObject(handler)) {
    options = handler
    // 回调函数是其中的handler属性
    handler = handler.handler
  }
  // handel是字符串,那去vm的实例中找
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

望大佬多指正,未完待续~