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,不了解相关知识的同学可能会有些迷惑,除了这部分东西,其他的其实也并不复杂。