7.2 initProps

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

  1. // 父组件
  2. <child :test="test"></child>
  3. var vm = new Vue({
  4. el: '#app',
  5. data() {
  6. return {
  7. test: 'child'
  8. }
  9. }
  10. })
  11. // 子组件
  12. Vue.component('child', {
  13. template: '<div>{{test}}</div>',
  14. props: ['test']
  15. })

因此分析props需要分析父组件和子组件的两个过程,我们先看父组件对传递值的处理。按照以往文章介绍的那样,父组件优先进行模板编译得到一个render函数,在解析过程中遇到子组件的属性,:test=test会被解析成{ attrs: {test: test}}并作为子组件的render函数存在,如下所示:

  1. with(){..._c('child',{attrs:{"test":test}})}

render解析Vnode的过程遇到child这个子占位符节点,因此会进入创建子组件Vnode的过程,创建子Vnode过程是调用createComponent,这个阶段我们在组件章节有分析过,在组件的高级用法也有分析过,最终会调用new Vnode去创建子Vnode。而对于props的处理,extractPropsFromVNodeData会对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.2.1 props的命名规范

先看检测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.2.2 响应式数据props

刚才说到分析props需要两个过程,前面已经针对父组件对props的处理做了描述,而对于子组件而言,我们是通过props选项去接收父组件传递的值。我们再看看子组件对props的处理:

子组件处理props的过程,是发生在父组件_update阶段,这个阶段是Vnode生成真实节点的过程,期间会遇到子Vnode,这时会调用createComponent去实例化子组件。而实例化子组件的过程又回到了_init初始化,此时又会经历选项的合并,针对props选项,最终会统一成{props: { test: { type: null }}}的写法。接着会调用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方法进行重写,具体的原理可以参考数据代理章节的内容,在这小节后半段也会有一个基本的实现。