Vue和React对比学习之组件传值(Vue2 12种、Vue3 9种、React 7种)

1,391 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

简介

VueReact是目前前端最火的两个框架。不管是面试还是工作可以说是前端开发者们都必须掌握的。

今天我们通过对比的方式来学习VueReact的组件传值这一部分。

本文首先讲述Vue2Vue3React的组件传值方式,然后具体介绍了每种传值的应用场景以及具体的使用。最后对比总结了VueReact在组件传值这部分的相同点和不同点。

希望通过这种对比方式的学习能让我们学习的时候印象更深刻,希望能够帮助到大家。

IMG_0387.jpeg

Vue2

Vue2 组件通信共有12种

  1. props
  2. $emit / v-on
  3. .sync
  4. v-model
  5. ref
  6. $children / $parent
  7. $attrs / $listeners
  8. provide / inject
  9. EventBus
  10. Vuex
  11. $root
  12. slot

应用场景

Vue2 组件通信方式虽然有很多种,但是不同方式有不同的应用场景。

父子组件通信

  • props
  • $emit / v-on
  • $attrs / $listeners
  • ref
  • .sync
  • v-model
  • $children / $parent
  • slot

兄弟组件通信

  • EventBus
  • Vuex
  • $parent

跨层级组件通信

  • provide/inject
  • EventBus
  • Vuex
  • $attrs / $listeners
  • $root

具体使用

props

props用于父组件向子组件传送数据。

子组件接收到数据之后,不能直接修改父组件的数据。会报错。

当需要修改props的时候应该在父组件修改。父组件修改后子组件会同步渲染。

如果子组件内一定要修改props的话推荐使用 computed,计算一个新属性给子组件使用,而不是直接修改。

// 父组件
<template>
  <child :msg="msg"></child>
</template>

// 子组件
<template>
  <div>{{msg}}</div>
</template>
<script>
export default {
  // 写法一 用数组接收
  props:['msg'],
  // 写法二 用对象接收
  props: {
    msg: String
  },
  // 写法三 用对象接收,可以限定接收的数据类型、设置默认值、验证等
  props:{
    msg:{
      type:String,
      default:'这是默认数据',
      // 必传
      required: true
    }
  },
  props: {
    user:{
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: () => return {},
      // 自定义验证
      validator: () => {
        return true/false
      }
    } 
  }
}
</script>

$emit / v-on

我们知道Vue是单向数据流,父组件传递给子组件的数据是不能直接修改的。

所以子组件想要修改父组件数据的时候需要先暴露事件并传递值给父组件,父组件监听该事件来完成值的修改。

// 子组件
<template>
  <div>
    <div>{{ msg }}</div>
    <button @click="handleClick">change</button>
  </div>
</template>
<script>
export default {
  props: ["msg"],
  methods: {
    handleClick() {
      const newMsg = "我被修改了";
      this.$emit("changeMsg", newMsg);
    },
  },
};
</script>

// 父组件
<template>
  <child :msg="msg" v-on:changeMsg="handleChangeMsg" ></child>
</template>
<script>
export default {
  data() {
    return {msg: "我会传递给子组件"}
  },
  methods:{
    handleChangeMsg(newMsg){
      this.msg = newMsg
    }
  }
}
</script>

.sync

需要修改父组件传递过来的属性的值,每次都需要暴露一个方法过去,然后在父组件监听然后再修改,是不是感觉特别麻烦。

.sync就是用来简化这一过程的。

// 子组件
<template>
  <div>
    <div>{{ msg }}</div>
    <button @click="handleClick">change</button>
  </div>
</template>
<script>
export default {
  props: ["msg"],
  methods: {
    handleClick() {
      const newMsg = "我被修改了";
      this.$emit("update:msg", newMsg);
    },
  },
};
</script>

// 父组件
<template>
  <child :msg.sync="msg"></child>
</template>
<script>
export default {
  data() {
    return {msg: "我会传递给子组件"}
  },
}
</script>

父组件不需要再监听事件然后修改值了。

父组件只需要在传递属性的时候加上.sync修饰符,然后子组件再修改完值的时候暴露update:属性名的事件就可以啦。

v-model

.sync我们知道,是用来简化数据修改的,那还有没有更简单的方法呢?有,那就是v-model

v-model默认传递名为value的属性,并自动监听input事件。

我们来看个例子

// 子组件
<template>
  <div>
    <input type="text" :value="value" @input="handleInput" />
  </div>
</template>
<script>
export default {
  props: {
    value: String,
  },
  methods: {
    handleInput(e) {
      this.$emit("input", e.target.value);
    },
  },
};
</script>

// 父组件
<template>
  <div>{{msg}}</div>
  <child v-model="msg"></child>
</template>
<script>
export default {
  data() {
    return {msg: "我会传递给子组件"}
  },
}

在上面的例子中,我们input框默认值是我会传递给子组件,当我们修改input框值的时候父组件值也会变。这其实就是双向绑定。

但有时候我们不可能就一直传递属性为value,然后监听input事件,这显示不满足我们的需求。

需要修改属性和事件怎么办呢?这就需要用到我们的model参数了。该属性定义在子组件。包含两个属性,prop用来定义属性名,event用来定义事件名。

比如我们想传递过来的属性名为customValue,监听的事件为customInput

// 子组件
<template>
  <div>
    <input type="text" :value="customValue" @input="handleInput" />
  </div>
</template>
<script>
export default {
  model: {
    prop: "customValue",
    event: "customInput",
  },
  props: {
    customValue: String,
  },
  methods: {
    handleInput(e) {
      this.$emit("customInput", e.target.value);
    },
  },
};
</script>

// 父组件
<template>
  <div>{{msg}}</div>
  <child v-model="msg"></child>
</template>
<script>
export default {
  data() {
    return {msg: "我会传递给子组件"}
  },
}

其它的事件或者属性类似,这里笔者就不再赘述了。但是需要注意一个组件只能使用一个v-model

ref

如果需要直接操作子组件我们就可以使用ref了。

ref 如果在普通的DOM元素上,引用指向的就是该DOM元素;

如果在子组件上,引用的指向就是子组件实例。

// 子组件
<template>
  <div>
    <div>{{ msg }}</div>
  </div>
</template>
<script>
export default {
  props: ["msg"],
  data() {
    return {
      name: "child6",
    };
  },
  methods: {
    say() {
      console.log("say child6");
    },
  },
  computed: {
    title() {
      return this.name + "title";
    },
  },
};
</script>

// 父组件
<template>
  <child :msg="msg" ref="childRef"></child>
</template>
<script>
export default {
  data() {
    return {msg: "我会传递给子组件"}
  },
  mounted() {
    // 通过$refs获取子组件
    console.log(this.$refs.childRef)
    // 接下来我们就可以直接操作子组件了
    console.log(this.$refs.childRef.$data)
    console.log(this.$refs.childRef.$props)
    console.log(this.$refs.childRef.$el)
    this.$refs.childRef.say()
    console.log(this.$refs.childRef.name)
    console.log(this.$refs.childRef.title)
    // ...
  }
}

需要注意,如果ref定义在循环中,this.$refs.xxx会是一个数组。

$children / $parent

类似ref,我们可以通过$children / $parent直接获取到子组件或父组件。

$children:获取到一个包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法。

$parent:获取到一个父节点的 VueComponent 对象,同样包含父节点中所有数据和方法。

// 父组件
export default{
  mounted(){
    this.$children[0].someMethod() // 调用第一个子组件的someMethod
    this.$children[0].name // 获取第一个子组件中的name属性
  }
}

// 子组件
export default{
  mounted(){
    this.$parent.someMethod() // 调用父组件的someMethod
    this.$parent.name // 获取父组件中的name属性
  }
}

$root

$children / $parent相似,$root 可以获取到根实例里。

也就是我们main.js创建的Vue实例

new Vue({
  router,
  store,
  render: (h) => h(App),
  data: {
    name: "根组件",
  },
}).$mount("#app");

provide / inject

provide / inject需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 React,这与 Reactcontext特性很相似。

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。

inject 选项应该是:

  • 一个字符串数组,或

  • 一个对象,对象的 key 是本地的绑定名,value 是:

    • 在可用的注入内容中搜索用的 key (字符串或 Symbol),或

    • 一个对象,该对象的:

      • from property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)
      • default property 是降级情况下使用的 value

提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 后代组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

在 2.5.0+ 的注入可以通过设置默认值使其变成可选项:

const Child = {
  inject: {
    foo: { default: 'foo' }
  }
}

如果它需要从一个不同名字的 property 注入,则使用 from 来表示其源 property:

const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: 'foo'
    }
  }
}

与 prop 的默认值类似,你需要对非原始值使用一个工厂方法:

const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: () => [1, 2, 3]
    }
  }
}

$attrs / $listeners

多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用$attrs / $listeners,比如父组件向孙子组件传递数据时。

$attrs包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外)。

$listeners包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。

可能有些小伙伴不懂,下面举个例子就明白了

//子组件
props: ['msg'],
created() {
  // 获取没在props中定义的但是传递过来的属性,除去 class、style
  console.log(this.$attrs); // {id: 'childId', name: 'randy'}
  
  // 获取除.native修饰的原生事件
  console.log(this.$listeners); // {customChange(){}}
  
  // 如果单纯只传递属性给子组件,不需要处理,我们可以直接传递$attrs、$listeners
},

// 父组件
<template>
  <child :msg="msg" @click.native="handleClick" @customChange="handleChange" class="child" id="childId"></child>
</template>
<script>
export default {
  data() {
    return {msg: "我会传递给子组件", name: 'randy'}
  },
}

slot

slot 可以用在父组件直接向子组件传递内容。我们在子组件标签内的内容都可以传递过去。

没命名的slot默认名称是default

// 父组件
<h3>默认插槽 default</h3>
<Slot1>
  <div>slot1</div>
  <div>哈哈</div>
</Slot1>

<h3>老具名插槽</h3>
<Slot1>
  <div>slot传递过来的 2.6已被废弃但是还能使用 vue3彻底移除</div>
  <div slot="header">header</div>
  <div slot="footer">footer</div>
  <div slot="content">content</div>
</Slot1>

<h3>新具名插槽</h3>
<Slot1>
  <template v-slot:header>header</template>
  <template v-slot:footer>footer</template>
  <template v-slot:content>content</template>
</Slot1>

<h3>新具名插槽缩写 #</h3>
<Slot1>
  <div>v-slot传递过来的</div>
  <template #header>header2</template>
  <template #footer>footer2</template>
  <template #content>content2</template>
</Slot1>

// 子组件 Slot1
<!-- 默认插槽 -->
<slot>我是后备内容,没有传递的时候展示</slot>

<slot name="header"></slot>
<slot name="content"></slot>
<slot name="footer"></slot>

不单父组件传递内容给子组件,子组件还可以通过作用域插槽传递数据给父组件。

// 父组件
<h3>老作用域插槽</h3>
<!-- scope 被 2.5.0 新增的 slot-scope 取代 -->
<!-- 除了 scope 只可以用于 <template> 元素,其它和 slot-scope 都相同。 -->
<Slot2>
  <template slot="main" slot-scope="slotProps">
    <div>user name: {{ slotProps.user.name }}</div>
    <div>user age: {{ slotProps.user.age }}</div>
    <div></div>
  </template>
  <template slot-scope="slotProps">
    <div>user name: {{ slotProps.user.name }}</div>
    <div>user age: {{ slotProps.user.age }}</div>
  </template>
  <template slot="footer" slot-scope="{ user: { name, age } }">
    <div>user name: {{ name }}</div>
    <div>user age: {{ age }}</div>
  </template>
</Slot2>

<h3>新作用域插槽</h3>
<Slot2>
  <template v-slot:main="slotProps">
    <div>user name: {{ slotProps.user.name }}</div>
    <div>user age: {{ slotProps.user.age }}</div>
    <div></div>
  </template>
  <template v-slot:default="slotProps">
    <div>user name: {{ slotProps.user.name }}</div>
    <div>user age: {{ slotProps.user.age }}</div>
  </template>
  <template v-slot:footer="{ user: { name, age } }">
    <div>user name: {{ name }}</div>
    <div>user age: {{ age }}</div>
  </template>
</Slot2>

// 子组件 Slot2
<template>
  <slot v-bind:user="user1"> </slot>
  <slot name="main" v-bind:user="user2"> </slot>
  <slot name="footer" :user="user3"> </slot>
</template>

export default {
  data() {
    return {
      user1: {
        name: "randy",
        age: 27,
      },
      user2: {
        name: "demi",
        age: 24,
      },
      user3: {
        name: "jack",
        age: 21,
      },
    };
  },
};

顺便说一说this.$slotsthis.$scopedSlots

this.$slots

this.$slots用来访问被插槽分发的内容。每个具名插槽有其相应的 property (例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到)。default property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。

每个属性包含一个VNode的数组。

请注意插槽不是响应性的。如果你需要一个组件可以在被传入的数据发生变化时重渲染,我们建议改变策略,依赖诸如 props 或 data 等响应性实例选项。

this.$scopedSlots

this.$scopedSlots用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 的函数。

我们通过调用对应的方法,传递响应的数据,就能得到VNode数组。类似this.$slots的返回值。

console.log(
  this.$scopedSlots.default({
    user: this.user1,
  })
);

就类似于前面的,不过有$scopedSlots$slots就不会有值了。

this.$slots.default

EventBus

EventBus主要利用了Vue实例 $on、$once、$off、$emit这四个方法来进行数据的传递。

首先我们定义一个EventBus

// Bus.js 
import Vue from "vue" 
export default new Vue()
import Bus from "./Bus.js"

// 暴露事件,传递数据
Bus.$emit("sendMsg", "这是要向外部发送的数据")

// 监听事件,进行处理
Bus.$on("sendMsg", data => {
  console.log("这是接收到的数据:", data) 
})

// 监听事件,进行处理,但是只监听一次,后面会自动取消监听
Bus.$once("sendMsg", data => {
  console.log("这是接收到的数据:", data) 
})

// 取消监听
Bus.$off("sendMsg")

Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

这个笔者就不多说了,大家应该都知道,不知道的可以查看Vue2版本官方文档自行学习。

Vue3

Vue3组件通讯有9中

  1. props
  2. emit
  3. ref / expose
  4. attrs
  5. v-model
  6. provide / inject
  7. mitt
  8. Vuex
  9. Pinia

应用场景

Vue3 组件通信方式虽然有很多种,但是不同方式有不同的应用场景。

父子组件通信

  • props
  • emit
  • attrs
  • ref / expose
  • v-model

兄弟组件通信

  • mitt
  • Vuex
  • Pinia

跨层级组件通信

  • provide/inject
  • attrs
  • mitt
  • Vuex
  • Pinia

具体使用

props

props用于父组件向子组件传值,使用方式和vue2没什么差别。

// 父组件
<child :msg="msg"></child>

// 子组件
<template>
  <div>{{ msg }}</div>
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
  props: {
    msg: String,
  },
  setup() {
    return {};
  },
});
</script>

// 父组件

emit

emitvue2$emit使用方式是一样的,但是需要注意:

  1. vue3中,emit是在setup函数的第二个参数上。
  2. vue3中,子组件暴露的方法需要在emits定义。
// 父组件
<template>
  <child :msg="msg" @changeMsg="handleChangeMsg"></child>
</template>
<script>
import { defineComponent, ref } from "vue";

export default defineComponent({
  
  setup(props, { emit }) {
    const msg = ref("message");
    const handleChangeMsg = (newValue) => {
      msg.value = newValue;
    };
    return { msg, handleChangeMsg };
  },
});
</script>

// 子组件
<template>
  <div class="">
    <div>{{ msg }}</div>
    <button @click="handelClick">changeMsg</button>
  </div>
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
  props: {
    msg: String,
  },
  emits: { changeMsg: null },
  setup(props, { emit }) {
    const handelClick = () => {
      emit("changeMsg", "我被改变了");
    };
    return { handelClick };
  },
});
</script>

ref / expose

vue3中定义ref需要使用ref()定义。

使用的时候需要注意:

  1. 当组件没定义expose暴露内容的时候,通过ref获取到的就是组件自身的内容,也就是setup函数return的内容。

  2. 当定义了expose暴露内容的时候,通过ref获取到的就是组件expose暴露内容,并且setup函数return的内容会失效,也就是会被覆盖。

// 父组件
<template>
  <child :msg="msg" ref="ref1"></child>
</template>
<script>
import { defineComponent, ref, onMounted } from "vue";

export default defineComponent({
  
  setup(props, { emit }) {
    const msg = ref("message");
    const ref1 = ref(null)
    
    onMounted(() => {
      console.log(ref1.value)
      console.log(ref1.value.name)
      console.log(ref1.value.user)
      ref1.value.say()
    })
    
    return { msg, ref1 };
  },
});
</script>

// 子组件
<template>
  <div class="">{{ msg }}</div>
</template>

<script>
import { defineComponent, ref, reactive } from "vue";

export default defineComponent({
  props: ["msg"],
  emits: {},
  setup(props, { expose }) {
    const say = () => {
      console.log("RefChild say");
    };

    const name = ref("RefChild");
    const user = reactive({ name: "randy", age: 27 });

    //  如果定义了会覆盖return中的内容
    // expose({
    //   user,
    // });

    return {
      name,
      user,
      say,
    };
  },
});
</script>

attrs

vue2中,我们知道this.$attrs包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。

vue3中有了更改,attrs被转移到setup的第二个参数context上,context.attrs并且class 和 style也都不再忽略了。也就是说class 和 style也会在attrs里面。

使用方式和vue2还是一样的,多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用attrs,比如父组件向孙子组件传递数据时。

//子组件
props: ['msg'],
setup(props, {attrs}) {
  // 获取没在props中定义的但是传递过来的属性,包括 class、style
  console.log(attrs); // {id: 'childId', name: 'randy', customChange(){}, class: 'child'}
  
  // 如果单纯只传递属性给子组件,不需要处理,我们可以直接传递attrs
},

// 父组件
<template>
  <child :msg="msg" @customChange="handleChange" class="child" id="childId"></child>
</template>
<script>
export default {
  data() {
    return {msg: "我会传递给子组件", name: 'randy'}
  },
}

v-model

vue3v-model我们只需要注意两点:

  1. vue2中,一个组件只能绑定一个v-model,但是vue3中可以绑定多个。

  2. vue2中如果不指明model属性,那么它的值默认是value,事件默认是input事件。但是在vue3中不需要model属性了,并且它的值默认是modelValue,事件默认是update:modelValue事件。

// 父组件
<Child1 v-model="name1" />
<!--比使用默认名,自定义名字为name-->
<!-- <Child1 v-model:name="name1" /> -->

// 子组件
<template>
  <div class="child1">
    <button @click="changeName">改变值</button>
  </div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
  props: {
    modelValue: String,
    // name: String,
  },
  setup(props, context) {
    
    const changeName = () => {
      context.emit("update:modelValue", "demi");
      // context.emit("update:name", "demi");
    };
    return {
      changeName,
    };
  },
});
</script>

如果不想用默认名,我们可以在v-model:后面加上自定义的名字。

provide / inject

provide / inject需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

原理和vue2一样,可能就是方法有所改变。

// 父组件
<script setup>
  import { provide } from "vue"
  provide("name", "沐华")
</script>

// 子组件
<script setup>
  import { inject } from "vue"
  const name = inject("name")
  console.log(name) // 沐华
</script>

上面的写法是setup的语法糖写法,更简洁,小伙伴不要陌生哦。

mitt

我们知道vue3去除了$on $once $off方法,所以没办法再使用 EventBus 跨组件通信了。但是现在有了一个替代的方案 mitt.js,原理还是 EventBus

先安装 npm i mitt -S

然后像以前封装 bus 一样,封装一下

import mitt from 'mitt'
const mitt = mitt()
export default mitt

然后两个组件之间通信的使用

// 组件 A
<script setup>
import mitt from './mitt'
const handleClick = () => {
    mitt.emit('handleChange')
}
</script>

// 组件 B 
<script setup>
import mitt from './mitt'
import { onUnmounted } from 'vue'
const someMethed = () => { ... }
mitt.on('handleChange',someMethed)
onUnmounted(()=>{
    mitt.off('handleChange',someMethed)
})
</script>

Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

这个笔者就不多说了,大家应该都知道,不知道的可以查看Vue3版本官方文档自行学习。

Pinia

如果你之前使用过 vuex 进行状态管理的话,那么 pinia 就是一个类似的插件。它是最新一代的轻量级状态管理插件。按照尤雨溪的说法,vuex 将不再接受新的功能,建议将 Pinia 用于新的项目。

这个笔者就不多说了,大家可以自行查看官方文档进行学习。

React

React组件通讯有7中

  1. props
  2. context
  3. ref
  4. children
  5. EventEmitter
  6. Redux
  7. Mobx

image.png

React 组件通信方式虽然有很多种,但是不同方式有不同的应用场景。

应用场景

父子组件通信

  • props
  • context
  • ref
  • children

兄弟组件通信

  • EventEmitter
  • Redux

跨层级组件通信

  • context
  • EventEmitter
  • Redux

具体使用

props

props用于父组件向子组件传值,可以传递属性或者方法甚至组件。

// 父组件
import React from "react";
import Children3 from "../components/Children3";

class Parent1 extends React.Component {
  constructor() {
    super();
    this.state = {
      name: "randy",
    };
  }

  changeName = () => {
    this.setState({
      name: "demi",
    });
  };

  render() {
    return (
      <div>
        <Children3
          name={this.state.name}
          changeName={this.changeName}
        ></Children3>
      </div>
    );
  }
}

export default Parent1;

// 类子组件
import React, { Component } from "react";

export class Children3 extends Component {
  render() {
    return (
      <div>
        <div>{this.props.name}</div>
        <button onClick={this.props.changeName}>changeName</button>
      </div>
    );
  }
}

export default Children3;

// 函数子组件
export default function Children3(props) {
  return (
    <div>
      <div>{props.name}</div>
      <button onClick={props.changeName}>changeName</button>
    </div>
  );
}

我们可以发现,在react中子组件向父组件传递消息是通过回调函数的方式,也就是父组件把方法以props的形式传递给子组件来调用。这一块和vue还是有很大差别的。

context

context上下文,用于父子或子孙组件之间通信,类似 vueprovide/inject

// 父组件
import React from "react";

// 创建带默认值的 context
const UserContext = React.createContext("randy");

// 传递值
<UserContext.Provider value="demi">
  <Context1></Context1>
</UserContext.Provider>

// 子组件/孙组件
// 类组件方式1
class Context1 extends React.Component {
  static contextType = UserContext;
  
  // 获取值
  render() {
    return (<div>{this.context}</div>)
  }
}

// 类组件方式2
Context1.contextType = UserContext;

// 类组件方式3
<UserContext.Consumer>
  { name => (<div>{name}</div>)}
</UserContext.Consumer>

// 函数式组件方式1
import { useContext } from "react";
function Context1() {
  const context = useContext(UserContext)
  
  return (<div>{context}</div>)
}

// 函数式组件方式2
function Context1() {
  return (
    <UserContext.Consumer>
      { name => (<div>{name}</div>)}
    </UserContext.Consumer>
  )
}

可以看到 context 使用方式有很多种。传递单个context使用哪种方式都可以,但是当传递多个context的时候只能使用Consumer的方式。使用Consumer嵌套可以获取多个context。这里笔者就不再细说了。

ref

通过ref可以获取到子组件实例,然后使用组件上的属性或方法,这个和vue是类似的。

// 类 父组件 使用createRef创建
import React from "react";

class RefTest extends React.Component {
  constructor() {
    super();
    this.ref1 = React.createRef();
  }

  componentDidMount() {
    // 获取的是组件
    console.log(this.ref1.current);
    // 回调的方式不需要.current
    console.log(this.ref2);
  }

  render() {
    return (
      <div>
        <Ref2 ref={this.ref1}></Ref2>

        <Ref3 ref={(el) => (this.ref2 = el)}></Ref3>
      </div>
    );
  }
}

export default RefTest;

// 函数 父组件 使用useRef创建
import { useRef } from "react";

const RefTest2 = () => {
  const ref1 = useRef();
  let ref3 = null;

  const outputRefs = () => {
    console.log(ref2.current);
    console.log(ref3);
  };

  return (
    <div>
      <Ref2 ref={ref2}></Ref2>
      <Ref3 ref={(el) => (ref3 = el)}></Ref3>
      
      <button onClick={outputRefs}>输出refs</button>
    </div>
  );
};

export default RefTest2;

创建ref的方式有createRef useRef和回调函数的方式。但是有一点我们需要注意,你不能在函数组件上使用 ref 属性,因为它们没有实例。如果想要获取函数式组件的ref,需要使用forwardRef并配合useImperativeHandle才能使用。这里笔者就不细说了。

children

react中,写在组件中的内容都是children,类似vue里面的插槽slot

根据插入内容的多少children可能是一个数组也可能是一个对象。当只有一个内容的时候是一个对象,当有多个内容的时候是一个数组。

// 父组件
<Children1>
  <div>child内容1</div>
  <div>child内容2</div>
</Children1>

// 子组件
// 通过 this.props.children 获取内容
// 上面传递了两个内容 所以this.props.children是一个数组。

props和插槽同时存在的时候,插槽为准。

// 父组件
<Children2 children="哈哈">我会被覆盖吗</Children2>

// 子组件
this.props.children 等于 我会被覆盖吗

EventEmitter

react并不支持自定义事件,所以没有vue的$on $off $emit $once等方法。所以需要借助外部插件。

EventEmitter 就是一种,类似mitt

npm install events

const EventEmitter = require('events')

const ee = new EventEmitter()

ee.on('message', function (text) {
  console.log(text)
})

ee.emit('message', 'hello world')

Redux

Redux是一个状态管理工具,类似 vuex,可以自行查看官方文档学习。

Mobx

Mobx是一个状态管理工具,类似 Redux,可以自行查看官方文档学习。

对比总结

image.png

相同点

  1. 都支持父子、兄弟、跨层级通信。

  2. 都推崇单向数据流,子组件不能直接修改父组件传递过来的数据。

  3. 都支持自定义事件来通信,比如VueEventBusmittReactEventEmitter

  4. 都支持通过ref获取子组件实例来操作子组件。

  5. 都支持通过插槽的方式直接传递数据。

  6. 都有各自的状态管理库,Vuevuex、piniaReactredux、mobx

不同点

  1. Vue中,子组件如果需要修改父组件数据,需要暴露方法,父组件监听该方法,然后修改数据。但是在React中,是通过属性传递回调函数给子组件调用的方式来修改的。

  2. React的ref相较于Vue使用方式更多并且更强大。可以通过创建和回调的方式创建ref。并且在获取元素方面,不但能获取子组件,还能通过ref转发获取到子组件的DOM元素。

  3. React的插槽相较于Vue稍微逊色一点点,没有具名插槽、作用域插槽那么多种类,都是通过children作为插槽传递内容进行通讯。

系列文章

Vue和React对比学习之生命周期函数(Vue2、Vue3、老版React、新版React)

Vue和React对比学习之组件传值(Vue2 12种、Vue3 9种、React 7种)

Vue和React对比学习之Style样式

Vue和React对比学习之Ref和Slot

Vue和React对比学习之Hooks

Vue和React对比学习之路由(Vue-Router、React-Router)

Vue和React对比学习之状态管理 (Vuex和Redux)

Vue和React对比学习之条件判断、循环、计算属性、属性监听

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!