之前讲到实例的事件初始化,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
中可以观察data
和props
,之所以可以这样做,就是因为在初始化的时候遵循了这种顺序,先初始化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
数组即可得到所有props
的key
。 - 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:当前
key
在propOptions
中对应的值。 - absent:当前
key
是否在propsData
中存在,即父组件是否传入了该属性。 - value:当前
key
在propsData
中对应的值,即父组件对于该属性传入的真实值。
判断prop
的type
属性是否是布尔类型(Boolean),getTypeIndex
函数用于判断prop
的type
属性中是否存在某种类型,如果存在,则返回该类型在type
属性中的索引(因为type
属性可以是数组),如果不存在则返回-1。
1、如果absent
为true
,即父组件没有传入该prop
属性并且该属性也没有默认值的时候,将该属性值设置为false,
2、如果父组件传入了该prop
属性,那么需要满足以下几点:
- 该属性值为空字符串或者属性值与属性名相等;
prop
的type
属性中不存在String
类型;- 如果
prop
的type
属性中存在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:
props
中prop
选项的key
; - value:父组件传入的
propsData
中key
对应的真实数据; - vm:当前实例;
- absent:当前
key
是否在propsData
中存在,即父组件是否传入了该属性。
其作用是校验父组件传来的真实值是否与prop
的type
类型相匹配,如果不匹配则在非生产环境下抛出警告。
函数内部首先判断prop
中如果设置了必填项(即prop.required
为true
)并且父组件又没有传入该属性,此时则抛出警告:提示该项必填。
接着判断如果该项不是必填的并且该项的值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
函数校验value
。assertType
函数校验后会返回一个对象。然后将被校验的类型添加到expectedTypes
中,并将vaild
变量设置为assertedType.valid。
这里请注意:上面循环中的条件语句有这样一个条件:!vaild
,即type
数组中还要有一个校验成功,循环立即结束,表示校验通过。
接下来,如果循环完毕后vaild
为false
,即表示校验未通过,则抛出警告。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
对象中是否存在某一项的key
与methods
中某个属性名重复,如果存在重复,就抛出警告:提示用户属性名重复。
接着再判断是否存在某一项的key
与prop
中某个属性名重复,如果存在重复,就抛出警告:提示用户属性名重复。
如果都没有重复,则调用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个参数,分别是:target
、key
和userDef
。其作用是为target
上定义一个属性key
,并且属性key
的getter
和setter
根据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.set
为userDef.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的内容,未完,待续。。。。