7.2 initProps
简单回顾一下props
的用法,父组件通过属性的形式将数据传递给子组件,子组件通过props
属性接收父组件传递的值。
// 父组件
<child :test="test"></child>
var vm = new Vue({
el: '#app',
data() {
return {
test: 'child'
}
}
})
// 子组件
Vue.component('child', {
template: '<div>{{test}}</div>',
props: ['test']
})
因此分析props
需要分析父组件和子组件的两个过程,我们先看父组件对传递值的处理。按照以往文章介绍的那样,父组件优先进行模板编译得到一个render
函数,在解析过程中遇到子组件的属性,:test=test
会被解析成{ attrs: {test: test}}
并作为子组件的render
函数存在,如下所示:
with(){..._c('child',{attrs:{"test":test}})}
render
解析Vnode
的过程遇到child
这个子占位符节点,因此会进入创建子组件Vnode
的过程,创建子Vnode
过程是调用createComponent
,这个阶段我们在组件章节有分析过,在组件的高级用法也有分析过,最终会调用new Vnode
去创建子Vnode
。而对于props
的处理,extractPropsFromVNodeData
会对attrs
属性进行规范校验后,最后会把校验后的结果以propsData
属性的形式传入Vnode
构造器中。总结来说,props
传递给占位符组件的写法,会以propsData
的形式作为子组件Vnode
的属性存在。下面会分析具体的细节。
// 创建子组件过程
function createComponent() {
// props校验
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
···
// 创建子组件vnode
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
);
}
7.2.1 props的命名规范
先看检测props
规范性的过程。props
编译后的结果有两种,其中attrs
前面分析过,是编译生成render
函数针对属性的处理,而props
是针对用户自写render
函数的属性值。因此需要同时对这两种方式进行校验。
function extractPropsFromVNodeData (data,Ctor,tag) {
// Ctor为子类构造器
···
var res = {};
// 子组件props选项
var propOptions = Ctor.options.props;
// data.attrs针对编译生成的render函数,data.props针对用户自定义的render函数
var attrs = data.attrs;
var props = data.props;
if (isDef(attrs) || isDef(props)) {
for (var key in propOptions) {
// aB 形式转成 a-b
var altKey = hyphenate(key);
{
var keyInLowerCase = key.toLowerCase();
if (
key !== keyInLowerCase &&
attrs && hasOwn(attrs, keyInLowerCase)
) {
// 警告
}
}
}
}
}
重点说一下源码在这一部分的处理,HTML对大小写是不敏感的,所有的浏览器会把大写字符解释为小写字符,因此我们在使用DOM
中的模板时,cameCase(驼峰命名法)的props
名需要使用其等价的 kebab-case
(短横线分隔命名) 命代替。即: <child :aB="test"></child>
需要写成<child :a-b="test"></child>
7.2.2 响应式数据props
刚才说到分析props
需要两个过程,前面已经针对父组件对props
的处理做了描述,而对于子组件而言,我们是通过props
选项去接收父组件传递的值。我们再看看子组件对props
的处理:
子组件处理props
的过程,是发生在父组件_update
阶段,这个阶段是Vnode
生成真实节点的过程,期间会遇到子Vnode
,这时会调用createComponent
去实例化子组件。而实例化子组件的过程又回到了_init
初始化,此时又会经历选项的合并,针对props
选项,最终会统一成{props: { test: { type: null }}}
的写法。接着会调用initProps
, initProps
做的事情,简单概括一句话就是,将组件的props
数据设置为响应式数据。
function initProps (vm, propsOptions) {
var propsData = vm.$options.propsData || {};
var loop = function(key) {
···
defineReactive(props,key,value,cb);
if (!(key in vm)) {
proxy(vm, "_props", key);
}
}
// 遍历props,执行loop设置为响应式数据。
for (var key in propsOptions) loop( key );
}
其中proxy(vm, "_props", key);
为props
做了一层代理,用户通过vm.XXX
可以代理访问到vm._props
上的值。针对defineReactive
,本质上是利用Object.defineProperty
对数据的getter,setter
方法进行重写,具体的原理可以参考数据代理章节的内容,在这小节后半段也会有一个基本的实现。