Vue源码学习之initState

Vue源码学习之initState

这次学习的initState方法,这个方法应该是整个Vue实例初始化过程中最重要的方法之一了,我们经常使用的属性,包括像是data,props,methods,watch,computed等都是在这个方法中进行初始化的。该方法介于beforeCreate和created两个钩子之间,所以在beforeCreate的时候我们还无法访问到Vue实例上的data,props,methods等属性。所以让我们看一下它的代码吧:

function initState (vm: Component) {
  vm._watchers = []
  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)
  }
}

整个方法没有什么特别的,对属性进行初始化的部分都是交由其他的方法实现,下面一个个分析这些初始化的过程。

1、initProps
  // 存放父组件传入子组件的props
  const propsData = vm.$options.propsData || {}
  // 存放经过转换后的最终的props的对象
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  // 一个存放props的key的数组,就算props的值是空的,key也会存在里面
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent

前几行就是一些常规的赋值,propsData是父组件子组件传的参数,_props是最后存props的值的对象,keys是props的key存储的地方,我们可以通过这个数组去遍历props,isRoot则是判断是不是根元素。

for (const key in propsOptions) {
    keys.push(key)
    // 校验props,包括对类型的校验以及产生最后的属性值
    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
        )
      }
      // 将props变成可响应的,非生产环境中,如果用户修改props,发出警告
      defineReactive(props, key, value, () => {
        if (vm.$parent && !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 {
      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.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }

在这段代码中,首先遍历propsOptions也就是options中的props,将key字段push到keys数组里,然后通过validateProp方法对prop进行校验,主要是对prop的类型校验,以及判断父组件是否有传入相应的值,如果没有,则查看子组件中有没有声明default属性,有则将default产生的值作为props的value。

在校验完props之后就是使用defineReactive将prop变成可响应的,这样当prop发生变化的时候,对应的依赖组件可以同步变化。在非生产环境中,如果修改prop的值则会发出警告。

最后使用proxy将不在vm上的属性代理到Vue实例上,让我们在组件里可以使用this[key]的方式调用props[key]的值。

2、initMethods

initState中调用的第二个方法是用于初始化methods的方法initMethods,该方法比较简单,就是遍历$options的传入methods,把每一个method绑定到当前vue实例上,并将method的this指向当前vue实例。

function initMethods (vm: Component, methods: Object) {
  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
        )
      }
      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 $.`
        )
      }
    }
    // 把每个methods[key]绑定到vm上,并将methods[key]的this指向vm
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}
3、initData

initData用于初始化我们传入Vue实例或者组件的data,下面就来一行一行看下代码吧。

// 获取$options里的data
let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

首先获取$options里的data,判断data是不是个方法,因为data在创建Vue实例的时候可以传入对象也可以传入方法,但是在创建组件的时候就只能传入方法,为什么Vue要这样区别,请参考Vue官网的解释。如果传入的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
    )
  }

接下来判断是否为普通对象类型,不是普通对象则将data赋值为空对象,并在非生产环境发出警告。

  // proxy data on instance(把data里的每个属性代理到vm实例上)
  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)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    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)) {
      // 判断key的首字母是否合法
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)

最后就是将对data中的每一个属性进行判断,包括判断属性是否存在methods中和属性是否存在props中,如果都不存在,则将data[key]使用proxy代理到vm上,方便我们调用,再使用observe方法将整个data变为可响应的,observe方法使用了Object.defineProperty对数据进行劫持,让其达到可响应的行为。

还有一种情况是用户没有在$options中传入data的情况,这时候会执行

observe(vm._data = {}, true /* asRootData */)

默认把data变为空对象,并observe。

4、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()

  for (const key in computed) {
    const userDef = computed[key]
    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) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    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)
      }
    }
  }
}

在代码中首先定义了一个存放Watcher的空对象watchers,然后定义判断是不是SSR的变量isSSR,接着遍历我们传入的computed对象,将computed[key]赋值给userDef,并定义了一个getter。接着就是在不是SSR的情况下,新建了一个Watcher,并赋值给watchers[key],这里用到了之前的getter,这个getter实际上就是当Watcher观测到数据修改的执行的方法。

接着就是使用defineComputed方法将userDef放到vm实例上,让我们可以直接通过this调用。

function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.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
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
  }
}

defineComputed中使用sharedPropertyDefinition将每个computed挂载在vm对象上,值得注意的是在不是SSR的情况下sharedPropertyDefinition的get方法是通过createComputedGetter创建的,在createComputedGetter使用到了watcher的depend函数,让watcher和dep之间建立的联系,成功将computed变成响应式的。

5、initWatch

接下来介绍的是initState中的最后一个方法initWatch,顾名思义我们可以知道这个方法是用于初始化watch的。

我们使用watch的用途一般都是监听一个数据,当这个数据发生变化时,执行一些操作,所以实现watch原理比较简单,就是新建一个watcher,并将watcher放入到对应数据的Observer的Dep中就可以了,这样当监听的数据变化时,就会通知我们的Watcher执行我们传入的函数。

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

第一个部分代码比较简单,做的工作是遍历传入的watch对象,通过createWatcher创建一个个的watcher。

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

第二部分createWatcher方法就是对handler的一些判断,然后使用$watch方法去监听该数据,并执行handler。

Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
    var vm = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    var watcher = new Watcher(vm, expOrFn, cb, options);
    if (options.immediate) {
      cb.call(vm, watcher.value);
    }
    return function unwatchFn () {
      watcher.teardown();
    }
  };

$watch方法简单的说就是根据传入的参数创建一个Watcher。

总结

这次说到的initState方法中的五个方法,其中initProps,initMetods相对来说比较简单,就是获取数据,然后对数据做一些处理,最后将数据挂载到vm实例上。其他三个initData,initComputed,initWatch中比较复杂的地方在于它们有用到一些vue中双向绑定的东西,像是Watcher,Observer,不了解相关知识的同学可能会有些迷惑,除了这部分东西,其他的其实也并不复杂。

猜你喜欢

转载自blog.csdn.net/qq_34179086/article/details/88081590