前言
通过这篇文章将了解
- 子组件 Vue 实例是怎么获取传入的
props数据的 prop响应原理
传入的prop数据是怎么添加到vm.$options.propsData上的
在开始之前需要明确一下:
在createComponent函数中,会执行extractPropsFromVNodeData,这个函数就是用来提取传入的prop数据,并添加到组件占位符VNnode的componentOptions.propsData上;(在Vue 源码(一)如何创建VNode里面介绍过)
在创建组件实例过程中,会调用_init函数,在_init 中会合并options,对于组件的options合并,会将传入的prop数据绑定到 vm.$options.propsData上
初始化
当执行_init方法时会调用initState方法去初始化options中的属性并添加响应,其中就包含props的初始化
initProps
props通过initProps函数初始化,定义在 src/core/instance/state.js 中
// propsOptions 子组件声明的 props 对象
function initProps (vm: Component, propsOptions: Object) {
// 获取 父组件传入的 props
const propsData = vm.$options.propsData || {}
// 将 _props 挂载到 vm 上并初始化为一个空对象
const props = vm._props = {}
// 缓存 props 的每个 key
const keys = vm.$options._propKeys = []
// 只有子组件实例才有 vm.$parent 属性,指向父组件实例
const isRoot = !vm.$parent
if (!isRoot) {
// 如果不是根实例,将 observer/index.js 中的 shouldObserve 设置成 false,这样就不会执行创建 Observer 实例了
toggleObserving(false)
}
// 遍历 props 对象
for (const key in propsOptions) {
// 缓存 key
keys.push(key)
// 验证 传入的 props,并返回 传入的值或者默认值
const value = validateProp(key, propsOptions, propsData, vm)
// 开发环境下
if (process.env.NODE_ENV !== 'production') {
defineReactive(props, key, value, () => {
// 注意这里!! 当父组件修改 传入的 props 属性时,会将 isUpdatingChildComponent 置为 true,所以不会报错
// 当在子组件直接修改 props 属性时, isUpdatingChildComponent 为 false,会报错
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 {
// 为 props 的每个 key 添加响应
defineReactive(props, key, value)
}
if (!(key in vm)) {
// 代理 key 到 vm 对象上
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
initProps 函数的流程如下:
- 获取传入的
prop数据 - 遍历组件定义的
props - 通过
validateProp验证传入的prop,获取传入的prop数据或者默认值 - 调用
defineReactive函数,通过Object.defineProperty将传入的prop数据添加到vm._props中,并设置存取描述符。 - 如果是根组件实例则通过
proxy函数,代理key到vm对象上。组件实例的options.props通过Vue.extend创建子组件实例时,已经将props的所有key代理到了Sub.prototype._props上了(Vue 源码(一)如何创建VNode)
疑问点
为什么要调用toggleObserving(false)?
如果传入的prop数据是一个对象,调用defineReactive时,还会调用observe,给对象的属性添加响应;其实在父组件中,已经通过observe给对象内部所有属性添加响应了,所以这里就没必要再次添加了
validateProp
验证 传入的props,并返回 传入的值或者默认值
代码定义在src/core/util/props.js中
export function validateProp (
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
const prop = propOptions[key]
// 组件标签上没有属性 key,则为 true
const absent = !hasOwn(propsData, key) // <x name /> 这种情况,absent 为 false
// 获取传入的值
let value = propsData[key]
/* 处理布尔类型的 prop */
// 如果 prop.type 不是数组,Boolean 和 prop.type 相同返回 0, 不同返回 -1
// 如果 prop.type 是数组,优先返回第一个相同的。如果相同,返回对应的索引,如果不同返回 -1
const booleanIndex = getTypeIndex(Boolean, prop.type)
// 说明 props 可以是 Boolean 类型
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, 'default')) {
// 组件标签上没有属性 key,并且没有设置默认值,则 value 为 false
value = false
} else if (value === '' || value === hyphenate(key)) {
// value 是 空字符串 或者和 key 一样(如果 key 是驼峰式 value 是连字符式 也可)
// 比如:<x name> 或 <x name="name">、<x nameNick="name-nick">
const stringIndex = getTypeIndex(String, prop.type)
// 如果 prop.type 为 Boolean, value 为 true
// 如果 prop.type 为 [Boolean, String],value 为 true
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
if (value === undefined) {
// 如果传入的是 undefined,则获取默认值
value = getPropDefaultValue(vm, prop, key)
const prevShouldObserve = shouldObserve
toggleObserving(true)
// 给默认值添加响应
observe(value)
toggleObserving(prevShouldObserve)
}
if (
process.env.NODE_ENV !== 'production' &&
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
assertProp(prop, key, value, vm, absent)
}
return value
}
validateProp函数根据key获取组件中对应prop定义,如果type属性中包含布尔类型,则转换prop数据;比如<child bool>这种会设为true。
如果没有传入会获取默认值,对于使用默认值的prop会对默认值添加响应;然后在开发环境下通过assertProp校验prop数据是否符合预期。
getPropDefaultValue
getPropDefaultValue作用是获取默认值
function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
// no default, return undefined
if (!hasOwn(prop, 'default')) {
return undefined
}
const def = prop.default
// 如果 默认值是一个对象或数组则报错
if (process.env.NODE_ENV !== 'production' && isObject(def)) {
warn()
}
// 这里是一种优化手段
// 当第一次和第二次都没传入值时,说明两次都是用的默认值,第一次已经对这个默认值添加监听了
// 所以第二次直接将第一次被监听的对象赋值给 value
// 这样执行到后面的 observe 时,会因为有 __ob__ 属性,不会再次执行后面添加响应的逻辑
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
// 如果 默认值是一个 function,并且 期望的类型不是 Function 时,说明 默认值是一个对象或者数组,执行这个函数拿到 默认值
return typeof def === 'function' && getType(prop.type) !== 'Function'
? def.call(vm)
: def
}
初始化过程如下
Props 更新
当修改父组件传递给子组件的prop数据时,子组件对应的值也会改变,同时会触发子组件的重新渲染。
在分析这个过程之前,先看下父子组件的依赖收集过程
父组件依赖收集
首先,在父组件render函数执行过程中,会访问到这个prop数据。并将父组件的Render Watcher添加到这个prop数据的dep.subs中
子组件依赖收集
在initProps函数中,对所有prop数据添加了响应。执行子组件render函数时,如果使用了某个prop数据,则会触发对应prop数据的getter方法,将子组件的Render Watcher添加到prop数据的dep.subs中
属性值是基本数据类型
prop数据是基本数据类型时,getter方法会将子组件的Render Watcher添加到prop数据对应的dep.subs中
属性值是对象
prop数据是对象时,父组件已经对prop数据的内部属性添加了响应,所以执行到defineReactive函数中的let childOb = !shallow && observe(val)时,因为已经存在__ob__属性,所以不会给属性值设置存取描述符,而只对vm._props.xxx设置存取描述符。
当子组件的render函数获取对象的属性值时,触发getter,实际上是将子组件的Render Watcher添加到父组件对应属性的dep.subs上
属性值是默认值
prop数据是默认值时,会通过getPropDefaultValue获取默认值,并在validateProp中对默认值添加响应
// ...
if (value === undefined) {
// 如果传入的是 undefined,则获取默认值
value = getPropDefaultValue(vm, prop, key)
const prevShouldObserve = shouldObserve
toggleObserving(true)
// 给默认值添加响应
observe(value)
toggleObserving(prevShouldObserve)
}
// ...
更新
当父组件修改prop数据时,会触发父组件中对应属性的setter,从而通知依赖此属性的所有Watcher更新;也就是说每次父组件修改prop数据都会导致自身更新(如果prop数据是对象,父组件修改的是对象内部属性的话,自身不会更新,因为没有将父组件的Render Watcher添加到修改属性的dep.subs中);在父组件重新渲染的最后,会执行patch过程(patch过程会单独拿出两节来说,这里就先了解下),进而执行 patchVnode 函数,patchVnode 通常是一个递归过程,当它遇到组件VNode时,会执行组件的prepatch钩子函数,在 src/core/vdom/patch.js 中:
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props 传入子组件的最新的 props 值
options.listeners, // updated listeners 自定义事件
vnode, // new parent vnode
options.children // new children
)
}
prepatch内调用updateChildComponent函数,传入最新的prop数据;因为在执行父组件render函数时,子组件会创建一个组件占位符VNode,在这个过程中会获取最新的prop数据,并添加到组件占位符 VNode 的componentOptions.propsData属性中;所以prepatch中的options.propsData是最新的prop数据
updateChildComponent函数,它的定义在 src/core/instance/lifecycle.js 中:
export function updateChildComponent (
vm: Component, // 子组件实例
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode, // 组件 vnode
renderChildren: ?Array<VNode>
) {
if (process.env.NODE_ENV !== 'production') {
// 设置成 true 的目的是,给 props[key] 赋值时,触发 set 方法,不会让 customSetter 函数报错
isUpdatingChildComponent = true
}
// ...
// 更新 props
if (propsData && vm.$options.props) {
toggleObserving(false)
// 之前的 propsData
const props = vm._props
// 子组件定义的 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的值触发 组件更新
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// ...
if (process.env.NODE_ENV !== 'production') {
// 更新完成后,置为 false
isUpdatingChildComponent = false
}
}
在这里只看 props 相关逻辑,首先将 isUpdatingChildComponent 变为 true,目的之后会说;接下来就是遍历propKeys,然后执行 props[key] = validateProp(key, propOptions, propsData, vm) 重新验证和计算新的 prop 数据,更新 vm._props,此时会触发 prop数据的setter过程,只要在渲染子组件的时候访问过这个 prop 值,那么根据响应式原理,就会触发子组件的重新渲染。大体流程是,调用子组件Redner Watcher的update方法,因为现在正在执行队列,所以不会再次调用nextTick,而是将子组件的Render Watcher直接添加到队列中等待执行(所以,更新过程是先父后子)。
回到 updateChildComponent 函数,接下来将最新的propsData添加到vm.$options.propsData里面。并执行isUpdatingChildComponent = false;修改它的作用其实就是防止修改vm._props属性时报错。
开发环境下在给 prop数据设置getter、setter时,会传入customSetter,它定义在initProps函数中;属性修改触发setter,setter函数内如果传入了customSetter,会执行这个函数。所以在开发环境下如果子组件直接修改prop数据会报错。
defineReactive(props, key, value, () => {
// 注意这里!! 当父组件修改 传入的 props 属性时,会将 isUpdatingChildComponent 置为 true,所以不会报错
// 当在子组件直接修改 props 属性时, isUpdatingChildComponent 为false,会报错
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
)
}
})
如果prop数据是对象
<hello :a="obj" />
<!-- obj = { name: 'xxx' } -->
如果修改的是prop的内部属性,不会触发父组件的patch过程,因为父组件的render函数中并没有用到该属性,自然也不会触发updateChildComponent函数;但是在子组件的渲染过程中,已经将子组件的Render Watcher添加到父组件对应属性的dep.subs里面了;所以会触发子组件的更新。
也就是说当父组件更新内部属性时,会触发此属性的setter方法,从而触发子组件的Watcher更新;因为父组件传入是对象,是引用数据类型,所以子组件获取的prop数据也是最新的
总结
子组件 Vue 实例是怎么获取传入的props数据的
在执行父组件的render函数时,会为子组件创建组件占位符VNode,此时会根据子组件中props的定义从组件标签的属性中匹配传入的数据,并存储在组件占位符VNode 中
prop 响应原理
初始化子组件的 Vue 实例时,通过Object.defineProperty给传入的prop数据添加拦截,如果传入的是一个对象类型,由于父组件已经对对象的属性添加了拦截,所以不会再次在子组件添加拦截。
和data的区别就是,data中的属性如果不是基本数据类型会为这个属性创建Observer实例;而props的数据不会;有一种情况除外,就是prop默认值是对象类型,会给这个默认值创建Observer实例。
接下来从两个方面分别说一下依赖收集和派发更新
- 传给子组件的是基本数据类型
- 传给子组件的是对象
传给子组件的是基本数据类型
父组件创建 VNode 时,收集当前 Render Watcher 到响应式属性的dep.subs中。创建 子组件VNode 时,也会收集当前Render Watcher 到prop数据的dep.subs中。
当父组件修改数据时,触发父组件的视图更新,获取最新的prop数据;在创建父组件 DOM树的过程中,赋值给子组件的vm._props;从而被prop数据的setter捕获,触发子组件视图更新。
也就是说,如果传给子组件的是基本数据类型,他们的更新原理是父组件驱动子组件更新
传给子组件的是对象
父组件创建 VNode 时,收集当前 Render Watcher 到响应式属性的dep.subs中。创建 子组件VNode 时,也会收集当前Render Watcher 到prop数据的dep.subs中。和上面不同的是,当子组件使用的是prop数据的内部属性时,会将Render Watcher 添加到父组件对应内部属性的dep.subs中。
当父组件修改属性的内部属性时,不会触发父组件更新,因为父组件没有使用这个内部属性,而使用的是整个对象。但是会触发子组件更新,因为子组件的Render Watcher 被收集到了这个内部属性的dep.subs里面了。
也就是说如果传给子组件的是一个对象,并且子组件使用了这个内部属性,子组件的 Render Watcher会被这个内部属性的dep.subs收集
如果父组件直接修改这个对象的引用,则和传入基本数据类型的更新流程一致。