从源码角度分析Vue父子组件间传值

2,823 阅读3分钟

文章可能会再更新,具体查看github

子组件向父组件传值: 一、ref、$refs的方式: 首先,假设我们有两个组件,父组件叫HelloAnother.vue,子组件叫childProps.vue。父组件里面有一个router路由,父组件的结构如下所示。

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <child-props :message = msg ref="childp">
    </child-props>
    <router-link to="/HelloWorld">
    <span>switch</span>
    </router-link>
  </div>
</template>

<script>
import childProps from '@/base/childProps'
export default {
  name: 'HelloAnother',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  components:{
    childProps
  },
  mounted: function() {
    console.log(this.$refs.childp.childPro);
    console.log(this._data)
    this.$refs.childp.tryChild.call(this.$refs.childp)
  }
}
</script>

在这个父组件中,child-props(注意:vue.js文档中建议我们组件名可以用驼峰法,但是当写在template的时候,需要转换成child-props,这是因为html的标签一般都是没有连字符的,为了预防与未来某个标准标签名重名)和router-link都是HelloAnother.vue的子组件。我们先看看childProps.vue的结构:

<template>
   <div>
       {{message}}
       {{data}}
   </div>
</template>
<script>
export default {
  name: "childProps",
  data(){
      return {
          test:'hello',
          hello:"yes",
          childPro: "enenen"
      }
  },
  props:{
      message: {
          type: String,
          default: ''
      },
      data: {
          type: Array,
          default: () => []
      }
  },
  methods:{
  }
}
</script>

我们再来看看childProps.vue打印出来的属性: 2 我们看到,它的Ctor(constructor)指向的是VueComponent构造函数,说明childProps.vue是一个注册后的component。instances指向的是它这个实例。下面这个图是展开后的instances,每个component的root指向(你的这个工程的)根.vue,也就是用vue-cli初始化的时候的那个App.vue组件。然后它的el指向的是div,这个也没毛病,就是渲染后的html的div节点。 3 f 接下来我们重点看一下childProps.vue的$vnode节点属性:它指向的是VNode构造函数,它长下面这个样子:

var VNode = function VNode (tag, data, children, text, elm, context, componentOptions, asyncFactory) {
  this.tag = tag;
  this.data = data;
  this.children = children;
  this.text = text;
  this.elm = elm;
  this.ns = undefined;
  this.context = context;
  //.............................
  this.isAsyncPlaceholder = false;
};

这里我们需要重点关注的是,vnode的context指向的是它自己的父组件(当然了childProps.vue的parent或者options.parent都是指向它的父组件的): 4 接下来,是父组件获取子组件值的方法: default 我们看到了父组件的ref的childp指向了childProps.vue组件。其实我们再看看我们的父组件结构。我们在child-props里面,添加了ref属性,并取名为"childp"。这样我们的父组件的refs里面就有了一个属性this["childp"]指向子组件。

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <child-props :message = msg ref="childp">
    </child-props>
    <router-link to="/HelloWorld">
    <span>switch</span>
    </router-link>
  </div>
</template>

以上是怎么实现的呢?是通过registerRef方法,将子组件的vnode.data.ref注册给了它的父组件vnode.context.$refs。

function registerRef (vnode, isRemoval) {
  // 获得这个子组件的ref名,比如"childp"
  var key = vnode.data.ref;
  if (!isDef(key)) { return }

  var vm = vnode.context;
  // 将vnode(当前子组件)的componentInstance(子组件实例VueComponent)缓存为ref
  // 这里也就是childProps.vue
  var ref = vnode.componentInstance || vnode.elm;
  var refs = vm.$refs;
  if (isRemoval) {
    if (Array.isArray(refs[key])) {
      remove(refs[key], ref);
    } else if (refs[key] === ref) {
      refs[key] = undefined;
    }
  } else {
    if (vnode.data.refInFor) {
      if (!Array.isArray(refs[key])) {
        refs[key] = [ref];
      } else if (refs[key].indexOf(ref) < 0) {
        // $flow-disable-line
        refs[key].push(ref);
      }
    } else {
      refs[key] = ref;
    }
  }
}

那么,对于vnode.componentInstance,我们是怎么去获取这个实例的呢?是通过createComponentInstanceForVnode方法赋值给的vnode.componentInstance:

vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      );

而createComponentInstanceForVnode方法内部会最终返回一个vnode.componentOptions.Ctor(options),这个Ctor其实就是VueComponent方法。

二、触发事件的方法: 我们在父组件中绑定一个方法:

<template>
  <div class="hello">
    <child-props :messages = msg ref="childp" @msgFunc = "func">
    </child-props>
  </div>
</template>
<script>
import childProps from '@/base/childProps'
export default {
  name: 'HelloAnother',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  components:{
    childProps
  },
  mounted: function() {
    //...
  },
  methods: {
    func (message) {
      console.log(message)
    }
  }
}
</script>

然后在子组件childProps.vue里面,绑定一个点击事件handleParent,在子组件的methods里面,this.$emit('msgFunc',this.message) 触发这个方法:

<template>
   <div>
       <button @click = "handleParent">try me</button>
   </div>
</template>
<script>
export default {
  name: "childProps",
  data(){
      return {
      }
  },
  props:{
      message: {
          type: String,
          default: 'default'
      }
  },
  methods: {
      handleParent () {
          this.$emit('msgFunc',this.message);
      }
  }
}
</script>

接下来是父组件向子组件传值: 使用props的方式: 我们来看看父组件如何向子组件传值: 首先我们的childProps.vue里面可以写:

<template>
  <div>I am {{message}}</div>
</template>
<script>
  export default {
      name: "child",
      props: {
          "message": {
              type: String,
              default: ''
          }
      },
      data(){
          return {
              text: "from child"
          }
      }
  }
</script>

然后我们在HelloAnother.vue里面引入子组件childProps.vue, 然后通过v-bind传入mes数据(也可以直接message = "字符串")

<template>
  <child-co :message = "mes"></child-co>
</template>
<script>
  import childCo from './child'
  export default {
      name: "childCo",
      data(){
          mes: "parents love their children"
      }
      components: {
          child
      }
  }
</script>

我们在子组件里面添加props属性的时候,一般有两种写法:

// 简单的数组(不推荐)
export default {
    props:['message','inform']
}
// 对象的形式(推荐)
// 因为这样显示属性的类型
export default {
    props: {
        message: {
            type: String,
            default: ''
        },
        
        inform: {
            type: Array,
        // 因为是对象
        // 所以default需要是一个返回该类型的函数
            default: () => []
        }
    }
}

其实我们写成第一种方式的话,Vue也会自动帮我们把props(Array)属性,转换为Object {},即第二种的形式,只不顾没有default属性,message的type值为null,具体实现是通过normalizeProps实现的:

function normalizeProps (options, vm) {
  var props = options.props;
  if (!props) { return }
  var res = {};
  var i, val, name;
  if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === 'string') {
        name = camelize(val);
        // 类型值为null
        res[name] = { type: null };
      } else {
        warn('props must be strings when using array syntax.');
      }
    }
  }
  // 判断是否为{}对象
  // return _toString.call(obj) === '[object Object]
  else if (isPlainObject(props)) {
    for (var key in props) {
      val = props[key];
      name = camelize(key);
      res[name] = isPlainObject(val)
        ? val
        : { type: val };
    }
  } else {
   //........
  }
  // 将对象化后的props给了options.props
  options.props = res;
}

**this.children方式:**
当然我们还有可以通过this.children的形式访问子组件。但是这种情况下,由于一个父组件可能有多个子组件,比如HelloAnother.vue就有router和childProps.vue两个子组件,.$children返回的就是一个长度为2的数组: [VueComponent, VueComponent]

需要注意 children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源