Vue源码解析:Vue生命周期之从生到死(四)

之前讲到实例的事件初始化,inject等的初始化,那么接下来,我们就说说,state的初始化,也就是我们平时的state,props,methods等等的初始化。接下来就讲讲initState这个函数。

//    src/core/instance/state.js
export 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)
  }
}

给实例上新增了一个属性_watchers,用来存储当前实例中所有的watcher实例,无论是使用vm.$watch注册的watcher实例还是使用watch选项注册的watcher实例,都会被保存到该属性中。 我们在讲vue的数据侦测的时候,说到使用了拦截器,其实,并不是所有的数据都用到了侦测,因为依赖越多,数据量越大,内存消耗也就越大,侦测也就越困难,所以在vue2.0开始,就将数据的侦测下放到了组件,这样可以很好的解决这样的问题。所以,在每个组件添加了_watcher,用来存放这个组件内用到的所有状态的依赖,当其中一个状态发生变化时,就会通知到组件,然后由组件内部使用虚拟DOM进行数据比对,从而降低内存开销,提高性能。

先判断实例中是否有props选项,如果有,就调用props选项初始化函数initProps去初始化props选项;

再判断实例中是否有methods选项,如果有,就调用methods选项初始化函数initMethods去初始化methods选项;

接着再判断实例中是否有data选项,如果有,就调用data选项初始化函数initData去初始化data选项;如果没有,就把data当作空对象并将其转换成响应式;

接着再判断实例中是否有computed选项,如果有,就调用computed选项初始化函数initComputed去初始化computed选项;

最后判断实例中是否有watch选项,如果有,就调用watch选项初始化函数initWatch去初始化watch选项;

总之一句话就是:有什么选项就调用对应的选项初始化子函数去初始化什么选项。

以上就是initState函数的所有逻辑,其实你会发现,在函数内部初始化这5个选项的时候它的顺序是有意安排的,不是毫无章法的。如果你在开发中有注意到我们在data中可以使用props,在watch中可以观察dataprops,之所以可以这样做,就是因为在初始化的时候遵循了这种顺序,先初始化props,接着初始化data,最后初始化watch

初始化props 

props就是子组件接收的来自父组件传递的数据,当在模版解析的时候,当解析到组件标签时会将所有的标签属性都解析出来然后在子组件实例化的时候传给子组件,当然这里面就包括props数据。通过props选项来接收父组件传来的数据:

// 写法一
props: ['name']

// 写法二
props: {
    name: String, // [String, Number]
}

// 写法三
props: {
    name:{
		type: String
    }
}

可以看到,props接收的数据类型不唯一,所以在初始化之前,必须要对props接收的数据做统一规范。 

格式化props的源码:

function normalizeProps (options, vm) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      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
}

当props存在的时候,则声明空对象,然后判断props的类型,如果是数组,遍历该数组中的每一项元素,如果该元素是字符串,那么先将该元素统一转化成驼峰式命名,然后将该元素作为key,将{type: null}作为value存入res中;如果该元素不是字符串,则抛出异常。如果props选项是一个对象,那就遍历对象中的每一对键值,拿到每一对键值后,先将键名统一转化成驼峰式命名,然后判断值是否还是一个对象,如果值是对象(写法三),那么就将该键值对存入res中;如果值不是对象(写法二),那么就将键名作为key,将{type: null}作为value存入res中。如果props选项既不是数组也不是对象,那么如果在非生产环境下就抛出异常,最后将res作为规范化后的结果重新赋值给实例的props选项。最后props应该是这样的:

props: {
    name:{
        type: xxx
    }
}

initProps函数:

//    src/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    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
        )
      }
      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)
    }
  }
  toggleObserving(true)
}

 可以看到,该函数接收俩个函数:vue实例,要被初始化的props.在刚开始的时候,初始化了四个变量:

  • propsData:父组件传入的真实props数据。
  • props:指向vm._props的指针,所有设置到props变量中的属性都会保存到vm._props中。
  • keys:指向vm.$options._propKeys的指针,缓存props对象中的key,将来更新props时只需遍历vm.$options._propKeys数组即可得到所有propskey
  • isRoot:当前组件是否为根组件。

紧接着判断如果组件不是跟组件,那么就不需要将props转化为响应式数据。接着,遍历props选项拿到每一对键值,先将键名添加到keys中,然后调用validateProp函数(关于该函数下面会介绍)校验父组件传入的props数据类型是否匹配并获取到传入的值value,然后将键和值通过defineReactive函数添加到props(即vm._props)中。添加完之后再判断这个key在当前实例vm中是否存在,如果不存在,则调用proxy函数在vm上设置一个以key为属性的代码,当使用vm[key]访问数据时,其实访问的是vm._props[key]

校验父组件的props,用到了validateProp函数。

//    src/core/util/props.js
export function validateProp (key,propOptions,propsData,vm) {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // boolean casting
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      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') {
    assertProp(prop, key, value, vm, absent)
  }
  return value
}

可以看到函数接收了四个参数: 

  • key:遍历propOptions时拿到的每个属性名。
  • propOptions:当前实例规范化后的props选项。
  • propsData:父组件传入的真实props数据。
  • vm:当前实例。

首先内部定义了三个变量:

  • prop:当前keypropOptions中对应的值。
  • absent:当前key是否在propsData中存在,即父组件是否传入了该属性。
  • value:当前keypropsData中对应的值,即父组件对于该属性传入的真实值。

判断proptype属性是否是布尔类型(Boolean),getTypeIndex函数用于判断proptype属性中是否存在某种类型,如果存在,则返回该类型在type属性中的索引(因为type属性可以是数组),如果不存在则返回-1。 

1、如果absenttrue,即父组件没有传入该prop属性并且该属性也没有默认值的时候,将该属性值设置为false,

2、如果父组件传入了该prop属性,那么需要满足以下几点:

  • 该属性值为空字符串或者属性值与属性名相等;
  • proptype属性中不存在String类型;
  • 如果proptype属性中存在String类型,那么Boolean类型在type属性中的索引必须小于String类型的索引,即Boolean类型的优先级更高;

则将该属性值设置为true

另外,在判断属性值与属性名相等的时候,是先将属性名由驼峰式转换成用-连接的字符串,下面的这几种写法,子组件的prop都将被设置为true。

如果不是布尔类型,是其它类型的话,那就只需判断父组件是否传入该属性即可,如果没有传入,则该属性值为undefined,此时调用getPropDefaultValue函数(关于该函数下面会介绍)获取该属性的默认值,并将其转换成响应式。

如果父组件传入了该属性并且也有对应的真实值,那么在非生产环境下会调用assertProp函数(关于该函数下面会介绍)校验该属性值是否与要求的类型相匹配。

最后将父组件传入的该属性的真实值返回。

getPropDefaultValue函数:

//    src/core/util/props.js
function getPropDefaultValue (vm, prop, key){
  // no default, return undefined
  if (!hasOwn(prop, 'default')) {
    return undefined
  }
  const def = prop.default
  // warn against non-factory defaults for Object & Array
  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
    )
  }
  // the raw prop value was also undefined from previous render,
  // return previous default value to avoid unnecessary watcher trigger
  if (vm && vm.$options.propsData &&
    vm.$options.propsData[key] === undefined &&
    vm._props[key] !== undefined
  ) {
    return vm._props[key]
  }
  // call factory function for non-Function types
  // a value is Function if its prototype is function even across different execution context
  return typeof def === 'function' && getType(prop.type) !== 'Function'
    ? def.call(vm)
    : def
}

该函数接收三个参数,分别是:

  • vm:当前实例;
  • prop:子组件props选项中的每个key对应的值;
  • key:子组件props选项中的每个key

其作用是根据子组件props选项中的key获取其对应的默认值。

首先判断prop中是否有default属性,如果没有,则表示没有默认值,直接返回。

如果有则取出default属性,赋给变量def。接着判断在非生产环境下def是否是一个对象,如果是,则抛出警告:对象或数组默认值必须从一个工厂函数获取。

接着,再判断如果父组件没有传入该props属性,但是在vm._props中有该属性值,这说明vm._props中的该属性值就是默认值。

最后,判断def是否为函数并且prop.type不为Function,如果是的话表明def是一个返回对象或数组的工厂函数,那么将函数的返回值作为默认值返回;如果def不是函数,那么则将def作为默认值返回。

assertProp函数:

//    src/core/util/props.js
function assertProp (prop,name,value,vm,absent) {
  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])
      expectedTypes.push(assertedType.expectedType || '')
      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
  if (validator) {
    if (!validator(value)) {
      warn(
        'Invalid prop: custom validator check failed for prop "' + name + '".',
        vm
      )
    }
  }
}

该函数接收5个参数,分别是:

  • prop:prop选项;
  • name:propsprop选项的key;
  • value:父组件传入的propsDatakey对应的真实数据;
  • vm:当前实例;
  • absent:当前key是否在propsData中存在,即父组件是否传入了该属性。

其作用是校验父组件传来的真实值是否与proptype类型相匹配,如果不匹配则在非生产环境下抛出警告。

函数内部首先判断prop中如果设置了必填项(即prop.requiredtrue)并且父组件又没有传入该属性,此时则抛出警告:提示该项必填。

接着判断如果该项不是必填的并且该项的值value不存在,那么此时是合法的,直接返回。

接下来定义了三个变量

  • type:prop中的type类型;
  • valid:校验是否成功;
  • expectedTypes:保存期望类型的数组,当校验失败抛出警告时,会提示用户该属性所期望的类型是什么;

通常情况下,type可以是一个原生构造函数,也可以是一个包含多种类型的数组,还可以不设置该属性。如果用户设置的是原生构造函数或数组,那么此时vaild默认为false!type),如果用户没有设置该属性,表示不需要校验,那么此时vaild默认为true,即校验成功。

另外,当type等于true时,即出现这样的写法:props:{name:true},这说明prop一定会校验成功。所以当出现这种语法的时候,此时type === true,所以vaild默认为true

接下来开始校验类型,如果用户设置了type属性,则判断该属性是不是数组,如果不是,则统一转化为数组,方便后续处理。

接下来遍历type数组,并调用assertType函数校验valueassertType函数校验后会返回一个对象。然后将被校验的类型添加到expectedTypes中,并将vaild变量设置为assertedType.valid。这里请注意:上面循环中的条件语句有这样一个条件:!vaild,即type数组中还要有一个校验成功,循环立即结束,表示校验通过。

接下来,如果循环完毕后vaildfalse,即表示校验未通过,则抛出警告。prop选项还支持自定义校验函数。所以还需要使用用户传入的自定义校验函数来校验数据。首先获取到用户传入的校验函数,调用该函数并将待校验的数据传入,如果校验失败,则抛出警告。

初始化method

//    src/core/instance/state.js
function initMethods (vm, methods) {
  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 $.`
        )
      }
    }
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}

初始化methods无非就干了三件事:判断method有没有?method的命名符不符合命名规范?如果method既有又符合规范那就把它挂载到vm实例上。

首先,遍历methods选项中的每一个对象,在非生产环境下判断如果methods中某个方法只有key而没有value,即只有方法名没有方法体时,抛出异常:提示用户方法未定义。

接着判断如果methods中某个方法名与props中某个属性名重复了,就抛出异常:提示用户方法名重复了。

接着判断如果methods中某个方法名如果在实例vm中已经存在并且方法名是以_$开头的,就抛出异常:提示用户方法名命名不规范。

其中,isReserved函数是用来判断字符串是否以_$开头。最后,如果上述判断都没问题,那就method绑定到实例vm上,这样,我们就可以通过this.xxx来访问methods选项中的xxx方法了。

初始化data

//    src/core/instance/state.js
function initData (vm) {
    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]
        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)) {
            proxy(vm, `_data`, key)
        }
    }
    // observe data
    observe(data, true /* asRootData */)
}

首先获取到用户传入的data选项,赋给变量data,同时将变量data作为指针指向vm._data,然后判断data是不是一个函数,如果是就调用getData函数获取其返回值,将其保存到vm._data中。如果不是,就将其本身保存到vm._data中。无论传入的data选项是不是一个函数,它最终的值都应该是一个对象,如果不是对象的话,就抛出警告:提示用户data应该是一个对象。

接下来遍历data对象中的每一项,在非生产环境下判断data对象中是否存在某一项的keymethods中某个属性名重复,如果存在重复,就抛出警告:提示用户属性名重复。

接着再判断是否存在某一项的keyprop中某个属性名重复,如果存在重复,就抛出警告:提示用户属性名重复。

如果都没有重复,则调用proxy函数将data对象中key不以_$开头的属性代理到实例vm上,这样,我们就可以通过this.xxx来访问data选项中的xxx数据了。

最后,调用observe函数将data中的数据转化成响应式。

初始化computed

计算属性的特点:计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。如何初始化computed呢?

//    src/core/instance/state.js
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]
        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
            )
        }

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

首先定义了一个变量watchers并将其赋值为空对象,同时将其作为指针指向vm._computedWatchers。

接着,遍历computed选项中的每一项属性,首先获取到每一项的属性值,记作userDef,然后判断userDef是不是一个函数,如果是函数,则该函数默认为取值器getter,将其赋值给变量getter;如果不是函数,则说明是一个对象,则取对象中的get属性作为取值器赋给变量getter

接着判断在非生产环境下如果上面两种情况取到的取值器不存在,则抛出警告:提示用户计算属性必须有取值器。

接着判断如果不是在服务端渲染环境下,则创建一个watcher实例,并将当前循环到的的属性名作为键,创建的watcher实例作为值存入watchers对象中。

最后,判断当前循环到的的属性名是否存在于当前实例vm上,如果存在,则在非生产环境下抛出警告;如果不存在,则调用defineComputed函数为实例vm上设置计算属性。

defineComputed函数:

//    src/core/instance/state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (target,key,userDef) {
  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)
}

该函数接受3个参数,分别是:targetkeyuserDef。其作用是为target上定义一个属性key,并且属性keygettersetter根据userDef的值来设置。下面我们就来看一下该函数的具体逻辑。

首先定义了变量sharedPropertyDefinition,它是一个默认的属性描述符。

接着,在函数内部定义了变量shouldCache,用于标识计算属性是否应该有缓存。该变量的值是当前环境是否为非服务端渲染环境,如果是非服务端渲染环境则该变量为true。也就是说,只有在非服务端渲染环境下计算属性才应该有缓存。

接着,判断如果userDef是一个函数,则该函数默认为取值器getter,此处在非服务端渲染环境下并没有直接使用userDef作为getter,而是调用createComputedGetter函数(关于该函数下面会介绍)创建了一个getter,这是因为userDef只是一个普通的getter,它并没有缓存功能,所以我们需要额外创建一个具有缓存功能的getter,而在服务端渲染环境下可以直接使用userDef作为getter,因为在服务端渲染环境下计算属性不需要缓存。由于用户没有设置setter函数,所以将sharedPropertyDefinition.set设置为noop。

如果userDef不是一个函数,那么就将它当作对象处理。在设置sharedPropertyDefinition.get的时候先判断userDef.get是否存在,如果不存在,则将其设置为noop,如果存在,则同上面一样,在非服务端渲染环境下并且用户没有明确的将userDef.cache设置为false时调用createComputedGetter函数创建一个getter赋给sharedPropertyDefinition.get。然后设置sharedPropertyDefinition.setuserDef.set函数。

接着,再判断在非生产环境下如果用户没有设置setter的话,那么就给setter一个默认函数,这是为了防止用户在没有设置setter的情况下修改计算属性,从而为其抛出警告。

最后调用Object.defineProperty方法将属性key绑定到target上,其中的属性描述符就是上面设置的sharedPropertyDefinition。如此以来,就将计算属性绑定到实例vm上了。

createComputedGetter函数:

//    src/core/instance/state.js
function createComputedGetter (key) {
    return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            watcher.depend()
            return watcher.evaluate()
        }
    }
}

该函数是一个高阶函数,其内部返回了一个computedGetter函数,所以其实是将computedGetter函数赋给了sharedPropertyDefinition.get。当获取计算属性的值时会执行属性的getter,而属性的getter就是 sharedPropertyDefinition.get,也就是说最终执行的 computedGetter函数。

computedGetter函数内部,首先存储在当前实例上_computedWatchers属性中key所对应的watcher实例,如果watcher存在,则调用watcher实例上的depend方法和evaluate方法,并且将evaluate方法的返回值作为计算属性的计算结果返回。

 depend和evaluate:

export default class Watcher {
    constructor (vm,expOrFn,cb,options,isRenderWatcher) {
        if (options) {
            // ...
            this.computed = !!options.computed
            // ...
        } else {
            // ...
        }

        this.dirty = this.computed // for computed watchers
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn
        }

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

    evaluate () {
        if (this.dirty) {
            this.value = this.get()
            this.dirty = false
        }
        return this.value
    }

    /**
     * Depend on this watcher. Only for computed property watchers.
     */
    depend () {
        if (this.dep && Dep.target) {
            this.dep.depend()
        }
    }

    update () {
        if (this.computed) {
            if (this.dep.subs.length === 0) {
                this.dirty = true
            } else {
                this.getAndInvoke(() => {
                    this.dep.notify()
                })
            }
        }
    }

    getAndInvoke (cb: Function) {
        const value = this.get()
        if (
            value !== this.value ||
            // Deep watchers and watchers on Object/Arrays should fire even
            // when the value is the same, because the value may
            // have mutated.
            isObject(value) ||
            this.deep
        ) {
            // set new value
            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)
            }
        }
    }
}


const computedWatcherOptions = { computed: true }
watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
)

可以看到,在实例化Watcher类的时候,第四个参数传入了一个对象computedWatcherOptions = { computed: true },该对象中的computed属性标志着这个watcher实例是计算属性的watcher实例,即Watcher类中的this.computed属性,同时类中还定义了this.dirty属性用于标志计算属性的返回值是否有变化,计算属性的缓存就是通过这个属性来判断的,每当计算属性依赖的数据发生变化时,会将this.dirty属性设置为true,这样下一次读取计算属性时,会重新计算结果返回,否则直接返回之前的计算结果。

当调用watcher.depend()方法时,会将读取计算属性的那个watcher添加到计算属性的watcher实例的依赖列表中,当计算属性中用到的数据发生变化时,计算属性的watcher实例就会执行watcher.update()方法,在update方法中会判断当前的watcher是不是计算属性的watcher,如果是则调用getAndInvoke去对比计算属性的返回值是否发生了变化,如果真的发生变化,则执行回调,通知那些读取计算属性的watcher重新执行渲染逻辑。

当调用watcher.evaluate()方法时,会先判断this.dirty是否为true,如果为true,则表明计算属性所依赖的数据发生了变化,则调用this.get()重新获取计算结果最后返回;如果为false,则直接返回之前的计算结果。

 

初始化watch

watch选项是一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。既然给用户提供的用法灵活,那么在代码中就需要按条件来判断,根据不同的用法做相应的处理。

initWatch函数:

//    src/core/instance/state.js
function initWatch (vm, watch) {
  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选项,拿到每一项的key和对应的值handler。然后判断handler是否为数组,如果是数组则循环该数组并将数组中的每一项依次调用createWatcher函数来创建watcher;如果不是数组,则直接调用createWatcher函数来创建watcher

createWatcher函数:

//    src/core/instance/state.js
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)
}

该函数接收4个参数,分别是:

  • vm:当前实例;
  • expOrFn:被侦听的属性表达式
  • handler:watch选项中每一项的值
  • options:用于传递给vm.$watch的选项对象

通过handler来判断,传入的是什么类型的数据,最后返回不同的结果。 

针对不同类型的值处理完毕后,expOrFn是被侦听的属性表达式,handler变量是回调函数,options变量为侦听选项,最后,调用vm.$watcher方法(关于该方法在介绍全局实例方法的时候会详细介绍)并传入以上三个参数完成初始化watch

以上就是初始化state的内容,未完,待续。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值