前言
在這個卷神
輩出的時代,只是熟練Vue
的胖頭魚,已經被毒打過多次了,面試中曾被質疑:“你居然不會React?”我無語凝噎,不知說啥是好。
這篇文章嘗試將
Vue
中一些常見的功能在React
中實現一遍,如果你恰巧是Vue
轉React
,或者React
轉Vue
,期待對你有些幫助。如果你是一名熟悉
React
和Vue
的同學跪求輕噴(手動求生)
每個功能,都有對應的Vue和React版本實現,也有對應的截圖或者錄屏
1. v-if
我們先從最常見的顯示隱藏開始,Vue中處理一個元素的顯示隱藏一般會用v-if
或者v-show
指令,只不過v-if
是“真正”的條件渲染,切換過程中條件塊內的事件監聽器和子元件會適當地被銷燬和重建。而v-show
就簡單了,只是css樣式上的控制。
Vue
<template>
<div class="v-if">
<button @click="onToggleShow">切換</button>
<div v-if="isShow">前端胖頭魚 顯示出來啦</div>
</div>
</template>
<script>
export default {
name: 'vif',
data () {
return {
isShow: true
}
},
methods: {
onToggleShow () {
this.isShow = !this.isShow
}
}
}
</script>
React
import React, { useState } from "react"
export default function Vif (){
const [ isShow, setIsShow ] = useState(true)
const onToggleShow = () => {
setIsShow(!isShow)
}
return (
<div className="v-if">
<button onClick={ onToggleShow }>切換</button>
{/* 也可以用三目運算子 */}
{/* { isShow ? <div>前端胖頭魚 顯示出來啦</div> : null } */}
{
isShow && <div>前端胖頭魚 顯示出來啦</div>
}
</div>
)
}
預覽
2. v-show
同上,這次我們通過v-show
來實現顯示隱藏的功能,同時觀察DOM的樣式變化
注意: 這裡為啥顯示的時候不設定為block
是因為有些元素本身不是塊級元素,如果強行設定為block
有可能導致錯誤的樣式。
Vue
<template>
<div class="v-show">
<button @click="onToggleShow">切換</button>
<div v-show="isShow">前端胖頭魚 顯示出來啦</div>
</div>
</template>
<script>
export default {
name: 'vshow',
data () {
return {
isShow: true
}
},
methods: {
onToggleShow () {
this.isShow = !this.isShow
}
}
}
</script>
React
import React, { useState } from "react"
export default function VShow (){
const [ isShow, setIsShow ] = useState(true)
const onToggleShow = () => {
setIsShow(!isShow)
}
return (
<div className="v-show">
<button onClick={ onToggleShow }>切換</button>
{
<div style={{ display: isShow ? '' : 'none' }}>前端胖頭魚 顯示出來啦</div>
}
</div>
)
}
預覽
3. v-for
一般情況下,渲染一個列表在Vue
中使用v-for
指令,v-for 指令需要使用item in items
形式的特殊語法,其中items
是源資料陣列,而item
則是被迭代的陣列元素的別名。當然了,每個元素都需要設定唯一的key
Vue
<template>
<div class="v-for">
<div
class="v-for-item"
v-for="item in list"
:key="item.id"
>
{{ item.name }}
</div>
</div>
</template>
<script>
export default {
name: 'vfor',
data () {
return {
list: [
{
id: 1,
name: '前端',
},
{
id: 2,
name: '後端',
},
{
id: 3,
name: 'android',
},
{
id: 4,
name: 'ios',
},
]
}
}
}
</script>
React
在React
沒有v-for
指令,我們可以採用map
遍歷的方式實現類似功能
import React, { useState } from "react"
export default function VFor (){
const [ list, setList ] = useState([
{
id: 1,
name: '前端',
},
{
id: 2,
name: '後端',
},
{
id: 3,
name: 'android',
},
{
id: 4,
name: 'ios',
},
])
return (
<div className="v-for">
{
list.map((item) => {
return <div className="v-for-item" key={ item.id }>{ item.name }</div>
})
}
</div>
)
}
預覽
4. computed
當某個變數需要依賴其他變數求值時,使用計算屬性會非常方便,並且Vue
的計算屬性是基於它們的響應式依賴進行快取的,依賴值未發生變化,不會重新計算,達到快取的作用。
我們來看一個簡單的加法例子:num3
由num1
和num2
相加所得,同時按鈕每點一次num1
加10,num3
也會跟著不斷加10
Vue
<template>
<div class="computed">
<button @click="onAdd">+10</button>
<div>計算結果:{{ num3 }}</div>
</div>
</template>
<script>
export default {
name: 'computed',
data () {
return {
num1: 10,
num2: 10,
}
},
computed: {
num3 () {
return this.num1 + this.num2
}
},
methods: {
onAdd () {
this.num1 += 10
}
}
}
</script>
React
React
沒有計算屬性,但是我們可以通過useMemo
這個hook來實現,和Vue
computed不太一樣的地方在於,我們必須手動維護依賴
import React, { useMemo, useState } from "react"
export default function Computed (){
const [ num1, setNum1 ] = useState(10)
const [ num2, setNum2 ] = useState(10)
const num3 = useMemo((a, b) => {
return num1 + num2
}, [ num1, num2 ])
const onAdd = () => {
setNum1(num1 + 10)
}
return (
<div className="computed">
<button onClick={ onAdd }>+10</button>
<div>計算結果:{ num3 }</div>
</div>
)
}
預覽
5. watch
有時候我們需要監聽資料變化然後執行非同步行為或者開銷較大的操作時,在Vue中可以使用watch
來實現
我們來模擬一個這樣的場景並且通過watch
來實現:選擇boy
或者girl
,選中後傳送請求,顯示請求結果。(這裡通過setTimeout模擬非同步請求過程)
Vue
<template>
<div class="watch">
<div class="selects">
<button
v-for="(item, i) in selects"
:key="i"
@click="onSelect(item)"
>
{{ item }}
</button>
</div>
<div class="result">
{{ result }}
</div>
</div>
</template>
<script>
export default {
name: 'watch',
data () {
return {
fetching: false,
selects: [
'boy',
'girl'
],
selectValue: ''
}
},
computed: {
result () {
return this.fetching ? '請求中' : `請求結果: 選中${this.selectValue || '~'}`
}
},
watch: {
selectValue () {
this.fetch()
}
},
methods: {
onSelect (value) {
this.selectValue = value
},
fetch () {
if (!this.fetching) {
this.fetching = true
setTimeout(() => {
this.fetching = false
}, 1000)
}
}
}
}
</script>
React
React
中要實現監聽某些資料的變化執行響應的動作,可以使用useEffect
import React, { useState, useMemo, useEffect } from "react"
import './watch.css'
export default function Watch() {
const [fetching, setFetching] = useState(false)
const [selects, setSelects] = useState([
'boy',
'girl'
])
const [selectValue, setSelectValue] = useState('')
const result = useMemo(() => {
return fetching ? '請求中' : `請求結果: 選中${selectValue || '~'}`
}, [ fetching ])
const onSelect = (value) => {
setSelectValue(value)
}
const fetch = () => {
if (!fetching) {
setFetching(true)
setTimeout(() => {
setFetching(false)
}, 1000)
}
}
useEffect(() => {
fetch()
}, [ selectValue ])
return (
<div className="watch">
<div className="selects">
{
selects.map((item, i) => {
return <button key={ i } onClick={ () => onSelect(item) }>{ item }</button>
})
}
</div>
<div className="result">
{ result }
</div>
</div>
)
}
預覽
6. style
有時候難免要給元素動態新增樣式style
,Vue
和React
都給我們提供了方便的使用方式。
在使用上基本大同小異:
相同點:
CSS property 名可以用駝峰式 (camelCase) 或短橫線分隔 (kebab-case,記得用引號括起來) 來命名
不同點:
- Vue可以通過陣列語法繫結多個樣式物件,React主要是單個物件的形式(這點Vue也可以)
- React 會自動新增 ”px”(這點Vue不會自動處理) 字尾到內聯樣式為數字的屬性,其他單位手動需要手動指定
- React樣式不會自動補齊字首。如需支援舊版瀏覽器,需手動補充對應的樣式屬性。Vue中當 v-bind:style 使用需要新增瀏覽器引擎字首的 CSS property 時,如 transform,Vue.js 會自動偵測並新增相應的字首。
Vue
<template>
<div class="style" :style="[ style, style2 ]"></div>
</template>
<script>
export default {
name: 'style',
data () {
return {
style: {
width: '100%',
height: '500px',
},
style2: {
backgroundImage: 'linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)',
borderRadius: '10px',
}
}
}
}
</script>
React
import React from "react"
export default function Style (){
const style = {
width: '100%',
height: '500px',
}
const style2 = {
backgroundImage: 'linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)',
borderRadius: '10px',
}
return (
<div className="style" style={ { ...style, ...style2 } } ></div>
)
}
預覽
7. class
如何動態地給元素新增class? Vue中我自己比較喜歡用陣列的語法(當然還有物件的寫法),React中也可以使用一些第三方包如classnames起到更加便捷新增class的效果。
下面我們看下不借助任何庫,如何實現按鈕選中的效果
Vue
<template>
<button :class="buttonClasses" @click="onClickActive">{{ buttonText }}</button>
</template>
<script>
export default {
name: 'class',
data () {
return {
isActive: false,
}
},
computed: {
buttonText () {
return this.isActive ? '已選中' : '未選中'
},
buttonClasses () {
// 通過陣列形式維護class動態列表
return [ 'button', this.isActive ? 'active' : '' ]
}
},
methods: {
onClickActive () {
this.isActive = !this.isActive
}
}
}
</script>
<style scoped>
.button{
display: block;
width: 100px;
height: 30px;
line-height: 30px;
border-radius: 6px;
margin: 0 auto;
padding: 0;
border: none;
text-align: center;
background-color: #efefef;
}
.active{
background-image: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
color: #fff
}
</style>
React
import React, { useMemo, useState } from "react"
import './class.css' // 此處樣式與上面是一樣的
export default function Class (){
const [ isActive, setIsActive ] = useState(false)
const buttonText = useMemo(() => {
return isActive ? '已選中' : '未選中'
}, [ isActive ])
const buttonClass = useMemo(() => {
// 和Vue中不太一樣的是我們需要手動join一下,變成'button active'形式
return [ 'button', isActive ? 'active' : '' ].join(' ')
}, [ isActive ])
const onClickActive = () => {
setIsActive(!isActive)
}
return (
<div className={ buttonClass } onClick={onClickActive}>{ buttonText }</div>
)
}
預覽
8.provide/inject
Vue和React中對於全域性狀態的管理都有各自好的解決方案,比如Vue中的Vuex
,React中的redux
和Mobx
,當然小型專案中引入這些有點大材小用了,有沒有其他解決方案呢?
Vue中可以使用provide/inject
React中則可以使用Context
假設全域性有有一個使用者資訊userInfo的變數,需要在各個元件中都能便捷的訪問到,在Vue和React中該如何實現呢?
Vue
Vue
中借用provide/inject
可以將頂層狀態,傳遞至任意子節點,假設我們再app.vue中宣告瞭一個userInfo
資料
app.vue
<template>
<div id="app">
<div class="title">我是Vue栗子</div>
<router-view/>
</div>
</template>
<script>
export default {
name: 'app',
// 宣告資料
provide () {
return {
userInfo: {
name: '前端胖頭魚'
}
}
}
}
</script>
provide.vue
<template>
<div class="provide-inject">{{ userInfo.name }}</div>
</template>
<script>
export default {
name: 'provideInject',
// 使用資料
inject: [ 'userInfo' ]
}
</script>
React
React
中要實現類似的功能,可以藉助Context,將全域性狀態共享給任意子節點
context/index.js
import { createContext } from "react";
export const UserInfoContext = createContext({
userInfo: {
name: ''
}
})
app.js
import { UserInfoContext } from './context/index'
function App() {
return (
<BrowserRouter>
// 注意這裡
<UserInfoContext.Provider
value={{ userInfo: { name: '前端胖頭魚' } }}
>
<div className="title">我是React栗子</div>
<Routes>
<Route path="/v-if" element={<Vif />} />
<Route path="/v-show" element={<VShow />} />
<Route path="/v-for" element={<VFor />} />
<Route path="/computed" element={<Computed />} />
<Route path="/watch" element={<Watch />} />
<Route path="/style" element={<Style />} />
<Route path="/class" element={<Class />} />
<Route path="/slot" element={<Slot />} />
<Route path="/nameSlot" element={<NameSlot />} />
<Route path="/scopeSlot" element={<ScopeSlot />} />
<Route path="/provide" element={<Provide />} />
</Routes>
</UserInfoContext.Provider>
</BrowserRouter>
);
}
provide.js
import React, { useContext } from "react"
import { UserInfoContext } from '../context/index'
export default function Provide() {
// 通過userContext,使用定義好的UserInfoContext
const { userInfo } = useContext(UserInfoContext)
return (
<div class="provide-inject">{ userInfo.name }</div>
)
}
預覽
9. slot(預設插槽)
插槽是Vue
中非常實用的功能,我把他理解成”坑位“,等待著你從外面把他填上,而這個”坑位“可以分成預設坑位
、具名坑位
、作用域坑位
,我們們通過一個實戰例子來看看React
中如何實現同等的功能。
假設我們要實現一個簡單的dialog
元件,基本功能是標題可以傳字串,內容部分可以完全自定義,應該怎麼實現呢?
Vue
dialog
<template>
<div class="dialog" v-show="visible">
<div class="dialog-mask" @click="onHide"></div>
<div class="dialog-body">
<div class="dialog-title" v-if="title">{{ title }}</div>
<div class="dialog-main">
// 注意這裡放了一個預設插槽坑位
<slot></slot>
</div>
<div class="dialog-footer">
<div class="button-cancel" @click="onHide">取消</div>
<div class="button-confirm" @click="onHide">確定</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "dialog",
props: {
title: {
type: String,
default: "",
},
visible: {
type: Boolean,
default: false,
},
},
methods: {
onHide () {
this.$emit('update:visible', false)
}
}
};
</script>
slot
<template>
<div class="slot">
<button @click="onToggleVisible">切換dialog</button>
<Dialog
:visible.sync="visible"
title="預設插槽"
>
// 這裡會替換到<slot></slot>的位置處
<div class="slot-body">前端胖頭魚</div>
</Dialog>
</div>
</template>
<script>
import Dialog from './components/dialog.vue'
export default {
name: 'slot',
components: {
Dialog,
},
data () {
return {
visible: false
}
},
methods: {
onToggleVisible () {
this.visible = !this.visible
}
}
}
React
要在React中同樣實現上面的功能應該怎麼辦呢?React
可沒有啥插槽啊!別急,雖然React
中沒有插槽的概念,但是卻可以通過props.children
獲取到元件內部的子元素,通過這個就可以實現預設插槽的功能
Dialog
import React, { useState, useEffect } from "react"
import './dialog.css'
export default function Dialog(props) {
// 原諒我用visible -1這種傻叉的方式先實現了, 重點不是在這裡
const { children, title = '', visible = -1 } = props
const [visibleInner, setVisibleInner] = useState(false)
const onHide = () => {
setVisibleInner(false)
}
useEffect(() => {
setVisibleInner(visible > 0)
}, [ visible ])
return (
<div className="dialog" style={ { display: visibleInner ? 'block' : 'none' }}>
<div className="dialog-mask" onClick={ onHide }></div>
<div className="dialog-body">
{ title ? <div className="dialog-title">{ title }</div> : null }
<div className="dialog-main">
{/* 注意這裡,通過children實現預設插槽功能 */}
{children}
</div>
<div className="dialog-footer">
<div className="button-cancel" onClick={ onHide }>取消</div>
<div className="button-confirm" onClick={ onHide }>確定</div>
</div >
</div >
</div >
)
}
slot
import React, { useState, useEffect } from "react"
import Dialog from './components/dialog'
export default function Slot() {
const [visible, setVisible] = useState(-1)
const onToggleVisible = () => {
setVisible(Math.random())
}
return (
<div className="slot">
<button onClick={ onToggleVisible }>切換dialog</button>
<Dialog
visible={visible}
title="預設插槽"
>
{/* 注意這裡,會被Dialog元件的children讀取並且替換掉 */}
<div className="slot-body">前端胖頭魚</div>
</Dialog>
</div>
)
}
預覽
10. name slot(具名插槽)
當元件內部有多個動態內容需要外部來填充的時候,一個預設插槽已經不夠用了,我們需要給插槽取個名字,這樣外部才可以”按部就班“到指定位置。
我們來豐富一下Dialog
元件,假設title
也可以支援動態傳遞內容呢?
Vue
Vue中通過<slot name="main"></slot>
形式先進行插槽的宣告,再通過v-slot:main
形式進行使用,一個蘿蔔一個坑也就填起來了
Dialog改造
<template>
<div class="dialog" v-show="visible">
<div class="dialog-mask" @click="onHide"></div>
<div class="dialog-body">
<div class="dialog-title" v-if="title">{{ title }}</div>
<!-- 注意這裡,沒有傳title屬性,時候通過插槽進行內容承接 -->
<slot name="title" v-else></slot>
<div class="dialog-main">
<!-- 宣告main部分 -->
<slot name="main"></slot>
</div>
<div class="dialog-footer">
<div class="button-cancel" @click="onHide">取消</div>
<div class="button-confirm" @click="onHide">確定</div>
</div>
</div>
</div>
</template>
// ... 其他地方和上面試一樣的
nameSlot
<template>
<div class="slot">
<button @click="onToggleVisible">切換dialog</button>
<Dialog
:visible.sync="visible"
>
<template v-slot:title>
<div class="dialog-title">具名插槽</div>
</template>
<template v-slot:main>
<div class="slot-body">前端胖頭魚</div>
</template>
</Dialog>
</div>
</template>
<script>
import Dialog from './components/dialog.vue'
export default {
name: 'nameSlot',
components: {
Dialog,
},
data () {
return {
visible: false
}
},
methods: {
onToggleVisible () {
this.visible = !this.visible
}
}
}
</script>
React
前面通過props.children
屬性可以讀取元件標籤內的內容算是和Vue
預設插槽實現了一樣的功能,但是具名插槽如何實現呢?React
好玩的其中一個點,我覺得是屬性啥玩意都可以傳、字串
、數字
、函式
、連DOM
也可以傳。所以實現具名插槽也很簡單,直接當屬性傳遞就可以
Dialog改造
import React, { useState, useEffect } from "react"
import './dialog.css'
export default function Dialog(props) {
// 原諒我用visible -1這種傻叉的方式先實現了, 重點不是在這裡
const { title, main, visible = -1 } = props
const [visibleInner, setVisibleInner] = useState(false)
const onHide = () => {
setVisibleInner(false)
}
useEffect(() => {
setVisibleInner(visible > 0)
}, [ visible ])
return (
<div className="dialog" style={ { display: visibleInner ? 'block' : 'none' }}>
<div className="dialog-mask" onClick={ onHide }></div>
<div className="dialog-body">
{/* { title ? <div className="dialog-title">{ title }</div> : null } */}
{/* 注意這裡,直接渲染title就可以了 */}
{ title }
<div className="dialog-main">
{/* 注意這裡,通過children實現預設插槽功能 */}
{/* {children} */}
{/* 這一這裡不是children了,是main */}
{ main }
</div>
<div className="dialog-footer">
<div className="button-cancel" onClick={ onHide }>取消</div>
<div className="button-confirm" onClick={ onHide }>確定</div>
</div >
</div >
</div >
)
}
nameSlot
import React, { useState } from "react"
import Dialog from './components/dialog'
import './slot.css'
export default function NameSlot() {
const [visible, setVisible] = useState(-1)
const onToggleVisible = () => {
setVisible(Math.random())
}
return (
<div className="slot">
<button onClick={ onToggleVisible }>切換dialog</button>
<Dialog
visible={visible}
// 注意這裡,直接傳遞的DOM
title={ <div className="dialog-title">預設插槽</div> }
// 注意這裡,直接傳遞的DOM
main={ <div className="slot-body">前端胖頭魚</div> }
>
</Dialog>
</div>
)
}
預覽
可以看到具名插槽,React
直接用屬性反而更簡潔一些
11. scope slot(作用域插槽)
有了預設插槽
、具名插槽
最後當然少不了作用域插槽啦!有時讓插槽內容能夠訪問子元件中才有的資料是很有用的,這也是作用域插槽的意義所在
假設:Dialog
元件內部有一個userInfo: { name: '前端胖頭魚' }
資料物件,希望使用Dialog
元件的外部插槽也能訪問到,該怎麼做呢?
Vue
Dialog
<template>
<div class="dialog" v-show="visible">
<div class="dialog-mask" @click="onHide"></div>
<div class="dialog-body">
<div class="dialog-title" v-if="title">{{ title }}</div>
<!-- 注意這裡,通過繫結userInfo外部可以進行使用 -->
<slot name="title" :userInfo="userInfo" v-else></slot>
<div class="dialog-main">
<!-- 注意這裡,通過繫結userInfo外部可以進行使用 -->
<slot name="main" :userInfo="userInfo"></slot>
</div>
<div class="dialog-footer">
<div class="button-cancel" @click="onHide">取消</div>
<div class="button-confirm" @click="onHide">確定</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "dialog",
// ...
data () {
return {
userInfo: {
name: '前端胖頭魚'
}
}
},
// ...
};
</script>
scopeSlot
<template>
<div class="slot">
<button @click="onToggleVisible">切換dialog</button>
<Dialog
:visible.sync="visible"
>
<template v-slot:title>
<div class="dialog-title">作用域插槽</div>
</template>
<!-- 注意這裡 -->
<template v-slot:main="{ userInfo }">
<!-- 注意這裡userInfo是Dialog元件內部的資料 -->
<div class="slot-body">你好{{ userInfo.name }}</div>
</template>
</Dialog>
</div>
</template>
React
還是那句話,React中萬物皆可傳,類似實現具名插槽中我們直接傳遞DOM,同樣我們也可以傳遞函式,將Dialog
元件內部的userInfo
資料通過函式傳參的方式給到外部使用
scopeSlot原始碼點這裡
Dialog改造
import React, { useState, useEffect } from "react"
import './dialog.css'
export default function Dialog(props) {
// 原諒我用visible -1這種傻叉的方式先實現了, 重點不是在這裡
const { title, main, visible = -1 } = props
const [visibleInner, setVisibleInner] = useState(false)
const [ userInfo ] = useState({
name: '前端胖頭魚'
})
const onHide = () => {
setVisibleInner(false)
}
useEffect(() => {
setVisibleInner(visible > 0)
}, [ visible ])
return (
<div className="dialog" style={ { display: visibleInner ? 'block' : 'none' }}>
<div className="dialog-mask" onClick={ onHide }></div>
<div className="dialog-body">
{/* 作用域插槽,當函式使用,並且把資料傳遞進去 */}
{ title(userInfo) }
<div className="dialog-main">
{/* 作用域插槽,當函式使用,並且把資料傳遞進去 */}
{ main(userInfo) }
</div>
<div className="dialog-footer">
<div className="button-cancel" onClick={ onHide }>取消</div>
<div className="button-confirm" onClick={ onHide }>確定</div>
</div >
</div >
</div >
)
}
scopeSlot
import React, { useState } from "react"
import Dialog from './components/dialog'
import './slot.css'
export default function ScopeSlot() {
const [visible, setVisible] = useState(-1)
const onToggleVisible = () => {
setVisible(Math.random())
}
return (
<div className="slot">
<button onClick={ onToggleVisible }>切換dialog</button>
<Dialog
visible={visible}
// 通過函式來實現插槽
title={ () => <div className="dialog-title">作用域插槽</div> }
// 接收userInfo資料
main={ (userInfo) => <div className="slot-body">你好{ userInfo.name }</div> }
>
</Dialog>
</div>
)
}