# props
在初始化 Vue 的时候,我们知道initState()会初始化props,methods,data等内容,这就是响应式原理的入口:
// instance/state.js
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
// 把props中的数据转化为响应式数据,并且注入到实例
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 */);
}
// 创建顺序computed watcher -> watch watcher -> render watcher
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
上面的代码会依次处理用户传入的props,methods,data,computed,watch,我们依次来看内部是怎么处理的。
在初始化 Vue 的时候,会对 options 进行合并,在这里需要对 props 进行一定的序列化
// instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this;
// ...
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
// ...
};
mergeOptions定义的地方是在core/util/options.js:
export function mergeOptions(
parent: Object,
child: Object,
vm?: Component
): Object {
// ...
normalizeProps(child, vm);
}
// 序列化props
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;
}
normalizeProps其实做了几个数据类型的判断:
- 数组: 会做一个倒序的遍历,如果是
string则会把key转成驼峰类型,同时赋值type=null,如果不是string,会报warn
export default {
props: ["name", "fisrt-name"],
};
// 转化后
export default {
props: {
name:{
type:null
},
firstName:{
type:null
}
}
}
- 对象: 会遍历该对象,对
key也会做一个驼峰处理,如果val是一个对象,则直接返回,否则将val作为type
export default {
props: {
name: {
value: "world",
type: String,
},
age:Number,
gender: '1'
},
};
// 转化后
export default {
props: {
name: {
value: "world",
type: String,
},
age:{
type:Number
},
gender:{
type:'1'
}
},
};
完成序列化后,就是对props的初始化initProps
// core/instance/state.js
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {};
const props = (vm._props = {});
const keys = (vm.$options._propKeys = []);
const isRoot = !vm.$parent;
if (!isRoot) {
toggleObserving(false);
}
for (const key in propsOptions) {
keys.push(key);
// 校验
const value = validateProp(key, propsOptions, propsData, vm);
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 {
// 设置setter和getter
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)) {
// 注入到vm里,目的为了使用更加方便
// vm.key === vm._props.key
proxy(vm, `_props`, key);
}
}
toggleObserving(true);
}
这里有一个函数toggleObserving,一开始设置为false,在defineReactive后设置为true,当设置为false的时候,禁止当前 对象变为响应式对象。
export function toggleObserving(value: boolean) {
shouldObserve = value;
}
只有在子组件的时候,才会这样处理,因为子组件的 props 来自父级组件,父组件props变化会自动触发子组件渲染,不需要对于对象进行递归的响应式处理或者数组变异处理,所以可以省略observe这个过程,直接defineReactive就可以。
之后会对props进行校验
export function validateProp(
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
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;
}
}
}
if (value === 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;
}
function getPropDefaultValue(vm, prop, key) {
// no default, return undefined
if (!hasOwn(prop, "default")) {
return undefined;
}
var def = prop.default;
// warn against non-factory defaults for Object & Array
if (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
);
}
if (
vm &&
vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key];
}
return typeof def === "function" && getType(prop.type) !== "Function"
? def.call(vm)
: def;
}
主要做了三件事情:
- 处理
Boolean类型的数据: 首先通过getTypeIndex(Boolean, prop.type)判断prop是否是Boolean类型,返回对应的索引。如果是一个Boolean,而且没有设置default同时父级也没有传递数据,则设置为false。第二种情况比较复杂,主要是处理在父组件中没写属性的值的情况或者同名的属性值情况,则设置为true。 - 处理默认值: 1.如果没有传
default,则直接返回undefined。2.default必须返回函数。3.上一次组件渲染父组件传递的prop的值是undefined,则直接返回 上一次的默认值vm._props[key],这样可以避免触发不必要的watcher的更新。4.判断def类型并返回对应的逻辑。
最后就是defineReactive进行响应式的处理,并且代理proxy(vm,_props, key)到实例下。