天天看點

Vue和React對比學習之Ref和Slot

簡介

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

相同點

  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

    中是實作不了的。

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樣式

繼續閱讀