# props
这一次,通过源码阅读,主要探索的方面包括如何初始化 Props、以及如何进行更新。
# 初始化
initState(vm);
function initState(vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
// ...
}
function initProps (vm, propsOptions) {
var propsData = vm.$options.propsData || {}; // 获取Vue实例选项上的Props
var props = vm._props = {}; // 获取挂载Vue实例上的_props
var keys = vm.$options._propKeys = []; // Props的Key值组成的数组
// ...
for (var key in propsOptions) loop( key ); // 循环遍历 vue 实例选项中Props,并且执行响应式处理以及挂载在对应实例上
// ...
}
初始化 Props 的关键点就在于loop函数,让我们接着该函数做了什么事情。
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
var loop = function ( key ) {
keys.push(key); // 每遍历一次Props中的值,都会收集其Key
// ...
defineReactive$$1(props, key, value, function () { // 将Vue实例上对象的_props中每一项属性都设置响应式
if (!isRoot && !isUpdatingChildComponent) { // 配置的Setter函数,避免子组件直接操作Props的警告
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
);
}
});
// 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)) { // 遍历时若发现新属性时,就将新属性重新挂载到Vue实例的_props中
proxy(vm, "_props", key);
}
};
理解上应该不会有太大的问题,loop函数中主要做了以下几件事:
- defineReactive
相信对这个函数应该都不会陌生,其实就是对Vue实例上的_props
对象中每一个属性都配置成响应式的,这样一来,当父组件中传递进来的 Props
变化时,则会通知相应的子组件中更新函数进行更新;
- 对相应的子组件配置Props属性的Setter警告函数
用过Vue的童鞋们应该都会遇到直接更改一个 Props
中的属性时,会抛出一个警告。而这个警告就是在 Vue 遍历 _props
对象中的值时,都会默认配置一个警告 Setter
函数;
- proxy
在遍历的过程中,一旦发现有新的属性时,都会将新属性重新挂载到 Vue 实例的 _props
中。这里有一个很重要的知识点,当我们直接访问一个 Props 中的属性时,即上面栗子中this.name
,其实是直接访问了 Vue 实例的 _props
对象中值而已。
至此,我们也知道 Vue 源码是如何实现初始化 Props 的了,那么,究竟是父组件是如何通知更新 Props 的呢?我们接着看下去。
# 更新
由于父组件在更新的过程中,会通知子组件也进行更新,这时候就会调取一个方法updateChildComponent
,而这个方法里就会对 Props
进行更新。我们就来看看是如何处理的:
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
);
function updateChildComponent (
vm,
propsData,
listeners,
parentVnode,
renderChildren
) {
// ...
// update props
if (propsData && vm.$options.props) {
toggleObserving(false); // 关闭依赖监听
var props = vm._props; // 获取Vue实例上_props对象
var propKeys = vm.$options._propKeys || []; // 获取保留在Vue实例上的props key值
for (var i = 0; i < propKeys.length; i++) { // 循环遍历props key
var key = propKeys[i];
var propOptions = vm.$options.props; // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm); // 先校验Props中定义的数据类型是否符合,符合的话就直接返回,并且直接赋值给Vue实例上_props对象中相应的属性中
}
toggleObserving(true); // 打开依赖监听
// keep a copy of raw propsData
vm.$options.propsData = propsData; // 新的PropsData直接取替掉选项中旧的PropsData
}
// ...
}
一开始看到这里代码时,我是懵逼状态的,因为很容易绕不出来 😂。。在这里面会有几个问题,分别是:
- validateProp 作用究竟是什么?
相信用过 Props 的同学都清楚,在传递给子组件时,子组件中是有权限决定传递的值类型的,大大提高传递的规范,举个例子:
props: {
name: {
type: String,
required: true
}
}
代码很好理解,就是规定 name
属性的类型以及是否必传。而方法validateProp
作用就是校验父组件传递给子组件的值是否按要求传递,若正确传递则直接赋值给 _props
对象上相应的属性。
- 校验通过后,直接赋值给
_props
对象上相应的属性的用意何在?
上面提到过,_props
对象上的每一个属性都会使用 proxy
方法进行响应式挂载。那么当我直接赋值到 _props
对象上相应的属性时,就会触发到其 Setter
函数进行相应的依赖更新。因此,当父组件更新一个传递到子组件的属性时,首先会触发其 Setter
函数通知父组件进行更新,然后通过渲染函数传递到子组件后,更新子组件中的 Props
。这时候,由于此时的 Props
对象中的属性收集到了子组件的依赖,更改后会通知相应的依赖进行更新。
- toggleObserving 究竟是干嘛用的?
首先它是一个递归遍历方法,Props
在通知子组件依赖更新时,必须搞清楚的一点,就是是整个值的变化来进行通知。如何理解?
简单滴说,对于属性值为基本数据类型的,当值改变时,是可以直接通知子组件进行更新的,而对于复杂数据类型来说,在更新时,会递归遍历其对象内部的属性来通知相应的依赖进行更新。
那么当调用方法toggleObserving
为 false 时,对于基础数据类型来说,当其值变化时则直接通知子组件更新,而对于其复杂数据类型来说,则不会递归下去,而只会监听整个复杂数据类型替换时,才会去通知子组件进行更新。因此在 Props
中所有属性通知完后,又会重新调用方法toggleObserving为 true 来打开递归开关。(真的不得不服尤大大啊,这么好的优化思路都能想出来,牛人 👍)
至此,你大概也知道整个更新流程了,但是我当时还是存在疑惑的,既然基础数据类型值更改或复杂数据类型整个值更改,可以直接通知到子组件进行更新,那么是否会有一种情况就是,复杂数据类型中属性更改时,又是如何通知子组件更新的呢??🤔
首先,我们一开始已经忽略一个方法,那就是defineReactive$$1,这个方法真的用的秒,可以看看上面的代码,在初始化 Props 时候,会对 Props 每一项的属性进行使用该方法进行响应式的处理,包括了复杂数据类型的中属性,此时该属性不但收集了父组件依赖,还收集了子组件的依赖,这样一来,当复杂数据类型中属性变化时,会先通知父组件更新,再通知子组件进行更新。(这时候我真的不得不服到五体投地。。。)
# 总结
- 当 Props 中属性为基础数据类型值更改或复杂数据类型替换时,会通过 Setter 函数通知父组件进行更新,然后通过渲染函数,传递到子组件中更新其 Props 中对象相应的值,这时候就会触发到相应值的 Setter 来通知子组件进行更新;
- 当 Props 中属性为复杂数据类型的属性更改时,由于使用defineReactive$$1方法收集到了父组件依赖以及子组件的依赖,这时候会先通知父组件进行更新,再通知子组件进行更新;
参考: