7.1 initProps

简单回顾一下props的用法,父组件通过属性的形式将数据传递给子组件,子组件通过props属性接收父组件传递的值。

  1. // 父组件
  2. <child :test="test"></child>
  3. // 子组件
  4. {
  5. props: ['test']
  6. }

因此分析props需要分析父组件和子组件的两个过程,我们先看父组件对传递值的处理。父组件会优先进行实例的挂载,render解析过程中,遇到子组件的占位符节点<child :test="test"></child>时,会创建子类构造器,遇到传递给子组件的属性时,会解析成 { attrs: {test: test}}的形式并作为render函数存在。再对attrs属性进行规范校验后,会将校验的结果以propsData属性的形式来创建子的Vnode。总结来说,props传递给占位符组件的写法,会以propsData的形式作为子组件Vnode的属性存在。

  1. // 创建子组件过程
  2. function createComponent() {
  3. // props校验
  4. var propsData = extractPropsFromVNodeData(data, Ctor, tag);
  5. ···
  6. // 创建子组件vnode
  7. var vnode = new VNode(
  8. ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
  9. data, undefined, undefined, undefined, context,
  10. { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
  11. asyncFactory
  12. );
  13. }

7.1.1 props的命名规范

回过头看检测props规范性的过程。其中attrs前面分析过,是编译生成render函数针对属性的处理,而props是针对用户自写render函数的属性值。接下来会对命名规则做判断。

  1. function extractPropsFromVNodeData (data,Ctor,tag) {
  2. // Ctor为子类构造器
  3. ···
  4. var res = {};
  5. // 子组件props选项
  6. var propOptions = Ctor.options.props;
  7. // data.attrs针对编译生成的render函数,data.props针对用户自定义的render函数
  8. var attrs = data.attrs;
  9. var props = data.props;
  10. if (isDef(attrs) || isDef(props)) {
  11. for (var key in propOptions) {
  12. // aB 形式转成 a-b
  13. var altKey = hyphenate(key);
  14. {
  15. var keyInLowerCase = key.toLowerCase();
  16. if (
  17. key !== keyInLowerCase &&
  18. attrs && hasOwn(attrs, keyInLowerCase)
  19. ) {
  20. // 警告
  21. }
  22. }
  23. }
  24. }
  25. }

有必要说一下这一部分的处理,HTML对大小写是不敏感的,所有的浏览器会把大写字符解释为小写字符,因此我们在使用DOM中的模板是,cameCase(驼峰命名法)的props名需要使用其等价的 kebab-case (短横线分隔命名) 命代替即: <child :aB="test"></child>需要写成<child :a-b="test"></child>

7.1.2 响应式数据props

刚才说到分析props需要两个过程,这里我们再看看子组件对props的处理,在创建子组件createComponent的过程中,和初始化根组件一样,会经历子组件的选项合并和初始化过程,在深入剖析Vue源码 - 选项合并(上)一节,我们知道,子组件props选项最终会统一成{props: { test: { type: null }}}的写法。选项合并和写法规范是初始化props的前提,接下来会执行initProps, initProps做的事情,简单概括一句话就是,将组件的props数据设置为响应式数据。

  1. function initProps (vm, propsOptions) {
  2. var propsData = vm.$options.propsData || {};
  3. var loop = function(key) {
  4. ···
  5. defineReactive(props,key,value,cb);
  6. if (!(key in vm)) {
  7. proxy(vm, "_props", key);
  8. }
  9. }
  10. // 遍历props,执行loop设置为响应式数据。
  11. for (var key in propsOptions) loop( key );
  12. }

其中proxy(vm, "_props", key);props做了一层代理,用户通过vm.XXX可以代理访问到vm._props上的值。针对defineReactive,本质上是利用Object.defineProperty对数据的getter,setter方法进行重写,具体的原理可以参考深入剖析Vue源码 - 数据代理,关联子父组件这一节的内容,在这小节后半段也会有一个基本的实现。