首頁 > 軟體

vue和react中關於插槽詳解

2022-08-19 14:01:07

簡述Slot

slot插槽是Vue對元件巢狀這種擴充套件機制的稱謂,在react可以也這樣稱呼,但是並不很常見。不過叫slot確實很形象。

這樣的形式就是slot插槽:

vue

<template>
  <container-comp>
    <content></content>
    <footer></footer>
  </container-comp>
</template>

react

() => (
  <ContainerComp>
    <Content />
    <Footer />
  </ContainerComp>
)

(我們可以把container-comp稱之為容器元件,把content、footer稱之為子元件)

這種機制的好處主要在於,在某個容器提供的模版或者資料中,可以根據需求靈活擴充套件需要渲染的子元件。專業點說就是通過容器和子元件之間的協定(資料交換和渲染方式),將彼此邏輯獨立解藕,提升了各自的複用性。

舉個例子,容器元件提供了一份渲染模版,將各個模組的位置預留出來,使用的時候根據各個子元件的順序或者插槽名稱,在不同場景可以選擇不同的子元件。

再舉個使用插槽的例子,容器元件中提供一些資料,比如定時請求某種介面得到資料,或者監聽,訂閱一些資料,比如在掛載完成後監聽滑鼠事件,得到位置資料。資料拿到之後,具體的對資料的渲染方式交給子元件來做,而這時候,容器可以通過引數傳遞的方式將得到的資料交給子元件。

通過上面簡單的的例子描述,可以看到,使用插槽的的程式碼設計符合單一職責原則,邏輯更加內聚。

而不管是vue還是react上述描述的這些功能都是支援的,只是有的叫法略有所別,但是其目的一致。(因為近期在專案中使用vue多一點,而之前對vue的瞭解只是籠統的學習過響應式原理,並沒有真正在專案裡寫過vue,所以,近期希望結合vue和react,來系統的回顧回顧相關的知識點。)

下面就先看下vue中的插槽都有什麼功能。一邊看vue,一邊對比react。 參考這裡:vue2官網:slot,可以看到,插槽相關的核心功能有:

  • 基本渲染插槽內容;
  • 具名插槽:一個容器多個插槽,需要分別命名;
  • 插槽傳遞引數,屬於相對高階點的用法;

基本插槽

vue基本插槽

最簡單插槽用法就像下面這樣:

<template>
  <main-comp>
    <div>內容</div>
    <!--  
    可以是任何自定義元件
    <my-sub></my-sub>
    -->
  </main-comp>
</template>

main-comp就是容器元件,而我們將<div>內容</div>作為容器的子元素,那麼,容器裡面怎麼寫呢?

<template>
  <div class="main">
    <slot>後備資訊,以防萬一</slot>
  </div>
</template>

可以看到,在容器裡面簡單的使用slot標籤,相當於佔位。當元件渲染的時候,<slot></slot> 將會被替換為<div>內容</div>,如果沒有使用時插槽的話,會渲染出slot標籤內的內容:“後備資訊,以防萬一”。當然,使用的插槽不止是<div>內容<div>這麼簡單,可以是任何自定義元件。

ok,對應react中實現對應的程式碼怎麼寫呢?

react基本插槽

使用插槽元件:

() => (
  <MainComp>
    <div>內容</div>
    {/* <MySub />  可以是任何自定義元件 */}
  </MainComp>
)

容器中定義插槽:

const MainComp = (props) => {
  return (
    <div class="main">
      {props.children ?? '後備資訊,以防萬一'}
    </div>
  )
}

react中,元件的子元件都存在props.children中,所以直接在jsx中渲染對應的位置渲染props.children變數就可以了,而後備內容可以應用任何js語法,來判斷props.children收否存在,從而顯示後備內容與否。

具名插槽

vue具名插槽

上面是最簡單的場景,但有時候,一個容器需要渲染很多插槽,比如需要渲染一個內容區域content和一個底部區域footer,這時候就需要對插槽命名了,稱之為具名插槽: 下面main-comp元件要使用兩個插槽,分別命名為content和footer:

<template>
  <main-comp>
    <template v-slot:content>
      <sub-comp1></sub-comp1>
    </template>
    <template  v-slot:footer>
      <footer-comp></footer-comp>
    </template>
  </main-comp>
</template>

定義插槽:

<template>
  <div>
    <slot name="content"></slot>
    <slot name="footer"></slot>
  </div>
</template>

slot用name屬性標註了其名字“xxx”,對應使用的時候要用v-slot:xxx,這樣“xxx”的template就會對應替換成name是xxx的slot的位置。有一點需要注意,v-slot這個指令對template生效,也就是說,要使用具名插槽,必須要用template將內容包裹起來。

另外,如果slot顯示指定name,其實它對應也是有name的,它預設的name叫default。

react具名插槽的討論

具名插槽,對應react的話,我用了這些年react,還真沒聽過“具名插槽”這個稱謂。不過儘管沒有100%一致的對應vue的具名插槽,但類似的功能有幾種實現方式:

模仿具名插槽

a. 用次序約定:

我們知道react的“插槽”寫法(巢狀子元件),子元件都是作為props.children陣列的子元素,那麼其實最簡單的一種方式就是,children的次序對應著某個子插槽。比如:
使用時:

() => {
  return (
    <MainComp>
      <SubComp1 />
      <FooterComp />
    </MainComp>
  )
}

那麼,props.children[0]對應的就是SubComp1,而props.children[1]對應的就是FooterComp,所以在MainComp內部就可以這樣:

const MainComp = (props) => {
  const content = props.children[0]
  const footer = props.children[1]
  
  return (
  <div>
    { content }
    { footer }
  </div>
  )
}

但是上述寫法的問題在於:具名呢?說好的名稱呢?使用的時候順序亂了咋辦?

b. 傳遞物件:

想要實現具名,可以下面這樣寫法,本質還是props.children,我們把物件作為“插槽”內容,物件的key就是插槽名稱,value就是子元件:

() => {
  return (
    <MainComp>
      {
        {
          content:(<SubComp1 />),
          footer:(<FooterComp />)
        }
      }
    </MainComp>
  )
}

對應的MainComp:

const MainComp = (props) => {
  const { content, footer  } = props.children
  return (
  <div>
    { content }
    { footer }
  </div>
  )
}

這裡這個props.children可以直接解構物件的屬性。(有一點要注意的是:props.children不一定是陣列,當只有一個元素的時候就不是陣列)。

c. 判斷元件的自定義靜態,實現具名

上述這種寫法看上去和vue的功能一致了,但是坦白講,這樣的程式碼在react世界裡面實屬罕見。不是說寫法錯誤,但是似乎不那麼符合使用習慣。

react中類似具名的插槽其實還可以通過給子元件顯示命名方式實現,其實就是給子元件掛了個靜態變數:

const Content = () => (<div>I am Content</div>)
const Footer = (props) => (<div>Footer Here {props.info}</div>)

// 兩個子元件標記出來
Content.compName =  'content'
Footer.compName =  'footer'

這樣在容器元件中就能通過這兩個標記,識別出對應的元件:

export function MainComp(props) {

  const isMany = React.Children.count(props.children) > 0
  let footer = (<div>footer</div>)
  let content = (<div>content</div>)

  if (isMany) { 
    React.Children.forEach(props.children, item => {
      const { compName } = item.type
      // 判斷子元件型別
      if (compName === 'footer') footer = item
      if (compName === 'content') content = item
    })
  }

  return (
    <div>
      title text<br/> 
      { content }
      { footer }
    </div>
  )
}

這裡面用了幾個React.Children的方法來判斷props.children,核心邏輯是當children是陣列時,遍歷每一個子項,判斷其“name”(這裡我們的約定為compName),再根據name設定對應需要渲染的子元件的變數。

isMany部分也可以這樣寫:

  try {
    React.Children.only(children)
  } catch (e) {
    React.Children.forEach(children, item => {
      const { compName } = item.type
      if (compName === 'footer') footer = item
      if (compName === 'content') content = item
    })
  }

這樣我們在使用“插槽”元件的時候就不用擔心子元件次序問題了:

() => {
  return (
    <MainComp>
      <Footer /> {/* 先寫footer還是能正確的渲染出來 */}
      <Content />
    </MainComp>
  )
}

上面a、b、c三種方法實現了和vue一樣的具名的slot,第一種只是簡單的次序對應,第二種能實現,但是不太符合react習慣,第三種能實現,也是react的寫法。

不過,一般簡單的需求,沒必要用第三種,可以直接使用屬性傳遞元件,我也不知道應該怎麼叫,就叫屬性插槽吧。

屬性插槽

一個更符合react習慣,近似實現vue具名slot的方法是,直接傳props,但props的型別是元件:

() => {
  return (
    <MainComp
      content={(<SubComp1 />)}
      footer={(<FooterComp />)}  
    /> 
  )
}

這樣在MainComp的內部直接通過props去拿對應的元件並渲染在適當的位置就行:

const MainComp = (props) => {
  const { content, footer } = props
  return (
  <div>
    { content }
    { footer }
  </div>
  )
}

嚴格意義上說,雖然這樣能實現和vue具名插槽一樣的功能,但使用卻不是用插槽的形式。 但是這樣程式碼在react世界中,卻是最常見的方式。這可能是兩種框架不同特點導致的微小差異了。

插槽傳參

vue插槽傳參

插槽傳遞引數,可以幫助我們實現一些更高階的功能。

先看下vue中,如何給插槽傳遞引數:

<template>
  <div class="main">
    <slot 
      :styleProps="shareStyle" 
      :data="shareStyle" 
      :description="desc">
    </slot>
  </div>
</template>

上面的程式碼是定義slot時候,我們給slot繫結了三個屬性,這看上去和我們使用元件,給元件傳遞引數的用法沒有什麼區別。

使用slot的時候這樣接收引數:

  <!-- slotProps 是通過main傳遞過來的 -->
  <template v-slot:default="slotProps">
    <!-- 預設插槽可以簡寫成 v-slot -->
    <div :style="slotProps.styleProps">{{ slotProps.description}}</div>
  </template>

首先我們看到使用的時候在template標籤中使用了指令v-slot,即v-slot:default="slotProps",這句話什麼意思呢?

就是當前這裡template對應default這個名稱的slot(defalut可以省略,上面定義slot的地方也沒寫name=“default”,其實是省略了)。

而這個slotProps表示所有傳遞過來的屬性,也就是說:

slotProps = {
  styleProps, data, description
}

所以在template中,可以使用slotProps上的任何屬性。也可以給作為插槽的自定義元件傳遞引數, 比如:

<template v-slot:rightItem="slotProps">
  <staff :staff="slotProps.data"></staff>
</template>

vue插槽傳參大致就是這樣了,接著看看react吧。

react:render-props

react中其實沒法直接給插槽傳遞引數,只能藉助一點技術手段:函數。

這種方式有個專有名詞叫:render-props。

render-props的具體的方式就是,子元件作為插槽是用函數的形式,而容器元件渲染的時候對應的就呼叫這個函數,在呼叫函數的時候,把需要傳遞的引數傳入函數,這樣在插槽函數的作用域內就拿到了資料:

() => {
  return ( 
    <MainComp>
      {
        (data) => (<Staff staff={data} />)
      } 
    </MainComp>
  )
}

看下容器MainComp元件:

const MainComp = (props) => {

  const [data, setData] = useState({})
  useEffect(() => {
    const info = await getData()
    setData(info)
  }, []) 
  return (
    <div>
      {
        props.children && props.children(data)
      }
    </div>
  )
}

當然了,這種函數形式的“插槽”不止可以用作插槽,用在普通的props上自然也是可以的。在react世界裡,凡是要從另一個元件拿資料的場合,都可以考慮傳個函數:

() => {
  return (
    <MainComp
      staff={(data) => (<Staff staff={data} />)}
    />
  )
}

對應的MainComp,最終也是通過函數呼叫給子元件傳遞引數,只是獲取子元件的方式換一下:

const MainComp = (props) => {
  const [data, setData] = useState({})
  useEffect(() => {
    const info = await getData()
    setData(info)
  }, []) 
  return (
    <div>
      {/* 這裡變一下 */}
      {
        props.staff && props.staff(data)
      }
    </div>
  )
}

以上就是對兩種框架“插槽”相關的實現方式的簡單總結。

總言之,不管vue或者react,都有著很靈活的用法,上面的這些要素和技巧,都可以在實際專案中可以根據需要自行組合或者擴充套件。

到此這篇關於vue和react中關於插槽詳解的文章就介紹到這了,更多相關vue react 插槽內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com