Vue过渡至React的基础理解

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 中通过definePropsdefineEmits来实现父子组件通信,在 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 中通过provideinject来实现向后代所有组件传递数据,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

useMemouseCallback经常一同出现,都是用作性能优化的手段。

  • 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>
  )
}
相关推荐
爱抽烟的大liu6 小时前
iOS 进阶6-Voip通信
前端
Zyx20076 小时前
用 CSS 演绎浪漫:从零构建“亲吻动画”全流程解析
前端·css
llq_3506 小时前
在 antd Table 中实现多行省略和 Tooltip
前端
优秀员工不受影响6 小时前
如何使用 Volta
前端
my一阁6 小时前
tomcat web实测
java·前端·nginx·tomcat·负载均衡
huangql5206 小时前
前端多版本零404部署实践:为什么会404,以及怎么彻底解决
前端
梵得儿SHI6 小时前
Vue 数据绑定深入浅出:从 v-bind 到 v-model 的实战指南
前端·javascript·vue.js·双向绑定·vue 数据绑定机制·单向绑定·v-bind v-model
Moment6 小时前
Electron 发布 39 版本 ,这更新速度也变态了吧❓︎❓︎❓︎
前端·javascript·node.js