簡介
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
上。
在
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
在
React
中
ref
被用來給元素或子元件注冊引用資訊。如果在普通的 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
,我們必須借助
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
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樣式