likes
comments
collection
share

Vue和React对比学习之Ref和Slot

作者站长头像
站长
· 阅读数 9

简介

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

今天我们通过对比的方式来学习VueReactRefSlot

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

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

Ref

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

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

Vue

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

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

Vue2

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

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

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

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

    <div v-for="(list, index) of lists" :key="index" ref="forRef">
      <div>{{ index }}:{{ list }}</div>
    </div>

    <RefChild ref="childRef" />
  </div>
</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.forRef); // [div, div, div]
    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>

Vue3

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

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

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

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

// 子组件
<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,
      say,
    });

    return {
      name,
      user,
      say,
    };
  },
});
</script>
// 父组件
<template>
  <div>
    <span ref="sigleRef">ref span</span>

    <div v-for="(list, index) of lists" :key="index" ref="forRef">
      <div>{{ index }}:{{ list }}</div>
    </div>

    <div v-for="(list, index) of lists" :key="index" :ref="setItemRef">
      <div>{{ index }}:{{ list }}</div>
    </div>

    <RefChild ref="childRef" />
  </div>
</template>

<script>
import RefChild from "@/components/RefChild";
import {
  defineComponent,
  ref,
  reactive,
  onMounted,
  onBeforeUpdate,
  onUpdated,
} from "vue";

export default defineComponent({
  components: {
    RefChild,
  },
  setup() {
    const sigleRef = ref(null);
    const forRef = ref(null);
    const childRef = 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(sigleRef.value); // <span>ref span</span>
      console.log(forRef.value); // <div><div>2:3</div></div>
      console.log(itemRefs); // [div, div, div]
      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, forRef, childRef, lists, setItemRef };
  },
});
</script>

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

React

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

React定义ref的方式有很多,可以通过createRef、useRef或者回调的方式创建。通过createRef、useRef创建的ref我们需要通过.current获取,通过回调函数方式创建的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 (
      <div>
        <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}
          </div>
        ))}
        {[1, 2, 3].map((item, index) => (
          <div key={index} ref={this.setItems}>
            {index}: {item}
          </div>
        ))}
      </div>
    );
  }
}

export default RefTest;

函数组件

函数组件可以通过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]
  };

  return (
    <div>
      <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}
        </div>
      ))}
      {[1, 2, 3].map((item, index) => (
        <div key={index} ref={setItems}>
          {index}: {item}
        </div>
      ))}

      <button onClick={outputRefs}>输出refs</button>
    </div>
  );
};

export default RefTest2;

我们可以发现,React在循环中获取ref不管是类组件还是函数组件也是需要传递一个回调函数获取ref数组的,如果不传递回调函数则获取的是最后一个元素。这个和Vue3是有点像的。

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>
      <div className="class1">ref1 content1</div>

      {/* ref挂在哪个元素上面就会是哪个元素 */}
      <div className="class2" ref={ref}>
        ref1 content2
      </div>
    </div>
  );
});

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,我们必须借助forwardRefuseImperativeHandle 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 (
    <div>
      <input type="text" ref={inputRef} defaultValue="ref7" />
    </div>
  );
});

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.childrenrender 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>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>

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

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

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

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

具名插槽有两个版本,可以使用slotv-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>
  <main>
    <div>This is main content.</div>
  </main>
  <footer>
    <div>This is footer content.</div>
  </footer>
</div>

作用域插槽

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

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

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

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

老版本父组件使用scopeslot-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.childrenRender 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>
  <div>
    This is new component content.
  </div>
</NewComponent>

渲染结果如下

<div>
  <h2>This is new component header.</h2>
  <div>
    This is new component content.
  </div>
</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.childrenRender props来实现具名插槽。

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

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

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

// 子组件
render() {
  const {header, footer, children} = this.props

  return (
    <div class="container">
      <header>
        {header}
      </header>
      <main>
        {children}
      </main>
      <footer>
        {footer}
      </footer>
    </div>
  )
}

这里我们的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>
  <main>
    <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 propschildren冲突的时候会以哪个为准呢?

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

我们来看一看

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

最后渲染结果如下

我会被覆盖吗

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

对比总结

Ref

相同点

  1. VueReact中都能通过ref获取到普通DOM元素或者子组件,然后来操作元素或组件。

  2. VueReact中都支持在循环中获取ref元素数组。

不同点

  1. Vue创建ref的方式相较React比较单一,而在React中可以通过createRef、useRef或者回调函数创建ref

  2. Vue2ref会被自动绑定到this.$refs上,并且在循环里也会自动绑定成一个数组。但是在Vue3中需要先定义ref变量再进行绑定然后通过该变量获取ref,值不再绑定到this.$refs上,并且在循环里需要自己传递回调函数来动态绑定。ReactVue3很相似,需要先创建ref变量再进行绑定然后通过该变量获取ref,并且在循环里需要自己传递回调函数来动态绑定。

  3. Reactref功能更为强大,可以通过Ref转发获取子组件里面具体的DOM元素,这在Vue中是实现不了的。

Slot

相同点

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

不同点

  1. Vue插槽种类丰富,并且都已经封装好,直接按需求对应使用即可。在React中,没有那么多的插槽种类,只有简单的props.children。但是在React中我们是可以通过render propschildren配合来实现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对比学习之条件判断、循环、计算属性、属性监听

后记

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