+关注继续查看

简介

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

今天我们通过对比的方式来学习 Vue React Ref Slot

本文首先讲述了 Vue React 各自支持的 Ref Slot 以及具体的使用,然后通过对比总结了它们之间的相同点和不同点。

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

Ref

Ref 可以帮助我们更方便的获取子组件或 DOM 元素。

当我们使用 ref 拿到子组件的时候,就可以使用子组件里面的属性和方法了,跟子组件自己在调用一样。

Vue

Vue ref 被用来给元素或子组件注册引用信息。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

关于 ref 注册时间的重要说明:因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在! $refs 也不是响应式的,因此你不应该试图用它在模板中做数据绑定。

Vue2

Vue2 中,使用 ref 我们并不需要定义 ref 变量,直接绑定即可,所有的 ref 都会绑定在 this.$refs 上。

子组件代码如下

<template>
  <div>{{ title }}</div>
</template>
<script>
export default {
  data() {
    return {
      title: "ref 子组件",
  methods: {
    say() {
      console.log("hi:" + this.title);
</script>

父组件代码如下

<template>
    <span ref="sigleRef">ref span</span>
    <RefChild ref="childRef" />
</template>
<script>
import RefChild from "@/components/RefChild";
export default {
  components: {
    RefChild,
  data() {
    return {
      lists: [1, 2, 3],
  mounted() {
    console.log(this.$refs.sigleRef); // <span>ref span</span>
    console.log(this.$refs.childRef); // 输出子组件
    // 直接可以使用子组件的方法和属性
    console.log(this.$refs.childRef.title); // ref 子组件
    this.$refs.childRef.say(); // hi:ref 子组件
    // 类似子组件自己调用
    console.log(this.$refs.childRef.$data); // {title: "ref 子组件"}
    console.log(this.$refs.childRef.$props); // 获取传递的属性
    console.log(this.$refs.childRef.$parent); // 获取父组件
    console.log(this.$refs.childRef.$root); // 获取根组件
</script>

Vue2 中当 v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的 数组

<template>
    <div v-for="(list, index) of lists" :key="index" ref="forRef">
      <div>{{ index }}:{{ list }}</div>
</template>
<script>
export default {
  data() {
    return {
      lists: [1, 2, 3],
  mounted() {
    console.log(this.$refs.forRef); // [div, div, div]
</script>

Vue3

Vue3 中,我们需要先使用 ref 创建变量,然后再绑定。之后 ref 也是通过该变量获取,这个和 Vue2 是有区别的。

子组件代码如下

<template>
  <div>{{ msg }}</div>
</template>
<script>
import { defineComponent, ref, reactive } from "vue";
export default defineComponent({
  props: ["msg"],
  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,
</script>

父组件代码如下

<template>
    <span ref="sigleRef">ref span</span>
    <RefChild ref="childRef" />
</template>
<script>
import RefChild from "@/components/RefChild";
import {
  defineComponent,
  onMounted,
} from "vue";
export default defineComponent({
  components: {
    RefChild,
  setup() {
    const sigleRef = ref(null);
    const childRef = ref(null);
    onMounted(() => {
      console.log(sigleRef.value); // <span>ref span</span>
      console.log(childRef.value); // 输出子组件
      // 直接可以使用子组件暴露的方法和属性
      console.log(childRef.value.name); // undefined
      console.log(childRef.value.user); // {name: 'randy', age: 27}
      childRef.value.say(); // RefChild say
      // 类似子组件自己调用
      console.log(childRef.value.$data); // {}
      console.log(childRef.value.$props); // 获取传递的属性 {msg: undefined}
      console.log(childRef.value.$parent); // 获取父组件
      console.log(childRef.value.$root); // 获取根组件
    return { sigleRef, childRef};
</script>

Vue2 中,在 v-for 中使用的 ref attribute 会用 ref 数组填充相应的 $refs property。当存在嵌套的 v-for 时,这种行为会变得不明确且效率低下。

Vue3 中,此类用法将不再自动创建 $ref 数组。要从单个绑定获取多个 ref ,请将 ref 绑定到一个更灵活的 函数 上 (这是一个新特性)。

如果没绑定函数,而是 ref 则获取的是最后一个元素。

<template>
    <div v-for="(list, index) of lists" :key="index" ref="forRef">
      <div>{{ index }}:{{ list }}</div>
    <div v-for="(list, index) of lists" :key="index" :ref="setItemRef">
      <div>{{ index }}:{{ list }}</div>
</template>
<script>
import {
  defineComponent,
  reactive,
  onMounted,
  onBeforeUpdate,
  onUpdated,
} from "vue";
export default defineComponent({
  setup() {
    const forRef = ref(null);
    const lists = reactive([1, 2, 3]);
    let itemRefs = [];
    const setItemRef = (el) => {
      if (el) {
        itemRefs.push(el);
    onBeforeUpdate(() => {
      itemRefs = [];
    onUpdated(() => {
      console.log(itemRefs);
    onMounted(() => {
      console.log(forRef.value); // <div><div>2:3</div></div>
      console.log(itemRefs); // [div, div, div]
    return { forRef, lists, setItemRef };
</script>

这里我们再提一嘴,在 Vue3 中,默认是暴露 setup 函数 return 里面的内容。但是如果想限制暴露的内容则可以定义 expose ,如果定义了 expose 则会以 expose 为准,会覆盖 setup 函数中 return 的内容。

React

React ref 被用来给元素或子组件注册引用信息。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

React 定义 ref 的方式有很多,可以通过 createRef、useRef 或者回调的方式创建。通过 createRef、useRef 创建的 ref 我们需要通过 .current 获取,通过回调函数方式创建的 ref 则可以直接获取。

React 其实也是支持类似 vue 的通过字符串的方式创建 ref ,然后通过 this.refs.xxx 获取某 ref 。但是这种方式官方已经不推荐使用了,我们了解即可。

类组件

类组件可以通过 createRef 或回调函数的方式创建 ref

// 类父组件
import React from "react";
import Ref2 from "../components/Ref2";
import Ref3 from "../components/Ref3";
const ref2 = React.createRef();
class RefTest extends React.Component {
  constructor() {
    super();
    this.ref3 = null
    this.ref8 = React.createRef();
    this.ref9 = React.createRef();
    this.refItems = [];
  componentDidMount() {
    // 获取的是组件
    console.log(ref2.current); // 获取子组件
    ref2.current.say(); // 调用子组件方法
    // 回调的方式不需要.current
    console.log(this.ref3); // 获取子组件
    console.log(this.ref8.current); // <div>普通元素</div>
    // 循环
    console.log(this.ref9.current); // <div>2: 3</div>
    console.log(this.refItems); // [div, div, div]
  setItems = (el) => {
    if (el) {
      this.refItems.push(el);
  render() {
    return (
        <Ref2 ref={ref2}></Ref2>
        <Ref3 ref={(el) => (this.ref3 = el)}></Ref3>
        <div ref={this.ref8}>普通元素</div>
        {[1, 2, 3].map((item, index) => (
          <div key={index} ref={this.ref9}>
            {index}: {item}
        {[1, 2, 3].map((item, index) => (
          <div key={index} ref={this.setItems}>
            {index}: {item}
export default RefTest;

React 的类组件中,支持 createRef 和回调函数的方式创建 ref ,并且对于循环的处理是和 vue3 一样的,如果只绑定一个变量就是循环体最后一个元素,如果要获取所有元素则需要使用方法来绑定。

并且对于回调函数的方式我们需要注意:

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null ,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

我们来举个例子来看看

refclick = (e) => {
  this.ref1 = e;
  console.log("@", e);
render() {
  return <div ref={(e) => this.refclick(e)}>这种写法会调用两次</div>
}

可以看出,除了初始化会执行一次,并且在更新的时候会连续执行两次。

image.png

我们改造一下,不写回调函数的方式

refclick = (e) => {
  this.ref1 = e;
  console.log("@", e);
render() {
  return <div ref={this.refclick}>这种写法不会调用两次</div>
}

只在初始化的时候输出一次,并且后续更新不会再触发。

image.png

函数组件

函数组件可以通过 useRef 或回调函数的方式创建 ref

import Ref2 from "../components/Ref2";
import Ref3 from "../components/Ref3";
import { useRef, createRef, useState } from "react";
const RefTest2 = () => {
  const ref2 = useRef();
  let ref3 = null;
  const ref9 = useRef();
  const refItems = [];
  const outputRefs = () => {
    // 获取的是组件
    console.log(ref2.current); // 获取子组件
    ref2.current.say(); // 调用子组件方法
    // 回调的方式不需要.current
    console.log(ref3); // 获取子组件
    // dom
    console.log(ref8.current); // <div>普通元素</div>
    // 循环
    console.log(ref9.current); // <div>2: 3</div>
    console.log(refItems); // [div, div, div]
  const setItems = (el) => {
    if (el) {
      refItems.push(el);
  return (
      <Ref2 ref={ref2}></Ref2>
      <Ref3 ref={(el) => (ref3 = el)}></Ref3>
      <div ref={ref8}>普通元素</div>
      {[1, 2, 3].map((item, index) => (
        <div key={index} ref={ref9}>
          {index}: {item}
      {[1, 2, 3].map((item, index) => (
        <div key={index} ref={setItems}>
          {index}: {item}
      <button onClick={outputRefs}>输出refs</button>
export default RefTest2;

跟类组件一样,在循环中获取 ref 不管是类组件还是函数组件也是需要传递一个回调函数获取 ref 数组的,如果不传递回调函数则获取的是最后一个元素。并且对于回调函数的写法,在组件更新的时候会执行两次。

Ref转发

Vue 中,我们在父组件是没办法拿到子组件具体的 DOM 元素的,但是在 React 中,我们可以通过 Ref 转发来获取到子组件里面的元素。这个是 React 特有的。

// 子组件
import React from "react";
// 第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。
// 常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。
const Ref1 = React.forwardRef((props, ref) => {
  return (
      <div className="class1">ref1 content1</div>
      {/* ref挂在哪个元素上面就会是哪个元素 */}
      <div className="class2" ref={ref}>
        ref1 content2
export default Ref1;
// 父组件
this.ref1 = React.createRef();
// 得到<div class="class2">ref1 content2</div>
console.log(this.ref1.current); // 获取的是子组件里面的DOM
<Ref1 ref={ref1}></Ref1>

Ref 转发通过 forwardRef 方法实现,通过该方法接收 ref ,然后绑定到我们需要暴露的 DOM 元素上,在父组件通过 ref 就能获取到该元素了。

获取函数组件ref

React 中如果子组件时函数式组件是获取不到 ref 的。所以我们不能在函数式组件上定义 ref

如果一定要在函数组件上使用 ref ,我们必须借助 forwardRef useImperativeHandle hook 来实现。 useImperativeHandle hook 可以暴露一个对象,这个对象我们在父组件中就能获取到。

// 子组件
import { useImperativeHandle, useRef, forwardRef } from "react";
const Ref7 = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => {
    // 这个对象在父组件能通过.current获取到
    // 暴露了三个方法
    return {
      focus: () => {
        inputRef.current.focus();
      blur: () => {
        inputRef.current.blur();
      changeValue: () => {
        inputRef.current.value = "randy";
  return (
      <input type="text" ref={inputRef} defaultValue="ref7" />
export default Ref7;
// 父组件
this.ref7 = React.createRef();
console.log(this.ref7.current); // 获取的是子组件 useImperativeHandle 方法里面返回的对象
//直接调用暴露的方法
this.ref7.current.focus();
// this.ref7.current.blur();
// this.ref7.current.changeValue();
<Ref7 ref={ref7}></Ref7>

Slot

Slot 也叫插槽,可以帮助我们更方便的传递内容到子组件。在 Vue 中通过 slot 来实现,在 React 中主要通过 props.children render props 来实现。

插槽可以传递字符串、DOM元素、组件等。

Vue

Vue 支持默认插槽、具名插槽、作用域插槽。

我们在在子组件标签里面定义的内容都可以认为是插槽,在 Vue 中需要在子组件使用 slot 接收插槽内容,不然不会展示。

默认插槽

默认插槽使用很简单。

<todo-button>randy</todo-button>

然后在 <todo-button> 的模板中,你可能有:

<button class="btn-primary">
  <slot></slot>
</button>

当组件渲染的时候, <slot></slot> 将会被替换为“randy”。

<button class="btn-primary">randy</button>

我们还可以在 <slot></slot> 中定义备选内容,也就是父组件没传递内容的时候子组件该渲染的内容。

<button class="btn-primary">
  <slot>我是备选内容</slot>
</button>

当我们父组件没传递任何内容的时候,

<todo-button></todo-button>

渲染如下

<button class="btn-primary">我是备选内容</button>

具名插槽

有时候我们需要传递多个插槽,并且每个插槽渲染在不同的地方该怎么呢?比如我们想定义一个 layout 组件。

<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>

这个时候就需要用到具名插槽了。

对于这样的情况, <slot> 元素有一个特殊的 attribute: name 。通过它可以为不同的插槽分配独立的 ID,也就能够以此来决定内容应该渲染到什么地方:

// 子组件
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

一个不带 name <slot> 出口会带有隐含的名字“default”。也就是我们上面说的默认插槽。

具名插槽有两个版本,可以使用 slot v-slot 传递, slot 的方式在 2.6已被废弃但是还能使用。下面我们都来说一说。

注意, v-slot 只能添加在 <template>

slot 方式

// 父组件
<base-layout>
  <template slot="header">
    <div>This is header content.</div>
  </template>
  <!-- 默认插槽也可以不用定义 -->
  <template slot="default">
    <div>This is main content.</div>
  </template>
  <template slot="footer">
    <div>This is footer content.</div>
  </template>
</base-layout>

v-slot 方式

// 父组件
<base-layout>
  <template v-slot:header>
    <div>This is header content.</div>
  </template>
  <template v-slot:default>
    <div>This is main content.</div>
  </template>
  <template v-slot:footer>
    <div>This is footer content.</div>
  </template>
</base-layout>

v-slot 还有简写形式,用 # 代替 v-slot:

// 父组件
<base-layout>
  <template #header>
    <div>This is header content.</div>
  </template>
  <template #default>
    <div>This is main content.</div>
  </template>
  <template #footer>
    <div>This is footer content.</div>
  </template>
</base-layout>

最后渲染结果如下

// 子组件
<div class="container">
  <header>
    <div>This is header content.</div>
  </header>
    <div>This is main content.</div>
  </main>
  <footer>
    <div>This is footer content.</div>
  </footer>
</div>

作用域插槽

有时候我们在父组件传递插槽内容的时候希望可以访问到子组件的数据,这个时候就需要用到作用域插槽。

作用域插槽也有新老两个版本,老版本使用 scope slot-scope 接收属性值,新版本使用 v-slot 接收属性值。

除了 scope 只可以用于 <template> 元素,其它和 slot-scope 都相同。但是 scope 被 2.5.0 新增的 slot-scope 取代。

// 子组件 通过v-bind绑定数据到slot上
<template>
    <slot v-bind:user="user1"> </slot>
    <slot name="main" v-bind:user="user2"> </slot>
    <slot name="footer" :user="user3"> </slot>
</template>
<script>
export default {
  data() {
    return {
      user1: {
        name: "randy",
        age: 27,
      user2: {
        name: "demi",
        age: 24,
      user3: {
        name: "jack",
        age: 21,
</script>

老版本父组件使用 scope slot-scope 来接收属性值,以 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>

scope 用法是一样的,只是把 slot-scope 替换成 scope 即可。

新版本父组件使用 v-slot 来接收属性值

// 父组件
<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>

React

React 没有 Vue 那么多种类的插槽,但是通过 this.props.children Render props 配合使用都能实现出 Vue 中的插槽功能。

render prop 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。不懂的小伙伴可以查看 React 官方文档

默认插槽

默认插槽可以通过 this.props.children 来实现。 this.props.children 能获取子组件标签内的所有内容。当传递的元素只有一个的时候 this.props.children 是一个对象,当传递的元素有多个的时候 this.props.children 是一个数组。

class NewComponent extends React.Component {
  constructor(props) {
    super(props);
  render() {
    return <div>{this.props.children}</div>
}

function 组件使用 props.children 获取子元素内容。

function NewComponent(props) {
  return <div>>{props.children}</div>
}

父组件使用 NewComponent 组件,传递内容。

<NewComponent>
  <h2>This is new component header.</h2>
    This is new component content.
</NewComponent>

渲染结果如下

<div>
  <h2>This is new component header.</h2>
    This is new component content.
</div>

我们还可以在子组件中定义备选内容,也就是父组件没传递内容的时候子组件该渲染的内容。

render() {
  const {children} = this.props
  return (
    <button class="btn-primary">
      {children ? children : '我是备选内容'}
    </button>
}

当我们父组件没传递任何内容的时候

<todo-button></todo-button>

渲染如下

<button class="btn-primary">我是备选内容</button>

具名插槽

我们可以使用 this.props.children Render props 来实现具名插槽。

比如我们想实现一个效果如下的 layout 组件

<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>

我们可以用 render props 传递具名内容实现类似 vue 的具名插槽。使用 children 传递默认内容实现类似 vue 的默认插槽。

// 子组件
render() {
  const {header, footer, children} = this.props
  return (
    <div class="container">
      <header>
        {header}
      </header>
        {children}
      </main>
      <footer>
        {footer}
      </footer>
}

这里我们的 render props 简化了一下没有传递渲染函数而是直接传递组件。

// 父组件
<base-layout 
  header={<div>This is header content.</div>}
  footer={<div>This is footer content.</div>}
  <div>This is main content.</div>
</base-layout>

渲染结果如下

// 子组件
<div class="container">
  <header>
    <div>This is header content.</div>
  </header>
    <div>This is main content.</div>
  </main>
  <footer>
    <div>This is footer content.</div>
  </footer>
</div>

当然内容复杂的话,我们可以使用 render props 传递渲染函数,传递渲染函数这也是官方推荐的使用方式。

作用域插槽

同样,在 React 中也能通过 Render props 实现类似 Vue 中的作用域插槽。

父组件传递渲染函数方法

// 父组件
import React from 'react';
import Children4 from './Children4.js';
class Index extends React.Component{
  constructor(props) {
    super(props);
  info = (data) => {
    return <span>{data}</span>;
  render() {
    return (
      <Children4 element={this.info}></Children4>
export default Index;

子组件调用父组件传递的渲染函数方法,并且传递参数过去。

// 子组件
import React from "react";
class Children4 extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      info: "子组件数据",
  render() {
    return <div>{this.props.element(this.state.info)}</div>;
export default Children4;

渲染结果如下

<div><span>子组件数据</span></div>

说到这好奇宝宝可能会问当 render props children 冲突的时候会以哪个为准呢?

比如在父组件传递了 children props 属性,然后又传递了 children 插槽。

我们来看一看

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

最后渲染结果如下

我会被覆盖吗

可以看到,同名 render props 属性会被 children 插槽覆盖。

对比总结

Ref

相同点

  1. Vue React 中都能通过 ref 获取到普通 DOM 元素或者子组件,然后来操作元素或组件。
  2. Vue React 中都支持在循环中获取 ref 元素数组。

不同点

  1. Vue 创建 ref 的方式相较 React 比较单一,而在 React 中可以通过 createRef、useRef 或者回调函数创建 ref
  2. Vue2 ref 会被自动绑定到 this.$refs 上,并且在循环里也会自动绑定成一个数组。但是在 Vue3 中需要先定义 ref 变量再进行绑定然后通过该变量获取 ref ,值不再绑定到 this.$refs 上,并且在循环里需要自己传递回调函数来动态绑定。 React Vue3 很相似,需要先创建 ref 变量再进行绑定然后通过该变量获取 ref ,并且在循环里需要自己传递回调函数来动态绑定。
  3. React ref 功能更为强大,可以通过 Ref 转发获取子组件里面具体的 DOM 元素,这在 Vue 中是实现不了的。
  4. React 中的通过回调函数创建的 ref ,在更新的时候会执行两次。

Slot

相同点

  1. Vue React 中都能通过插槽的方式传递 DOM 元素或组件。

不同点

  1. Vue 插槽种类丰富,并且都已经封装好,直接按需求对应使用即可。在 React 中,没有那么多的插槽种类,只有简单的 props.children 。但是在 React 中我们是可以通过 render props children 配合来实现 Vue 中所有插槽。
  2. React 中,不但能传递字符串、 DOM 元素和组件,还能传递渲染函数。在 Vue 中可以传递字符串、 DOM 元素和组件,但是没有传递渲染函数这种用法的。

系列文章

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对比学习之条件判断、循环、计算属性、属性监听

后记

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

【前端架构】从 JQuery 到 React、Vue、Angular——前端框架的演变及其差异
【前端架构】从 JQuery 到 React、Vue、Angular——前端框架的演变及其差异