Vue?3開發(fā)者轉(zhuǎn)型React保姆級教程(含實際項目案例)
本文基于實際項目案例,對比 Vue 3 與 React 的核心差異,幫助 Vue 開發(fā)者快速上手 React。
前言
作為 Vue 3 開發(fā)者,當(dāng)我第一次接觸 React 時,最大的感受是:很多概念都有對應(yīng),但寫法和思維方式完全不同。
本文將結(jié)合一個倉庫瀏覽器頁面的實戰(zhàn)案例,講解從 Vue 3 到 React 需要掌握的核心概念。
項目背景

我們正在構(gòu)建一個倉庫瀏覽器頁面:
- 左側(cè):可折疊的文件樹
- 右側(cè):代碼高亮 / Markdown 預(yù)覽
先看一下兩個框架的代碼對比:
Vue 3 版本
<template>
<div class="file-tree">
<div
v-for="node in data"
:key="node.id"
@click="handleClick(node)"
>
{{ node.name }}
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
data: FileNode[]
selectedId: string | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
select: [node: FileNode]
toggle: [id: string]
}>()
const handleClick = (node: FileNode) => {
if (node.type === 'folder') {
emit('toggle', node.id)
} else {
emit('select', node)
}
}
</script>
React 版本
import React from 'react'
interface FileTreeProps {
data: FileNode[]
selectedId: string | null
onToggle: (id: string) => void
onSelect: (node: FileNode) => void
}
export const FileTree: React.FC<FileTreeProps> = ({
data,
selectedId,
onToggle,
onSelect,
}) => {
const handleClick = (node: FileNode) => {
if (node.type === 'folder') {
onToggle(node.id)
} else {
onSelect(node)
}
}
return (
<div className="file-tree">
{data.map((node) => (
<div
key={node.id}
onClick={() => handleClick(node)}
>
{node.name}
</div>
))}
</div>
)
}
核心差異:
| Vue 3 | React |
|---|---|
<template> | JSX |
defineProps | 函數(shù)參數(shù)解構(gòu) |
defineEmits | 函數(shù)參數(shù)(回調(diào)函數(shù)) |
v-for | Array.map() |
@click | onClick |
一、useCallback 是什么?
1.1 問題場景
在倉庫瀏覽器中,我們有一個 useFileTree Hook 管理狀態(tài):
// useFileTree.ts
export function useFileTree() {
const [selectedFile, setSelectedFile] = useState<FileNode | null>(null)
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())
// 這個函數(shù)每次渲染都會重新創(chuàng)建
const selectFile = (file: FileNode) => {
if (file.type === 'file') {
setSelectedFile(file)
}
}
const toggleFolder = (folderId: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev)
if (next.has(folderId)) {
next.delete(folderId)
} else {
next.add(folderId)
}
return next
})
}
return {
selectedFile,
expandedFolders,
selectFile, // 每次渲染都是新函數(shù)
toggleFolder, // 每次渲染都是新函數(shù)
}
}
問題: selectFile 和 toggleFolder 每次渲染都會生成新的函數(shù)引用,導(dǎo)致使用它們的子組件不必要地重新渲染。
1.2 Vue 3 的解決方案
Vue 3 中,你可能不需要特別處理這個問題,因為:
ref和reactive是響應(yīng)式的- 模板編譯器會自動優(yōu)化
但如果你確實需要緩存函數(shù),Vue 3 也提供了 computed 和 watch。
1.3 React 的解決方案:useCallback
import { useState, useCallback } from 'react'
export function useFileTree() {
const [selectedFile, setSelectedFile] = useState<FileNode | null>(null)
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())
// useCallback 緩存函數(shù)引用
const selectFile = useCallback((file: FileNode) => {
if (file.type === 'file') {
setSelectedFile(file)
}
}, []) // 依賴數(shù)組為空,函數(shù)只創(chuàng)建一次
const toggleFolder = useCallback((folderId: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev)
if (next.has(folderId)) {
next.delete(folderId)
} else {
next.add(folderId)
}
return next
})
}, []) // 同樣只創(chuàng)建一次
return {
selectedFile,
expandedFolders,
selectFile, // 引用穩(wěn)定,不會觸發(fā)子組件重渲染
toggleFolder, // 引用穩(wěn)定
}
}
1.4 useCallback 語法詳解
const memoizedFn = useCallback(
() => {
// 函數(shù)體
},
[dependency1, dependency2] // 依賴數(shù)組
)
對比 Vue 3:
| 場景 | Vue 3 | React |
|---|---|---|
| 緩存計算值 | computed | useMemo |
| 緩存函數(shù) | 通常不需要 | useCallback |
| 監(jiān)聽變化 | watch | useEffect |
1.5 什么時候用 useCallback?
推薦使用:
- 作為 props 傳遞給子組件的回調(diào)函數(shù)
- 作為
useEffect的依賴項 - 需要保持引用穩(wěn)定的函數(shù)
不需要使用:
- 僅在當(dāng)前組件使用的函數(shù)
- 事件處理函數(shù)(如
onClick)
二、組件傳參詳解
2.1 父傳子:Props
Vue 3 寫法:
<!-- 父組件 -->
<template>
<FileTree
:data="fileData"
:selected-id="selectedId"
/>
</template>
<script setup>
const fileData = ref([...])
const selectedId = ref('1')
</script>
<!-- 子組件 FileTree.vue -->
<script setup lang="ts">
interface Props {
data: FileNode[]
selectedId: string | null
}
const props = defineProps<Props>()
</script>
React 寫法:
// 父組件
import { FileTree } from './components/FileTree'
const App = () => {
const [fileData] = useState<FileNode[]>([...])
const [selectedId] = useState<string | null>('1')
return (
<FileTree
data={fileData}
selectedId={selectedId}
/>
)
}
// 子組件 FileTree.tsx
interface FileTreeProps {
data: FileNode[]
selectedId: string | null
}
export const FileTree: React.FC<FileTreeProps> = ({
data,
selectedId
}) => {
// 使用 data 和 selectedId
}
2.2 子傳父:回調(diào)函數(shù)
Vue 3 寫法:
<!-- 父組件 -->
<template>
<FileTreeNode
@select="handleSelect"
@toggle="handleToggle"
/>
</template>
<script setup>
const handleSelect = (node: FileNode) => {
console.log('選中了:', node)
}
const handleToggle = (id: string) => {
console.log('切換文件夾:', id)
}
</script>
<!-- 子組件 -->
<script setup>
const emit = defineEmits<{
select: [node: FileNode]
toggle: [id: string]
}>()
const handleClick = () => {
emit('select', someNode)
}
</script>
React 寫法:
// 父組件
const App = () => {
const handleSelect = (node: FileNode) => {
console.log('選中了:', node)
}
const handleToggle = (id: string) => {
console.log('切換文件夾:', id)
}
return (
<FileTreeNode
onSelect={handleSelect}
onToggle={handleToggle}
/>
)
}
// 子組件
interface FileTreeNodeProps {
onSelect: (node: FileNode) => void
onToggle: (id: string) => void
}
export const FileTreeNode: React.FC<FileTreeNodeProps> = ({
onSelect,
onToggle,
}) => {
const handleClick = () => {
onSelect(someNode) // 直接調(diào)用回調(diào)函數(shù)
}
}
2.3 復(fù)雜數(shù)據(jù)傳遞
在倉庫瀏覽器中,我們需要傳遞嵌套的文件樹數(shù)據(jù):
Vue 3:
<template>
<FileTreeNode
v-for="node in data"
:key="node.id"
:node="node"
:level="0"
:expanded-folders="expandedFolders"
:selected-file-id="selectedFileId"
@toggle="$emit('toggle', $event)"
@select="$emit('select', $event)"
/>
</template>React:
export const FileTree: React.FC<FileTreeProps> = ({
data,
expandedFolders,
selectedFileId,
onToggle,
onSelect,
}) => {
return (
<div>
{data.map((node) => (
<FileTreeNode
key={node.id}
node={node}
level={0}
expandedFolders={expandedFolders}
selectedFileId={selectedFileId}
onToggle={onToggle}
onSelect={onSelect}
/>
))}
</div>
)
}
2.4 傳遞方式對比總結(jié)
| 場景 | Vue 3 | React |
|---|---|---|
| 父傳子 | v-bind:prop 或 :prop | 直接作為 JSX 屬性 |
| 子傳父 | defineEmits + emit | 傳遞回調(diào)函數(shù)作為 props |
| 事件命名 | @event-name | onEventName (駝峰) |
| 插槽 | <slot> | children 或命名插槽 props |
| Provide/Inject | provide/inject | Context API |
三、其他 Vue 3 → React 核心概念映射
3.1 響應(yīng)式數(shù)據(jù)
// Vue 3
const count = ref(0)
const user = reactive({ name: '張三' })
// React
const [count, setCount] = useState(0)
const [user, setUser] = useState({ name: '張三' })
// 更新方式
count.value++ // Vue 3
setCount(count + 1) // React
user.name = '李四' // Vue 3 (reactive 自動響應(yīng))
setUser({ ...user, name: '李四' }) // React (需要創(chuàng)建新對象)
3.2 計算屬性 vs useMemo
// Vue 3 const doubleCount = computed(() => count.value * 2) // React const doubleCount = useMemo(() => count * 2, [count])
3.3 生命周期
// Vue 3
onMounted(() => {
// 組件掛載后
})
onUnmounted(() => {
// 組件卸載前
})
watch(selectedFile, (newVal) => {
// 監(jiān)聽變化
})
// React
useEffect(() => {
// 組件掛載后
return () => {
// 組件卸載前
}
}, []) // 空依賴數(shù)組 = 只執(zhí)行一次
useEffect(() => {
// selectedFile 變化時執(zhí)行
}, [selectedFile])
3.4 模板 vs JSX
<!-- Vue 3 模板 -->
<template>
<div v-if="isLoading">加載中...</div>
<div v-else-if="error">出錯了: {{ error }}</div>
<ul v-else>
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
// React JSX
return (
<div>
{isLoading ? (
<div>加載中...</div>
) : error ? (
<div>出錯了: {error}</div>
) : (
<ul>
{list.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
)
四、實戰(zhàn):倉庫瀏覽器核心代碼解析
4.1 類型定義 (types.ts)
// 文件節(jié)點接口 - 兩個框架基本一致
export interface FileNode {
id: string
name: string
type: 'file' | 'folder'
children?: FileNode[]
content?: string
}
4.2 自定義 Hook (useFileTree.ts)
import { useState, useCallback } from 'react'
export function useFileTree() {
// 狀態(tài)管理
const [selectedFile, setSelectedFile] = useState<FileNode | null>(null)
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())
// 使用 useCallback 緩存函數(shù)
const selectFile = useCallback((file: FileNode) => {
if (file.type === 'file') {
setSelectedFile(file)
}
}, [])
const toggleFolder = useCallback((folderId: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev)
if (next.has(folderId)) {
next.delete(folderId)
} else {
next.add(folderId)
}
return next
})
}, [])
return {
selectedFile,
expandedFolders,
selectFile,
toggleFolder,
}
}
對應(yīng) Vue 3 組合式函數(shù):
// useFileTree.ts (Vue 3 版本)
import { ref } from 'vue'
export function useFileTree() {
const selectedFile = ref<FileNode | null>(null)
const expandedFolders = ref(new Set<string>())
const selectFile = (file: FileNode) => {
if (file.type === 'file') {
selectedFile.value = file
}
}
const toggleFolder = (folderId: string) => {
if (expandedFolders.value.has(folderId)) {
expandedFolders.value.delete(folderId)
} else {
expandedFolders.value.add(folderId)
}
}
return {
selectedFile,
expandedFolders,
selectFile,
toggleFolder,
}
}
4.3 文件樹節(jié)點 (FileTreeNode.tsx)
import React from 'react'
import { Folder, File, ChevronRight, ChevronDown } from 'lucide-react'
interface FileTreeNodeProps {
node: FileNode
level: number
expandedFolders: Set<string>
selectedFileId: string | null
onToggle: (id: string) => void
onSelect: (node: FileNode) => void
}
export const FileTreeNode: React.FC<FileTreeNodeProps> = ({
node,
level,
expandedFolders,
selectedFileId,
onToggle,
onSelect,
}) => {
const isFolder = node.type === 'folder'
const isExpanded = expandedFolders.has(node.id)
const isSelected = selectedFileId === node.id
const handleClick = () => {
if (isFolder) {
onToggle(node.id)
} else {
onSelect(node)
}
}
return (
<div>
<div
className={`flex items-center gap-2 p-2 cursor-pointer ${
isSelected ? 'bg-blue-100' : 'hover:bg-gray-100'
}`}
style={{ paddingLeft: `${level * 16}px` }}
onClick={handleClick}
>
{isFolder ? (
<>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
<Folder size={16} className="text-yellow-500" />
</>
) : (
<>
<span className="w-4" />
<File size={16} className="text-gray-500" />
</>
)}
<span>{node.name}</span>
</div>
{/* 遞歸渲染子節(jié)點 */}
{isFolder && isExpanded && node.children && (
<div>
{node.children.map((child) => (
<FileTreeNode
key={child.id}
node={child}
level={level + 1}
expandedFolders={expandedFolders}
selectedFileId={selectedFileId}
onToggle={onToggle}
onSelect={onSelect}
/>
))}
</div>
)}
</div>
)
}
五、常見陷阱與最佳實踐
5.1 狀態(tài)不可變性
// ? 錯誤:直接修改
const toggleFolder = (id: string) => {
expandedFolders.add(id) // 不會觸發(fā)重渲染!
setExpandedFolders(expandedFolders)
}
// ? 正確:創(chuàng)建新對象
const toggleFolder = (id: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return next // 返回新 Set
})
}
5.2 依賴數(shù)組
// ? 錯誤:遺漏依賴
useEffect(() => {
console.log(selectedFile.name)
}, []) // selectedFile 變化時不會執(zhí)行
// ? 正確:包含所有依賴
useEffect(() => {
console.log(selectedFile.name)
}, [selectedFile])
// ? 如果依賴是對象的某個屬性
useEffect(() => {
console.log(selectedFile.name)
}, [selectedFile?.name])
5.3 Key 的重要性
// ? 錯誤:使用 index 作為 key
{list.map((item, index) => (
<div key={index}>{item.name}</div>
))}
// ? 正確:使用唯一標(biāo)識
{list.map((item) => (
<div key={item.id}>{item.name}</div>
))}
六、思維導(dǎo)圖:Vue 3 → React 概念映射
Vue 3 React
─────────────────────────────────────────────
<template> JSX
v-bind / :prop 直接作為屬性
v-on / @event onClick={handler}
v-model value + onChange
v-if / v-else 三元表達(dá)式
v-for Array.map()
ref() useState()
reactive() useState() + 對象
computed() useMemo()
watch() useEffect()
onMounted() useEffect(() => {}, [])
onUnmounted() useEffect 返回的清理函數(shù)
defineProps() 函數(shù)參數(shù)解構(gòu)
defineEmits() 傳遞回調(diào)函數(shù)作為 props
provide/inject Context API
<slot> children prop
七、總結(jié)
從 Vue 3 轉(zhuǎn)型 React,核心變化在于:
- 模板語法 → JSX:更靈活但需要手寫更多邏輯
- 響應(yīng)式系統(tǒng) → 狀態(tài)管理:
ref/reactive→useState - 事件系統(tǒng) → 回調(diào)函數(shù):
emit→ 傳遞函數(shù)作為 props - 生命周期 → useEffect:需要理解依賴數(shù)組的概念
- useCallback/useMemo:性能優(yōu)化的重要工具
學(xué)習(xí)建議:
- 先理解 React 的單向數(shù)據(jù)流
- 掌握 useState 和 useEffect
- 逐步學(xué)習(xí) useCallback、useMemo 等優(yōu)化 Hooks
- 多看優(yōu)秀開源項目的代碼
參考資料
到此這篇關(guān)于Vue 3開發(fā)者轉(zhuǎn)型React保姆級教程的文章就介紹到這了,更多相關(guān)Vue3開發(fā)者轉(zhuǎn)型React內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React.js入門實例教程之創(chuàng)建hello world 的5種方式
React 是近期非常熱門的一個前端開發(fā)框架。應(yīng)用非常廣泛,接下來通過本文給大家介紹React.js入門實例教程之創(chuàng)建hello world 的5種方式 ,需要的朋友參考下吧2016-05-05
react-beautiful-dnd 實現(xiàn)組件拖拽功能
這篇文章主要介紹了react-beautiful-dnd 實現(xiàn)組件拖拽功能,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08
React數(shù)據(jù)傳遞之組件內(nèi)部通信的方法
這篇文章主要介紹了React數(shù)據(jù)傳遞之組件內(nèi)部通信的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12
react+zarm實現(xiàn)底部導(dǎo)航欄的示例代碼
本文主要介紹了react?+?zarm?實現(xiàn)底部導(dǎo)航欄的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05
Webpack 4.x搭建react開發(fā)環(huán)境的方法步驟
這篇文章主要介紹了Webpack 4.x搭建react開發(fā)環(huán)境的方法步驟,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08

