简介
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>
}
可以看出,除了初始化会执行一次,并且在更新的时候会连续执行两次。
我们改造一下,不写回调函数的方式
refclick = (e) => {
this.ref1 = e;
console.log("@", e);
render() {
return <div ref={this.refclick}>这种写法不会调用两次</div>
}
只在初始化的时候输出一次,并且后续更新不会再触发。
函数组件
函数组件可以通过
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
相同点
-
在
Vue
和
React
中都能通过
ref
获取到普通
DOM
元素或者子组件,然后来操作元素或组件。
-
在
Vue
和
React
中都支持在循环中获取
ref
元素数组。
不同点
-
Vue
创建
ref
的方式相较
React
比较单一,而在
React
中可以通过
createRef、useRef
或者回调函数创建
ref
。
-
在
Vue2
中
ref
会被自动绑定到
this.$refs
上,并且在循环里也会自动绑定成一个数组。但是在
Vue3
中需要先定义
ref
变量再进行绑定然后通过该变量获取
ref
,值不再绑定到
this.$refs
上,并且在循环里需要自己传递回调函数来动态绑定。
React
和
Vue3
很相似,需要先创建
ref
变量再进行绑定然后通过该变量获取
ref
,并且在循环里需要自己传递回调函数来动态绑定。
-
React
的
ref
功能更为强大,可以通过
Ref
转发获取子组件里面具体的
DOM
元素,这在
Vue
中是实现不了的。
-
React
中的通过回调函数创建的
ref
,在更新的时候会执行两次。
Slot
相同点
-
在
Vue
和
React
中都能通过插槽的方式传递
DOM
元素或组件。
不同点
-
Vue
插槽种类丰富,并且都已经封装好,直接按需求对应使用即可。在
React
中,没有那么多的插槽种类,只有简单的
props.children
。但是在
React
中我们是可以通过
render props
和
children
配合来实现
Vue
中所有插槽。
-
在
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对比学习之条件判断、循环、计算属性、属性监听
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!