react基础教程
多一句没有,少一句不行,用更短时间,教会更实用的技术!
https://zh-hans.react.dev/learn/start-a-new-react-project (opens new window)
# 环境搭建
create-react-app
npx create-react-app my-app
cd my-app
npm start
# JSX
JSX 是 JavaScript 的语法扩展。React 使用 JSX 来替代常规的 JavaScript。浏览器不能直接解析JSX,需要使用babel进行编译
# 在 jsx 中可以通过 {} 来使用 js 表达式,但是不能使用 if else 语句,可以使用三元运算符
if语句、switch语句、变量声明属于语句,不是表达式,不能出现在{}中
# jsx 中可以使用map实现列表渲染
const list = [1, 2, 3, 4, 5];
const listItems = list.map((item) => <li>{item}</li>);
# jsx 中实现条件渲染
在React中,可以通过逻辑与运算符&&、三元表达式(?:)实现基础的条件渲染
const isLogin = true;
const element = isLogin ? <div>登录成功</div> : <div>登录失败</div>;
# jsx 实现复杂条件渲染
列表中需要根据文章状态适配三种情况,单图,三图,和无图三种模式
自定义函数 + if判断语句
function getArticleType(article) {
if (article.images.length === 1) {
return <div>单图</div>;
} else if (article.images.length === 3) {
return <div>三图</div>;
} else {
return <div>无图</div>;
}
}
# 事件绑定
on + 事件名称 = { 事件处理程序 },整体上遵循驼峰命名法
# 使用时间对象参数
在事件回调函数中设置形参e
function App() {
function handleClick(e) {
console.log(e);
}
return (
<div>
<button onClick={handleClick}>点我</button>
</div>
);
}
传递自定义参数
事件绑定的位置改造成箭头函数的写法,在执行clickHandler实际处理业务函数的时候传递实参
function App() {
function handleClick(name) {
console.log(name);
}
return (
<div>
<button onClick={() => handleClick("wu")}>点我</button>
</div>
);
}
不能直接写函数调用,这里事件绑定需要一个函数引用,而不是函数调用,所以需要使用箭头函数
# 同时传递事件对象和自定义参数
function App() {
function handleClick(e, name) {
console.log(e, name);
}
return (
<div>
<button onClick={(e) => handleClick(e, "wu")}>点我</button>
</div>
);
}
# useState
useState 是一个 React Hook(函数),它允许我们向组件添加一个状态变量, 从而控制影响组件的渲染结果.
本质:和普通JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图)
const [count, setCount] = useState(0); // count 是状态变量,setCount 是修改状态变量的方法
- useState是一个函数,返回值是一个数组
- 数组中的第一个参数是状态变量,第二个参数是set函数用来修改状态变量
- useState的参数是状态变量的初始值
# 状态不可变
在React中,状态被认为是只读的,我们应该始终替换它而不是修改它,直接修改状态不能引发视图更新
const [count, setCount] = useState(0);
const handleClick = () => {
count = count + 1; // 错误写法
setCount(count + 1); // 正确写法
};
# 修改对象状态
const [person, setPerson] = useState({ name: "wu", age: 18 });
const handleClick = () => {
person.age = "19"; // 错误写法
setPerson({ ...person, age: 19 }); // 正确写法
};
# 组件的样式处理
React组件基础的样式控制有俩种方式
# 内联样式
<div style={{ color: "red" }}>hello world</div>
# class样式
<div className="red">hello world</div>
# 组件通讯
# 父子
props 可传递任意的数据
数字、字符串、布尔值、数组、对象、函数、组件(jsx)
<Child name="wu" age={18} />
props 是只读的,不能直接修改
props.name = "wu2"; // 错误写法
特殊的父传子
当我们把内容嵌套在子组件中时,子组件的props中会自动接收到父组件传递过来的内容,放到props.children中
<Child>
<div>hello world</div>
</Child>
console.log(props.children); // 打印 hello world
# 子父
子组件通过调用父组件传递过来的方法,来修改父组件中的状态变量
function Parent() {
const [count, setCount] = useState(0);
return <Child setCount={setCount} count={count} />;
}
function Child(props) {
return <button onClick={() => props.setCount(props.count + 1)}>点我</button>
}
# 兄弟
兄弟组件之间不能直接通讯,需要通过共同的父组件来实现通讯
使用状态提升实现兄弟组件通讯
- A组件先通过子传父的方式把数据传给父组件App
- App拿到数据后通过父传子的方式再传递给B组件
使用context机制实现跨层级组件通讯
- 使用createContext方法创建一个上下文对象Ctx
- 在顶层组件(App)中通过 Ctx.Provider 组件提供数据
- 在后代组件中通过 Ctx.Consumer 组件消费数据
# useEffect
useEffect 是一个 React Hook(函数),用于在React组件中创建不是由事件引起而是由渲染本身引起的操作(副作用),比如发送AJAX请求,更改DOM等等。
上面的组件中没有发生任何的用户事件,组件渲染完毕之后就需要和服务器要数据,整个过程属于“只由渲染引起的操作”,所以需要使用useEffect来实现
# 基础使用
在组件渲染完毕之后,立刻从服务端获取频道列表数据并显示到页面中
useEffect(() => {
axios.get("https://api.shenzjd.com").then((res) => {
console.log(res);
});
}, []);
参数1是一个函数,可以把它叫做副作用函数,在函数内部可以放置要执行的操作
参数2是一个数组(可选参),在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行,当是一个空数组的时候,副作用函数只会在组件渲染完毕之后执行一次
# useEffect 依赖项参数说明
依赖项 | 副作用函数执行情况 |
---|---|
没有依赖项 | 组件初始渲染 + 组件更新时执行 |
空数组 | 只在初始渲染时执行一次 |
添加特定依赖项 | 组件初始渲染 + 特性依赖项变化时执行 |
# 清除副作用
在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用
useEffect(() => {
const timer = setInterval(() => {
console.log("hello world");
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
说明:清除副作用的函数最常见的执行时机是在组件卸载时自动执行
# 自定义hook
自定义Hook是以 use 打头的函数,通过自定义Hook函数可以用来实现逻辑的封装和复用
# React Hooks 使用规则
- 只能在组件中或者其他自定义Hook函数中调用
- 只能在组件的顶层调用,不能嵌套在 if、for、其他函数中
# redux 快速上手
Redux 是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行作用:通过集中管理的方式管理应用的状态
为了职责清晰,数据流向明确,Redux把整个数据修改的流程分成了三个核心概念,分别是:state、action和reducer
- state - 一个对象 存放着我们管理的数据状态
- action - 一个对象 用来描述你想怎么改数据
- reducer - 一个函数 更具action的描述生成一个新的state
# Redux 基本使用
https://cn.redux.js.org/ (opens new window)
https://redux-toolkit.js.org/ (opens new window)
# redux-toolkit
Redux Toolkit(RTK)- 官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式
pnpm install @reduxjs/toolkit
- 简化store的配置方式
- 内置immer支持可变式状态修改
- 内置thunk更好的异步创建
# react-redux
react-redux - 用来 链接 Redux 和 React组件 的中间件
pnpm install react-redux
# 配置基础环境
- 使用 CRA 生成项目
npx create-react-app my-app
// 或者
pnpm dlx create-react-app my-app
- 安装@reduxjs/toolkit和react-redux
pnpm install react-redux @reduxjs/toolkit
- 启动项目
pnpm run start
# store 目录设计
- 通常集中状态管理的部分都会单独创建一个单独的
store
目录 - 应用通常会有很多个子store模块,所以创建一个
modules
目录,在内部编写业务分类的子store - store中的入口文件 index.js 的作用是组合modules中所有的子模块,并导出store
# 使用redux-toolkit创建counterStore
// counterStore.js
import { createSlice } from '@reduxjs/toolkit'
const counterStore = createSlice({
name: 'counter',
// 初始状态数据
initialState: {
count: 0
},
// 修改数据的方法 同步方法 支持直接修改
reducers: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
}
})
// 解构出来 actionCreater函数
const { increment, decrement } = counterStore.actions
// 获取reducer
const counterReducer = counterStore.reducer
// 导出
export { increment, decrement }
export default counterReducer
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './modules/counterStore'
const store = configureStore({
reducer: {
counter: counterReducer
}
})
# 为react注入store
react-redux 负责把Redux和React 链接 起来,内置 Provider组件 通过 store 参数把创建好的store实例注入到应用中,链接正式建立
import { Provider } from 'react-redux'
import { store } from './store'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
# react组件使用store中的数据
在React组件中使用store中的数据,需要用到一个 钩子函数 - useSelector,它的作用是把store中的数据映射到组件中
import { useSelector } from 'react-redux'
const count = useSelector(state => state.counter)
# react组件修改store中的数据
React组件中修改store中的数据需要借助另外一个hook函数 - useDispatch,它的作用是生成提交action对象的dispatch函数
import { useDispatch, useSelector } from 'react-redux'
import { increment, decrement } from './store/modules/counterStore'
function App() {
const count = useSelector(state => state.counter)
const dispatch = useDispatch()
return (
<div className='App'>
<button onClick={() => dispatch(increment())}>+</button>
<span>{count.count}</span>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
)
}
# 异步状态操作
- 创建store的写法保持不变,配置好同步修改状态的方法
- 单独封装一个函数,在函数内部return一个新函数,在新函数中 2.1 封装异步请求获取数据 2.2 调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
- 组件中dispatch的写法保持不变
const channelStore = createSlice({
name: 'channel',
initialState: {
channelList: []
},
reducers: {
getChannelList(state, action) {
state.channelList = action.payload
}
}
})
const { setChannelList } = channelStore.actions
const fetchChannelList = () => {
return async dispatch => {
const res = await axios.get('https://api.shenzjd.com')
dispatch(setChannelList(res.data.data.channels))
}
}
export { fetchChannelList }
import { useDispatch } from 'react-redux'
import { fetchChannelList } from './store/modules/channelStore'
function App() {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchChannelList())
}, [dispatch])
}
# react-router
pnpm install react-router-dom
# 路由导航
路由系统中的多个路由之间需要进行路由跳转,并且在跳转的同时有可能需要传递参数进行通信
声明式导航
<Link to="/">首页</Link>
编程式导航
import { useNavigate } from 'react-router-dom'
const Login = () => {
const navigate = useNavigate()
return (
<div onClick={() => navigate('/')}>登录</div>
)
}
# 路由传参
searchParams
navigate('/login?name=wu&age=18')
import { useSearchParams } from 'react-router-dom'
const [searchParams, setSearchParams] = useSearchParams()
console.log(searchParams.get('name'))
params
navigate('/login/wu/18')
import { useParams } from 'react-router-dom'
const params = useParams()
console.log(params.name)
# 嵌套路由配置
在一级路由中又内嵌了其他路由,这种关系就叫做嵌套路由,嵌套至一级路由内的路由又称作二级路由
- 使用 children 属性配置路由嵌套关系
- 使用
<Outlet/>
组件配置二级路由渲染位置
const routes = [
{
path: '/',
element: <Layout />,
children: [
{ path: 'login', element: <Login /> },
{ path: 'register', element: <Register /> }
]
}
]
const Layout = () => {
return (
<div>
<h1>Layout</h1>
<div>
<Link to="/login">登录</Link>
<Link to="/register">注册</Link>
</div>
<Outlet />
</div>
)
}
# 默认二级路由
当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置index属性为true
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'login', element: <Login /> },
{ path: 'register', element: <Register /> }
]
}
# 404 路由配置
当访问的路由在配置中不存在时,会自动匹配到404路由,并渲染404组件
{
path: '*',
element: <NotFound />
}
# 两种路由模式
history模式和hash模式, ReactRouter分别由createBrowerRouter和createHashRouter 函数负责创建
模式 | url实现 | 底层原理 | 是否需要后端支持 |
---|---|---|---|
history | url/login | history对象 + pushState事件 | 需要后端支持 |
hash | url#login | 监听 hashchange 事件 | 不需要后端支持 |
# 路由懒加载
路由懒加载是指路由的JS资源只有在被访问时才会动态获取,目的是为了优化项目首次打开的时间
import { lazy } from 'react'
const Login = lazy(() => import('./views/Login'))
# 路径别名
pnpm install @craco/craco
craco.config.js
const path = require('path')
module.exports = {
// plugins: [
// { plugin: require('@craco/craco') }
// ],
webpack: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
}
修改package.json
"scripts": {
"start": "craco start",
"build": "craco build"
}
联想路径配置
VsCode的联想配置,需要我们在项目目录下添加 jsconfig.json 文件,加入配置之后VsCode会自动读取配置帮助我们自动联想提示
根目录下新增配置文件 - jsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
# 打包体积优化
pnpm install source-map-explorer
# 性能相关优化Api
# useReducer
useReducer 和 useState 的区别
- useState 适合管理简单状态数据
- useReducer 适合管理复杂状态数据
作用:和useState的作用类似,用来管理相对复杂的状态数据
- 定义一个reducer函数(根据不同的action返回不同的新状态)
- 在组件中调用useReducer,并传入reducer函数和状态的初始值
- 事件发生时,通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)
function reducer(state, action) {
switch (action.type) {
case 'INC': return state + 1
case 'DEC': return state - 1
default: return state
}
}
const [state, dispatch] = useReducer(reducer, 0)
dispatch({ type: 'INC' })
useReducer 分派action时传参
# useMemo
在组件每次重新渲染的时候缓存计算的结果
const result = useMemo(() => {
// 根据count1返回计算的结果
}, [count1])
使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算
作用:允许组件在Props没有改变的情况下跳过渲染
React组件默认的渲染机制:只要父组件重新渲染子组件就会重新渲染
如果Son组件本身并不需要做渲染更新,是不是存在浪费?
const MemoComponent = memo(function SomeComponent(props) {
return <div>SomeComponent</div>
})
经过memo函数包裹生成的缓存组件只有在props发生变化的时候才会重新渲染
# memo 和 props 的比较机制
在使用memo缓存组件之后,React会对每一个 prop 使用 Object.is 比较新值和老值,返回true,表示没有变化
prop是简单类型
Object.is(3, 3) => true 没有变化
prop是引用类型(对象 / 数组)
Object([], []) => false 有变化,React只关心引用是否变化
# useCallback
在组件多次重新渲染的时候缓存函数
const Input = memo(function Input({onChange}) {
console.log('Input组件重新渲染')
return <input type="text" onChange={e => onChange(e.target.value)} />
})
function App() {
// 传递给子组件的函数
const changeHandler = useCallback(value => {
console.log(value)
}, [])
// 触发父组件重新渲染的函数
const [value, setValue] = useState(0)
return (
<div className='App'>
<Input onChange={changeHandler} />
<button onClick={() => setValue(value + 1)}>+</button>
</div>
)
}
使用useCallback包裹函数之后,函数可以保证在App重新渲染的时候保持引用稳定
# React.forwardRef
使用ref暴露DOM节点给父组件
const Input = forwardRef(function Input(props, ref) {
return <input {...props} ref={ref} />
})
function App() {
const inputRef = useRef(null)
return <Input ref={inputRef} />
}
# useImperativeHandle
通过ref暴露子组件中的方法
const Input = forwardRef((props, ref) => {
const inputRef = useRef(null)
const focus = () => {
inputRef.current.focus()
}
useImperativeHandle(ref, () => ({
focus
}))
return <input {...props} ref={inputRef} />
})
# Class
类组件的写法
类组件就是通过JS中的类来组织组件的代码
- 通过类属性state定义状态数据
- 通过setState方法来修改状态数据
- 通过render来写UI模版(JSX语法一致)
class Counter extends Component {
state = {
count: 0
}
clickHandler = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
return <div onClick={this.clickHandler}>{this.state.count}</div>
}
}
# 类组件的生命周期
组件从创建到销毁的各个阶段自动执行的函数就是生命周期函数
- componentDidMount:组件挂载完毕自动执行 - 异步数据获取
- componentWillUnmount: 组件卸载时自动执行 - 清理副作用
# zustand
极简的状态管理工具
# 异步支持
对于异步的支持不需要特殊的操作,直接在函数中编写异步逻辑,最后只需要调用set方法传入新状态即可
const useStore = create(set => {
return {
channelList: [],
fetchChannelList: async () => {
const res = await axios.get('https://api.shenzjd.com')
set({ channelList: res.data.data.channels })
}
}
})
# 切片模式
当单个store比较大的时候,可以采用 切片模式 进行模块拆分组合,类似于模块化
# vite
pnpm create vite
# 使用react模板
pnpm create vite my-app -- --template react-ts
# useState与TS
useState本身是一个泛型函数,可以传入具体的自定义类型
type User = {
name: string
age: number
}
const [user, setUser] = useState<User>({ name: 'wu', age: 18 })
- 限制useState函数参数的初始值必须满足类型为: User | ()=> User
- 限制setUser函数的参数必须满足类型为:User | ()=> User | undefined
- user状态数据具备User类型相关的类型提示
useState-初始值为null
当我们不知道状态的初始值是什么,将useState的初始值为null是一个常见的做法,可以通过具体类型联合null来做显式注解
type User = {
name: string
age: number
}
const [user, setUser] = useState<User | null>(null)
- 限制useState函数参数的初始值可以是 User | null
- 限制setUser函数的参数类型可以是 User | null
props与TypeScript
为组件prop添加类型,本质是给函数的参数做类型注解,可以使用type对象类型或者interface接口来做注解
type Props = {
name: string
}
function Button(props: Props) {
return <button>{props.name}</button>
}
props与TypeScript - 为children添加类型
children是一个比较特殊的prop, 支持多种不同类型数据的传入,需要通过一个内置的ReactNode类型来做注解
type Props = {
children: ReactNode
}
function Button(props: Props) {
return <button>{props.children}</button>
}
注解之后,children可以是多种类型,包括:React.ReactElement 、string、number、React.ReactFragment 、React.ReactPortal 、boolean、 null 、undefined
# 资源
我用夸克网盘分享了「黑马程序员-react教程.pdf」,点击链接即可保存。打开「夸克APP」在线查看,支持多种文档格式转换。 链接:https://pan.quark.cn/s/d29ef5ff01f9 (opens new window) 提取码:X7bX