简介
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>
<div>
<span ref="sigleRef">ref span</span>
<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.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>
<div v-for="(list, index) of lists" :key="index" ref="forRef">
<div>{{ index }}:{{ list }}</div>
</div>
</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,
say,
});
return {
name,
user,
say,
};
},
});
</script>
父组件代码如下
<template>
<div>
<span ref="sigleRef">ref span</span>
<RefChild ref="childRef" />
</div>
</template>
<script>
import RefChild from "@/components/RefChild";
import {
defineComponent,
ref,
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>
<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>
</div>
</template>
<script>
import {
defineComponent,
ref,
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 (
<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;
在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 (
<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;
跟类组件一样,在循环中获取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>
<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
,我们必须借助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 (
<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.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>
<!-- 我们希望把主要内容放这里 -->
</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”。也就是我们上面说的默认插槽。
具名插槽有两个版本,可以使用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>
<main>
<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>
<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>
老版本父组件使用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>
<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.children
和Render 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 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对比学习之路由(Vue-Router、React-Router)
Vue和React对比学习之状态管理 (Vuex和Redux)
Vue和React对比学习之条件判断、循环、计算属性、属性监听
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!