var A = {
template: `
<div>
<p>{{ name }}</p>
<p>info:</p>
<ul>
<li>{{ info.aa }}</li>
<li>{{ info.bb }}</li>
</ul>
</div>`,
props: {
name: String,
info: Object,
},
}
var vm = new Vue({
el: '#app',
components: {
A
},
template: `<div id="app"><A :name="name" :info="info" /></div>`,
data () {
return {
name: 'test',
info: {
aa: 11,
bb: 22,
}
}
},
})
Props 的规范化过程
这里的规范化指的是对于子组件,规范 props 的定义。
mergeOptions → normalizeProps
function normalizeProps (options, vm) {
debugger
var props = options.props;
if (!props) { return }
var res = {};
var 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 {
warn('props must be strings when using array syntax.');
}
}
} else if (isPlainObject(props)) {
for (var key in props) {
val = props[key];
name = camelize(key);
res[name] = isPlainObject(val)
? val
: { type: val };
}
} else {
warn(
"Invalid value for option \"props\": expected an Array or an Object, " +
"but got " + (toRawType(props)) + ".",
vm
);
}
options.props = res;
}
规范化后的格式,props 属性驼峰化,并且至少会有一个 type。
options.props = {
xxYyy: {
type,
}
}
注意一点,因为这是对于子组件 props 定义的规范化,所以 type 才是必须的,而不一定有值,如果 props 属性写法如下:
function createComponent (
Ctor,
data,
context,
children,
tag
) {
// ...
var baseCtor = context.$options._base;
// ...
// 生成子组件构造函数,会走 props 的规范化、props 代理。
Ctor = baseCtor.extend(Ctor);
// 这里拿到父组件传下来的 props 值,{ attr: { name, info } }
data = data || {};
// ...
// 最终保存在 vnode 上的 props 信息,{ name, info }
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
// ...
// return a placeholder vnode
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
// vnode.componentOptions.propsData 保存着父组件传下来的 props 值
return vnode
}
在 createComponent 中先生成 A 组件的构造函数,此时会执行 props 的规范化过程;然后执行 extractPropsFromVNodeData 得到 propsData,这是最后保存在 vnode 上关于 props 的信息;最后返回 A 组件的 vnode,vnode.componentOptions.propsData 保存着父组件传下来的 props 值。
extractPropsFromVNodeData 函数如下,大致流程:
先拿到 A 组件 props 的定义:var propOptions = Ctor.options.props;
拿到父组件传下来的 attrs 和 props 。
接着遍历 A 组件中的 props,往 res 里塞对应的 key-value ,最后返回。这里的 hyphenate 和 checkProp 是为了兼容驼峰和全小写横线连接两种写法,在父组件中往子组件传值可以用 :userName="userName" 也可以用 :user-name="userName" 最后经过处理都会转成 { userName } 保存在 res 中返回。
function extractPropsFromVNodeData (
data,
Ctor,
tag
) {
var propOptions = Ctor.options.props;
if (isUndef(propOptions)) {
return
}
var res = {};
var attrs = data.attrs;
var props = data.props;
if (isDef(attrs) || isDef(props)) {
for (var key in propOptions) {
var altKey = hyphenate(key);
{
var keyInLowerCase = key.toLowerCase();
// ...
}
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey, false);
}
}
return res
}
function validateProp (
key,
propOptions,
propsData,
vm
) {
var prop = propOptions[key];
var absent = !hasOwn(propsData, key);
var value = propsData[key];
// boolean ...
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key);
// since the default value is a fresh copy,
// make sure to observe it.
var prevShouldObserve = shouldObserve;
toggleObserving(true);
observe(value);
toggleObserving(prevShouldObserve);
}
{
assertProp(prop, key, value, vm, absent);
}
return value
}
props 的初始化结束。
Props 的更新过程
子组件 props 更新。props 数据的值在父组件中发生变化,触发父组件的 render 。
patch 过程中执行 patchVnode 函数。
vnode hook prepatch 函数 → updateChildComponent 函数。
function updateChildComponent (
vm,
propsData, // 父组件更新后的 props 数据
listeners,
parentVnode,
renderChildren
) {
// ...
// update props
if (propsData && vm.$options.props) {
toggleObserving(false);
var props = vm._props;
var propKeys = vm.$options._propKeys || [];
for (var i = 0; i < propKeys.length; i++) {
var key = propKeys[i];
var propOptions = vm.$options.props; // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm);
}
toggleObserving(true);
// keep a copy of raw propsData
vm.$options.propsData = propsData;
}
// ...
}
例子如下:
Props 的规范化过程
这里的规范化指的是对于子组件,规范 props 的定义。
mergeOptions → normalizeProps
规范化后的格式,props 属性驼峰化,并且至少会有一个 type。
注意一点,因为这是对于子组件 props 定义的规范化,所以 type 才是必须的,而不一定有值,如果 props 属性写法如下:
对于这种写法
res[name] = isPlainObject(val) ? val : { type: val };
最后会直接赋给res['test']
。Props 的初始化过程
这里要分两步,一步是父组件如何把绑定的值做为 props 传给子组件;一步是子组件如何获取父组件传过来的值做初始化。
父组件传值
首先,vm 的 render 函数如下,
attrs
上保存着传下来的 props 值。接着执行 A 组件 vnode 的生成。
createElement → createComponent
,和 props 相关的关键逻辑大致如下:在
createComponent
中先生成 A 组件的构造函数,此时会执行 props 的规范化过程;然后执行extractPropsFromVNodeData
得到propsData
,这是最后保存在 vnode 上关于 props 的信息;最后返回 A 组件的 vnode,vnode.componentOptions.propsData
保存着父组件传下来的 props 值。extractPropsFromVNodeData
函数如下,大致流程:var propOptions = Ctor.options.props;
attrs
和props
。key-value
,最后返回。这里的hyphenate
和checkProp
是为了兼容驼峰和全小写横线连接两种写法,在父组件中往子组件传值可以用:userName="userName"
也可以用:user-name="userName"
最后经过处理都会转成{ userName }
保存在 res 中返回。子组件获取值初始化
现在我们知道子组件 vnode 上
componentOptions.propsData
是可以拿到父组件传下来的值的,而子组件 props 的初始化主要在initProps
函数中。()initState → initProps
主要流程如下:
validateProp
函数获得 props 属性最后的值,通过defineReactive
转为响应式对象,将 props 属性都保存在vm._props
上,最后将vm._props
代理到实例vm
上。这一步最重要的就是
validateProp
和defineReactive
这两步,响应式暂时不看,只看validateProp
observe
将 default 值转为响应式对象。assertProp
主要做的和看传的类型是否合法,这里有个需要注意的地方,之所以不需要对父组件传的值做响应式,是因为父子组件中对于引用类型的 prop,最后指向的是相同的引用对象。如果是普通类型,在initProps
中defineReactive
会执行响应式转换。props 的初始化结束。
Props 的更新过程
子组件 props 更新。
props
数据的值在父组件中发生变化,触发父组件的render
。patch 过程中执行 patchVnode 函数。
vnode hook
prepatch
函数 →updateChildComponent
函数。可以看到只是重新执行
validateProp
赋值即可,这里又有两种情况,一种是父组件传的值是普通类型,更新后validateProp
返回赋值,会触发 prop 的setter
触发子组件渲染 watcher 更新;一种是父组件传的值是引用类型,此时可能只是对象中的某一项被修改,
validateProp
返回的还是同一个引用,不会触发setter
,但是子组件中访问过这个 prop,子组件渲染 watcher 会被收集到这个 prop 的依赖中,父组件中修改 props 时触发setter
,也就会通知子组件渲染 watcher 更新。