1. 核心基础
vue 称之为渐进式的 JavaScript 框架,而 react 则是构建 Web 和原生交互见面的库,两者在使用感受上,vue 的写法整体比较容易理解,并且 vue 内部帮助开发者做了很多性能优化,而 react 则是趋向于函数式编程的思想,jsx 的写法更加灵活,但是开发者需要多关注组件的渲染性能,因为每一次数据更新可能会导致组件对应的函数执行,函数执行过程中就需要多关注哪些是否需要重新执行一遍。
1.1 视图更新
1.1.1 状态定义与修改
在 vue 中都是定义响应式数据,当响应式数据发生变化时,自动更新页面中相应的内容。
html
<script lang="ts" setup>
import { ref } from 'vue'
const num = ref(0)
const changeNum = () => {
num.value++
}
</script>
<template>
<div>
<div>当前数字: {{ num }}</div>
<button @click="changeNum">增加</button>
</div>
</template>
在 react 中这种响应式数据称之为状态,需要使用useState来定义一个状态,useState返回一个数组,数组第一项是一个变量,第二项为该变量的 setter 函数。
jsx
import { useState } from 'react'
export default function TestDemo() {
const [num, setNum] = useState(0)
const changeNum = () => {
setNum(num + 1)
}
return (
<div>
<div>当前数字:{num}</div>
<button onClick={changeNum}>增加</button>
</div>
)
}
当useState中定义对象和数组时,需要特别注意,官方文档原文:
state 中可以保存任意类型的 JavaScript 值,包括对象。但是,你不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。
数组是另外一种可以存储在 state 中的 JavaScript 对象,它虽然是可变的,但是却应该被视为不可变。同对象一样,当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。
具体示例:
jsx
import { useState } from 'react'
export default function TestDemo() {
const [userInfo, setUserInfo] = useState({
name: 'aaa',
info: {
age: 18,
},
})
const [books, setBooks] = useState([
{
id: 1,
name: 'book1',
},
{
id: 2,
name: 'book2',
},
])
const changeUserAge = () => {
// 生成一个新的对象,避免直接修改 state 中的对象
setUserInfo({
...userInfo,
info: {
...userInfo.info,
age: userInfo.info.age + 1,
},
})
}
const addBook = () => {
// 不能直接push,要生成一个新的数组
setBooks([
...books,
{
id: books.length + 1,
name: 'book' + (books.length + 1),
},
])
}
return (
<div>
<div>当前用户年龄:{userInfo.info.age}</div>
<button onClick={changeUserAge}>改变年龄</button>
<div>
{books.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
<button onClick={addBook}>添加一本书</button>
</div>
)
}
1.1.2 useImmer
当对象嵌套比较深后,上面这种写法就比较麻烦,官方文档中也提到了使用use-immer来进行优化,优化后的写法:
jsx
import { useState } from 'react'
import { useImmer } from 'use-immer'
export default function TestDemo() {
const [userInfo, setUserInfo] = useImmer({
name: 'aaa',
info: {
age: 18,
},
})
const [books, setBooks] = useImmer([
{
id: 1,
name: 'book1',
},
{
id: 2,
name: 'book2',
},
])
const changeUserAge = () => {
setUserInfo(draft => {
draft.info.age += 1
})
}
const addBook = () => {
setBooks(draft => {
draft.push({
id: books.length + 1,
name: 'book' + (books.length + 1),
})
})
}
return (
// ...
)
}
由 Immer 提供的
draft是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。这就是你能够随心所欲地直接修改对象的原因所在!从原理上说,Immer 会弄清楚draft对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象。
1.2 事件处理
1.2.1 事件监听
vue 中通过v-on(缩写@)来监听 DOM 事件
html
<button @click="changeNum">增加</button>
在 react 中没有指令的概念,与 html 原生绑定事件的方式相似,但是在 react 中需要使用小驼峰的形式。
jsx
<button onClick={addBook}>添加一本书</button>
注意:jsx 元素类似 HTML,但仍有一些区别,html 元素可通过转化器转化为 jsx 元素
1.2.2 默认事件处理
在 vue 中可以通过事件修饰符来阻止事件的默认事件
html
<button @click.prevent="changeNum">增加</button>
在 react 中则需要通过事件对象自行处理
jsx
function Demo() {
function changeNum(e: React.MouseEvent) {
e.preventDefault()
// ...
}
return <button onClick="changeNum">增加</button>
}
1.2.3 事件冒泡和捕获
vue 中阻止事件冒泡和定义捕获阶段直接使用事件修饰符
html
<button @click.stop="changeNum">增加</button>
<button @click.capture="changeNum">增加</button>
在 react 中阻止事件冒泡和捕获
jsx
function Test() {
function changeNum(e: React.MouseEvent) {
// 阻止事件冒泡
e.stopPropagation()
// ...
}
return <button onClick={changeNum}>添加一本书</button>
}
jsx
// 捕获阶段
<button onClickCapture={addBook}>添加一本书</button>
1.2.4 合成事件
React 根据浏览器的事件机制并基于自身的 Fiber Tree 自己也实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,在React中这套事件机制被称之为合成事件。

如何想要访问原生的事件对象,则需要通过e.nativeEvent进行访问。
合成事件和原生事件的执行顺序上:
原生事件 -> 合成事件 -> document 上挂在的事件
jsx
import { useEffect, useRef } from 'react'
export default function TestDemo() {
const buttonRef = useRef < HTMLButtonElement > null
const clickHandler = (e: React.MouseEvent) => {
console.log('合成 点击')
}
const nativeClickHandler = () => {
console.log('原生 点击')
}
useEffect(() => {
if (buttonRef.current) {
buttonRef.current.addEventListener('click', nativeClickHandler)
}
document.addEventListener('click', () => {
console.log('document 点击')
})
}, [])
return (
<div>
<button ref={buttonRef} onClick={clickHandler}>
click
</button>
</div>
)
}

1.3 生命周期
vue 中官方提供了生命周期钩子,在 react 函数组件中需要通过useEffect来模拟实现。
jsx
import { useEffect } from 'react'
export default function TestDemo() {
useEffect(() => {
console.log('组件挂载完成')
return () => {
console.log('组件卸载完成')
}
}, [])
return <div>TestDemo</div>
}
上面这段代码并不是从生命周期的角度去理解,应该从函数执行产生副作用的角度去理解。 TestDemo函数执行就相当于是组件进行渲染,函数执行过程中所有的副作用全部放到useEffect中,当TestDemo函数执行完后,useEffect中的函数再执行,所以可以模拟组件的挂载。
1.4 组件通信
1.4.1 基本的父子组件通信
在 vue 中通过defineProps和defineEmits来实现父子组件通信,在 react 中类似:
tsx
interface IProps {
name?: string
// 父组件将自己的某个方法传递给子组件,自组件可通过该方法间接操作父组件的数据
changeName: (name: string) => void
}
export default function TestComp(props: IProps) {
const { name, changeName } = props
return (
<div>
<button onClick={() => changeName('新的名字')}>{name}</button>
</div>
)
}
react 中父组件仍然是通过props将数据传递给子组件,但在 react 中没有定义事件的概念,而是状态提升,通过传递一个方法给子组件,实现子组件向父组件通信。
1.4.2 上下文
vue 中通过provide和inject来实现向后代所有组件传递数据,react 中则是通过createContext创建上下文来实现
ts
// context.ts
import { createContext } from 'react'
export const ThemeContext = createContext<string>('light')
tsx
// parent.tsx
import TestComp from '@/components/test-comp'
import { ThemeContext } from '@/context'
export default function TestDemo() {
return (
<ThemeContext.Provider value="dark">
<TestComp />
</ThemeContext.Provider>
)
}
tsx
// child.tsx
import { useContext } from 'react'
import { ThemeContext } from '@/context'
export default function TestComp() {
const theme = useContext(ThemeContext)
return <div>{theme}</div>
}
1.4.3 第三方工具
不论是在 vue 还是 react 中都可以通过使用第三方库来实现组件之间的通信。
Mitt - 轻量级的事件发布订阅库
tsx
import mitt from 'mitt'
const emitter = mitt()
// 订阅事件
emitter.on('event', (data) => {
console.log(data)
})
// 发布事件
emitter.emit('event', { message: 'hello' })
// 取消订阅
emitter.off('event')
2. 性能优化
2.1 memo
组件在props没有变化的情况下,跳过重新渲染
tsx
import { useState, memo } from 'react'
interface ChildProps {
name: string
}
const Child = memo((props: ChildProps) => {
console.log('Child rendered')
return <div>1</div>
})
export default function TestDemo() {
const [count, setCount] = useState<number>(0)
const [name, setName] = useState<string>('test')
console.log('TestDemo rendered')
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
{/* 只要name不变,Child组件就不会重新渲染 */}
<Child name={name} />
</div>
)
}
2.2 useMemo & useCallback
useMemo和useCallback经常一同出现,都是用作性能优化的手段。
useMemo缓存函数调用的结果。useCallback缓存函数本身。
这两个 hook 通常配合memo来优化子组件
tsx
import { useState, memo, useCallback, useMemo } from 'react'
interface ChildProps {
name: string
changeName?: (name: string) => void
}
const Child = memo((props: ChildProps) => {
console.log('Child rendered')
return <div>{props.name}</div>
})
export default function TestDemo() {
const [count, setCount] = useState<number>(0)
const [name, setName] = useState<string>('test')
// name改变fullName才重新计算得到新的值
const fullName = useMemo(() => {
return name + '_full'
}, [name])
// 依赖为空,每次渲染都不会再生成新的函数
const handleNameChange = useCallback((newName: string) => {
setName(newName)
}, [])
console.log('TestDemo rendered')
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
{/* 在fullName和handleNameChange不变的情况下,Child就不会重新渲染 */}
<Child name={fullName} changeName={handleNameChange} />
</div>
)
}
2.3 lazy & Suspense
使用 lazy 可以懒加载一个组件,可以在路由中使用
ts
const router = createBrowserRouter([
{
path: '/',
Component: BaseLayout,
children: [
{
path: 'about',
Component: lazy(() => import('@/views/about')),
},
],
},
])
组件懒加载的同时也可以配合Suspense组件和 react19 中新增的useAPI 在组件加载时展示后备方案
tsx
import { Suspense, lazy } from 'react'
// 懒加载组件
const LazyComponent = lazy(() => import('../lazy-demo/LazyComponent'))
export default function TestDemo() {
return (
<div>
<Suspense fallback={<div>正在加载组件...</div>}>
{/* 只有当LazyComponent被加载且组件内部异步数据请求完成后才会展示 */}
<LazyComponent />
</Suspense>
</div>
)
}
tsx
import { use } from 'react'
function fetchData() {
return new Promise<string>((resolve) => {
setTimeout(() => {
resolve('这是通过异步加载的数据!')
}, 2000)
})
}
const LazyComponent = () => {
// react19提供的use函数,可以读取promise的资源值
const data = use(fetchData())
return (
<div>
<h2>这是一个懒加载的组件</h2>
<p>{data}</p>
</div>
)
}
export default LazyComponent
2.4 合理的组件拆分
tsx
import { useState } from 'react'
interface ChildProps {
userInfo: any
}
function getFullName(name: string) {
for (let i = 0; i < 1000000000; i++) {}
return name + '!!!'
}
function Child(props: ChildProps) {
return (
<div>
<div>{getFullName(props.userInfo.name)}</div>
<div>{props.userInfo.age}</div>
</div>
)
}
export default function TestDemo() {
const [userInfo, setUserInfo] = useState({
name: '张三',
age: 18,
})
const ageAdd = () => {
setUserInfo({
...userInfo,
age: userInfo.age + 1,
})
}
return (
<div>
{/* 当点击按钮时,年龄加 1,此时子组件会重新渲染 */}
<button onClick={ageAdd}>++</button>
<Child userInfo={userInfo} />
</div>
)
}
针对涉及到长时间计算的地方单独提取为一个组件,避免在只改变 age 的情况下,也要重新调用getFullName函数
tsx
const FullName = memo((props: { name: string }) => {
return <div>{getFullName(props.name)}</div>
})
function Child(props: ChildProps) {
return (
<div>
<FullName name={props.userInfo.name} />
<div>{props.userInfo.age}</div>
</div>
)
}