系列文章
- [Vue源码学习] new Vue()
- [Vue源码学习] 配置合并
- [Vue源码学习] $mount挂载
- [Vue源码学习] _render(上)
- [Vue源码学习] _render(下)
- [Vue源码学习] _update(上)
- [Vue源码学习] _update(中)
- [Vue源码学习] _update(下)
- [Vue源码学习] 响应式原理(上)
- [Vue源码学习] 响应式原理(中)
- [Vue源码学习] 响应式原理(下)
- [Vue源码学习] props
- [Vue源码学习] computed
- [Vue源码学习] watch
- [Vue源码学习] 插槽(上)
- [Vue源码学习] 插槽(下)
前言
从之前的章节中,我们知道Vue是如何将普通数据转换为响应式数据,但是组件除了拥有自身的数据外,还可以接收来自父组件中传入的数据,那么在本章节中,我们就来看看Vue是如何处理来自外部的数据。
propsData
由于props选项是用来接收来自父组件的数据,所以首先得从父组件构造propsData说起。在创建组件的父占位符的过程中,会调用Vue.extend方法,构造子组件构造器,在此过程中,会调用normalizeProps方法,规范化组件的props选项,代码如下所示:
/* core/util/options.js */
function normalizeProps(options: Object, vm: ?Component) {
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选项支持数组、对象等多种书写格式,所以需要使用normalizeProps方法,使每个数据的配置都规范化为统一的格式,从而方便之后的解析。在规范化完成后,在Vue.extend方法中,还会调用initProps方法,将props代理到组件的原型上,因为这部分数据是可以共享的,其代码如下所示:
/* core/global-api/extend.js */
Vue.extend = function (extendOptions: Object): Function {
// ...
if (Sub.options.props) {
initProps(Sub)
}
// ...
}
function initProps(Comp) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
可以看到,在initProps方法中,主要就是将数据代理到组件原型上的_props对象中。处理完props选项后,就调用extractPropsFromVNodeData方法来构建propsData,代码如下所示:
/* core/vdom/create-component.js */
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// ...
}
/* core/vdom/helpers/extract-props.js */
export function extractPropsFromVNodeData(
data: VNodeData,
Ctor: Class<Component>,
tag?: string
): ?Object {
// we are only extracting raw values here.
// validation and default values are handled in the child
// component itself.
const propOptions = Ctor.options.props
if (isUndef(propOptions)) {
return
}
const res = {}
const { attrs, props } = data
if (isDef(attrs) || isDef(props)) {
for (const key in propOptions) {
const altKey = hyphenate(key)
if (process.env.NODE_ENV !== 'production') {
const keyInLowerCase = key.toLowerCase()
if (
key !== keyInLowerCase &&
attrs && hasOwn(attrs, keyInLowerCase)
) {
tip(
`Prop "${keyInLowerCase}" is passed to component ` +
`${formatComponentName(tag || Ctor)}, but the declared prop name is` +
` "${key}". ` +
`Note that HTML attributes are case-insensitive and camelCased ` +
`props need to use their kebab-case equivalents when using in-DOM ` +
`templates. You should probably use "${altKey}" instead of "${key}".`
)
}
}
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey, false)
}
}
return res
}
可以看到,在extractPropsFromVNodeData方法中,首先从组件配置中提取刚刚处理过的props选项,并赋值给propOptions,然后从VNodeData中提取attrs和props,接着开始遍历propOptions,处理每一个数据,首先调用hyphenate方法,将属性名处理成连字符的形式,然后使用checkProp方法尝试从attrs或props中提取数据,代码如下所示:
/* core/vdom/helpers/extract-props.js */
function checkProp(
res: Object,
hash: ?Object,
key: string,
altKey: string,
preserve: boolean
): boolean {
if (isDef(hash)) {
if (hasOwn(hash, key)) {
res[key] = hash[key]
if (!preserve) {
delete hash[key]
}
return true
} else if (hasOwn(hash, altKey)) {
res[key] = hash[altKey]
if (!preserve) {
delete hash[altKey]
}
return true
}
}
return false
}
可以看到,首先尝试从props中提取数据,否则尝试从attrs中提取数据,如果数据存在于attrs中,还会将数据从attrs中删除。
经过extractPropsFromVNodeData方法处理后,就可以得到从父组件中传到子组件的数据propsData,在createComponent方法的最后,会将propsData放到VNode.componentOptions中。
从前面的章节中,我们知道,在父组件patch的过程中,会创建子组件的实例,同时也会将组件的父占位符VNode当作配置选项传入,所以在子组件调用initInternalComponent方法进行配置合并的过程中,就可以从父占位符VNode中提取propsData。那么接下来,就来看看子组件是如何处理该数据的。
initProps
在初始化子组件,得到propsData后,会调用initState方法,在该方法中,又会调用initProps方法,这个方法就是用来处理propsData的,代码如下所示:
/* core/instance/state.js */
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
// ...
}
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 (!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 {
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)
}
可以看到,在initProps方法中,对于非根组件实例,会调用toggleObserving方法,阻止嵌套数据的响应式化,然后遍历组件的props选项,对每个数据使用validateProp方法,该方法是用来检测数据是否满足配置中的type、required、validator选项,在不满足时会提示警告,如果有default选项的话,在没有传入数据时返回默认值,validateProp方法最终会返回对应的数据。
在得到数据之后,就是调用defineReactive方法,将该数据转换为响应式数据,需要注意的是,由于前面调用了toggleObserving(false)方法,所以不会对嵌套数据进行响应式化,只有最外层数据才会经过响应式处理,然后判断如果该属性没有定义在Vue实例上,就使用proxy方法,将_props上的数据代理到Vue实例上。最后就是调用toggleObserving(true)方法,恢复标志位。
经过initProps方法处理后,已经将父组件传入的数据,经过响应式处理后,代理到子组件实例上了。但是当在子组件中使用最外层数据时,父组件中对应的数据是无法感知的,也就是说,父组件中该数据对应的dep集合中是不包含子组件的渲染Watcher的(嵌套数据除外)。那接下来,就来看看在父组件中修改数据时,子组件是如何收到最新的数据,并进行更新的。
update
首先需要明确的是,对于props选项中的最外层数据来说,它在父组件对应的dep中只会添加父组件的渲染Watcher,所以当数据在父组件中进行修改时,最开始只会将父组件的渲染Watcher添加到更新列表queue中,而不会添加子组件的渲染Watcher,然后在下一帧执行flushSchedulerQueue方法,执行父组件的重新渲染。在执行patch的过程中,会进行对比更新的逻辑,当开始对比两个子组件的占位符VNode时,由于是相同的组件VNode,所以同样会调用patchVnode方法,在该方法中,会执行组件的prepatch钩子函数,更新子组件,代码如下所示:
/* core/vdom/create-component.js */
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
可以看到,在prepatch钩子函数中,这里的options.propsData就是父组件更新后提取出来的props数据,那继续来看updateChildComponent方法,代码如下所示:
/* core/instance/lifecycle.js */
export function updateChildComponent(
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// ...
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// ...
}
可以看到,在updateChildComponent方法中,通过遍历在initProps方法中绑定的属性名_propKeys,来处理数据,还是同样调用validateProp方法检测并取得最新的数值,由于在组件初始化时,props中的数据已经定义了响应式,所以这次重新赋值,就会触发该数据对应的set访问器,如果检测到数据发生变化,就会将子组件的渲染Watcher动态的添加到更新列表queue中,当父组件完成更新后,就会触发子组件的重新渲染,最终,父子组件就都得到了更新。
总结
Vue会根据props选项,在创建父占位符节点的时候构建propsData,然后在实例化子组件时,将propsData进行响应式处理,当数据在父组件中进行更新时,会在对比新旧组件VNode的过程中,调用prepatch钩子函数,对子组件中的数据进行更新,从而触发子组件的重新渲染。