• 7.2 initProps
    • 7.2.1 props的命名规范
    • 7.2.2 响应式数据props

    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方法进行重写,具体的原理可以参考数据代理章节的内容,在这小节后半段也会有一个基本的实现。