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 LazyComponent2.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>
  )
}