一、准备工作
本文略长,建议耐心读完,每一节的内容与上一节的内容存在关联,最好跟着案例过一遍,加深记忆。
1.1 创建项目
- 第一步,执行下面的命令来创建一个 React 项目。
npx create-react-app react-example
cd react-example
- 第二步,安装依赖,运行项目
yarn install 或 npm install
yarn start 或 npm run start
1.2 项目结构
如图:
1.3 初始化
将 src/index.js
的默认代码删掉,保留下面这部分。
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById("root"));
const App = () => {
return <div>Hello</div>
}
root.render(<App />);
现在项目看起来就像这样,一个简单的 Hello
。
二、React 的基本用法
如果你还不熟悉 React 的基础语法,可以阅读我前面写的 React & 工作日常语法。
1.1 输出 Hello, world
第一步肯定是要先来句 Hello,world!
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById("root"));
const App = () => {
return <div>Hello, world!</div> // 改动点
}
root.render(<App />);
1.2 组件的使用
组件
可以由函数或者类来进行创建,像上面的App
函数就是一个组件,里面可以混合写 HTML & JS 代码,React 会自动帮我们解析这些语法,像这种写法就叫就JSX
。
下面我将以函数的方式来定义组件作为案例,类组件在讲生命周期时做演示。
- 第一步,随便定义一个叫
List
的组件,内容随便写,比如 Hi List
const List = () => {
return <div>Hi List.</div>
}
- 第二步,引入
List
,目前入口只有App
,很明显只能放这里。
const App = () => {
return <div>Hello, world! </List> </div>
}
效果:
1.3 组件嵌套组件
像
App
组件嵌套List
组件,我们管这叫父子组件
,App 是父组件, List 是子组件,当 List 套上 Item 组件时,那 Item 的父组件就是 List,而祖父组件是 App, 以此类推。
现在我们来给 List
嵌套一个 Item
组件。
const List = () => {
// 多行时要用 () 包裹
return (
<ul>
<Item/> // 引入
</ul>
)
}
const Item = () => {
return <li>item1</li>
}
效果:
别忘了还可以写 JSX 的,现在试试用 Array.map 循环多个 Item 组件。
const List = () => {
return (
<ul>
{[1, 2, 3, 4].map((num) => Item(num) )}
</ul>
)
}
const Item = (num) => {
return <li key={num}>item{num}</li>
}
在看效果前思考下,上面代码具体会发生哪些变化?
- Item 函数组件多了个
num
参数,我们用它来接受,并用{num}
括号包括起来进行访问 <Item/>
的调用方式变成Item(num)
,因为我们要将 num 值传递给 Item 函数,所以我们又发现了一个特性,在不传参的情况下直接声明<Item/>
是会自动触发函数的。- 循环组件时,
key
属性得带上且是唯一的。
效果:
1.3 组件绑定点击事件
干巴巴的列表,没有交互行为怎么能行呢,
现在我们就来定义一个事件,当点击某个 Item 时提示对应的 num 数值,代码如下:
const onClickItem = (num) => {
alert(num);
}
const Item = (num) => {
return <li key={num} onClick={() => onClickItem(num)}>item{num}</li>
}
解释:
- 点击事件用
onClick
驼峰形式来表示,其它事件也是类似的,比如 onFocus, onMouse 等。 onClick={() => 函数}
花括号返回一个箭头函数,函数里面就是返回我们定义的函数,后面点击时会触发。
不用怀疑语法是否有问题,连 JSX 语法都有了,你还在乎这个?
效果:
1.5 响应式数据
光弹窗没啥用啊,要不点击后直接变更 Item 的数据如何?
问题是… 怎么变更?直接将 num = xxx?熟悉 JS 的朋友应该知道,当 num 参数传递的是【基本类型】时只是一个副本,更改后对原来的 num 是无效的。
就算有效,你又如何将视图中的 num 也发生变化呢? 看来还是得借助 React 提供的语法了。
唉没办法,学吧~
-
第一步,先将原来的
[1,2,3,4]
以useState
提供的函数来定义并导出arr
变量数组,如图:
效果还是与原来一样,这里就不贴图了。 -
第二步,在 arr 后面再声明一个
setArr
函数,名字随便定义,但一般用 set 开头作为规范好点,表示用来变更 arr 数据的。
const List = () => {
const [arr, setArr] = useState([1, 2, 3, 4])
// ...省略
- 第三步,触发
setArr
函数,更改 arr 数组里的数据来达到我们想要的视图变更效果
由于setArr
是在 List 函数组件里定义的,其它函数无法直接访问,得通过传参的方式带过去、一直传到 onClickItem 中,有点绕,将就一下吧,相信你不介意的(/逃)。
这里我们重点关注这一段代码:
setArr(state => {
const arr = [...state];
arr[1] = 1000;
return arr;
})
当点击后,将 arr[1] 改为 1000,并将新的 arr 返回出去,效果如图:
你可能注意到,state 就是原先的 arr,然后我们用扩展符号将它拷贝一份到新的 arr 中,为什么要这样做呢?直接 state[1] = 1000
再 return 出去不行吗,多此一举嘛这不是。
是可以这样做,数据也会发生变化了,但视图不会发生变化,因为 React 明确规定:state 是不可变数据,你不能直接改变它,而是要用一份副本去改变
。
为什么 state 要坚持不可变原则呢?官方也说了,当你要实现一个撤销&恢复的功能时就没辙了,或者说实现起来更复杂?React 这时要是还允许你去那岂不是有点不地道了,这不跟吃了上顿不考虑下顿的道理嘛。
小结:
- useState 可以声明响应式数据。
- state 数据不可变,要用副本代替,遵循不可变原则。
1.6 什么是钩子函数 Hook Function
其实我们已经用过钩子函数了,上面的 useState
就是一个钩子函数,React 内置了许多的钩子,比如 useEffect, useRef
等,这里就不一一介绍,只需明白以 use
为开头的均属于钩子函数即可。
我们也可以自定义钩子,问题来了,在什么样的场景下需要呢?里面写啥呢?
这个其实没有唯一答案,每个人对理解钩子的程度是不同的,导致有千千万万种类型的钩子。
我个人更喜欢将它归为处理脏活
的一类函数,更通俗点来说,是用来处理响应式数据
的,比如,有个奖品业务的功能模块,针对这个模块,可能有验证奖品配置的逻辑,那么我就会给这个奖品加上几个 hook 函数,以便后续方便调用。
const usePrize = () => {
const verifyPrizeConfig = (state) => {
// Do something ...
}
const resetPrizeConfig = (state) => {
// Do something ...
}
return [
verifyPrizeConfig,
resetPrizeConfig,
]
}
const [ verifyPrizeConfig ] = usePrize();
1.7 组件的生命周期
每个组件渲染时,React 会逐步按顺序触发一些内置函数,这些被称为”生命周期函数“,我们可以根据不同周期函数来做一些业务处理,比如我想在组件渲染前先请求接口得到数据。
这里需要注意,函数组件
是没有命周期函数的,只有类组件
才有,既然这样,我们将 App
这个组件变成类组件即可,其它保持不变,谁规定类组件
里面就不能嵌套函数组件
?
- 第一步,将 App 函数组件改为类组件,如下:
// 源 App 函数组件
// const App = () => {
// return <div>Hello, world! <List/> </div>
// }
// 新 APP 类组件
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div>Hello, world! <List /> </div>
}
}
- 第二步,添加生命周期函数,这里用最常见的
componentDidMount
函数,表示组件第一次渲染时提前触发。
class App extends React.Component {
// 省略
componentDidMount() {
// Do something...
alert('Init')
}
}
1.8 函数组件模拟生命周期
你可能想,这不公平,函数组件凭啥没有周期函数?别急,React 提供的钩子函数 useEffect
就派上用场了,该钩子完全可以模拟生命周期的三大核心函数:
componentDidMount() {} 组件第一次渲染时,就刚刚用到的
componentDidUpdate() {} 组件数据更新时(state 更新)
componentWillUnMount() {} 组件销毁时
使用方式也很简单,这里以 List 组件作为案例,不能忘记我们的老伙伴~
- 第一步,模拟
componentDidMount
,如下:
import { useState, useEffect } from 'react';
const List = () => {
const [arr, setArr] = useState([1, 2, 3, 4]);
// 模拟 componentDidMount
useEffect(() => {
alert('List init')
});
return (
<ul>
{arr.map((num) => Item(num, setArr) )}
</ul>
)
}
- 第二步,模拟
componentDidUpdate
,在 useEffect 的第二个参数监听 state:
// ... 省略
const [arr, setArr] = useState([1, 2, 3, 4])
useEffect(() => {
alert('List init')
}, [arr]) // componentDidUpdate
当点击更新数据时,这个函数就会再次触发,如图:
- 第三步,模拟
componentWillUnMount
,在 useEffect 里面 return 一个函数即可。
const [arr, setArr] = useState([1, 2, 3, 4])
useEffect(() => {
alert('List init')
return () => { // componentWillUnMount
alert('List destroyed!');
};
}, [arr])
componentWillUnMount 什么时候触发呢?实际上,每当 state 更新时,组件会重新渲染一次,这属于销毁行为,因此当点击更新数据后,会先触发 componentWillUnMount ,再触发 componentDidUpdate 。
效果:
三、React-router-dom
1.1 什么是 React-router-dom
一个页面怎么够用呢,现在我们想要通过点击 item 进入另一个页面,且保持页面不刷新,这里便可以用 React 提供的插件 React-router-dom ,俗称路由
来实现。
1.2 React-router-dom 和 React-router 版本的区别
React-router-dom 是基于 React-router 改造的新版本,现在大家常用的版本是 React-router-dom
,
本案例将用 React-router-dom 来作为演示。
1.3 React-router-dom 的使用
- 第一步,下载 react-router-dom。
yarn add react-router-dom
- 第二步,在
src/index.js
中引入。
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
- 第三步,将我们之前的 App 组件引入方式重新改造一下
// 旧代码
// root.render(<App />);
// 新代码
const router = createBrowserRouter([
{
path: "/",
element: <App/>,
},
]);
root.render(<RouterProvider router={router} />);
现在效果和原来没区别,这里解释下步骤:
- 将原来 root.render 里的 App 替换成
RouterProvider
组件 - 在
createBrowserRoute
里面的element
引入 App。 - path (
路由
),当访问/
根目录时才会渲染 App 组件。
- 第四步,新建
AppDetail
组件并挂载到/detail
路由上。
const AppDetail = () => {
return <h1>Detail data</h1>
}
const router = createBrowserRouter([
{
path: "/",
element: <App/>,
},
// 挂载到 /detail 路由
{
path: "/detail",
element: <AppDetail/>,
},
]);
- 第五步,访问
/detail
看看效果
- 第六步,以点击跳转的方式访问
/detail
,这里用到 router 提供的<Link>
标签。
import {
createBrowserRouter,
RouterProvider,
Link,
} from "react-router-dom";
const Item = (num, setArr) => {
return (
<li key={num} onClick={() => onClickItem(num, setArr)}>
<Link to='/detail'>item{num}</Link>
</li>
)
}
效果:
react-router-dom 就这么简单,至于其它 API 的使用可以参考文档,这里不过多讲解。
四、React-redux
1.1 什么是 React-redux
React-redux 可以用来管理全局状态 state 的工具,使得组件之间可以访问同一份 state。
1.2 什么时候用 React-redux
当多个组件重复使用同一份 state 时就可以考虑用 redux 将它提升到全局中,以便于维护。
1.3 React-redux 下载&配置
- 第一步,下载
react-redux
,这里官方还提供了一个工具包@reduxjs/toolkit
,里面包含了 react-redux 所有功能以及内置一些其它功能,是官方极力推荐的,这两个一起下载。
yarn add react-redux @reduxjs/toolkit
- 第二步,在 src 下新建
store/index.js
文件,负责管理全局 state,初始化内容如下:
// ./src/store/index.js
import { configureStore, createSlice } from '@reduxjs/toolkit'
const userSlice = createSlice({
name: 'User', // name 必填的,当前作用域的标识符,可以理解为 nameSpace 命名空间,否则页面上无法正常展示。
initialState: { // 声明 state 的地方
},
reducers: { // 声明 reducer 函数的地方
}
});
// 将 store 导出去。
const store = configureStore({
reducer: userSlice.reducer
})
export default store;
- 第三步,在
src/index.js
中引入store
import store from './store/index';
import { Provider } from 'react-redux'
root.render(
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
);
解释:
- 从 store.js 中引入
store
对象。 - 从 redux 中引入
Provider
组件,并将原先的RouterProvider
组件包裹起来。 - 将
store
作为参数传递给Provider
组件。
目前来讲,页面跟原来没有区别,但是现在我们多了 redux 的功能,何乐而不为呢。
1.4 什么是 Reducer
Reducer
与 Vuex 中的 mutations 差不多同一个意思,里面专门定义一些处理 state 的函数,reducer 主要接受一个 state 和一个 action ,根据这两个参数处理相关逻辑,然后返回新的 state (遵循前面所说的“不可变原则”
)。
1.5 什么是 dispatch(action)
dispatch
是用来调用 reducer 函数的。action
是 dispatch 调用 reducer 函数时要传递的一个描述对象,好让 reducer 知道该干什么事。该描述对象总共就俩参数:type
/payload
,type 是调用 reducer 的函数名,payload 是我们要传参的数据,给 reducer 接受用的。
1.6 使用
理解 reducer/dispatch/action
三大核心概念之后,我们来开始使用:
- 第一步,在
initialState
对象中定义全局响应式数据。
// 省略...
initialState: {
// 新增
user: {
name: 'Jack',
desc: 'Hello,world!'
}
},
- 第二步,新建
User
组件,该组件用来访问上面声明的响应式数据,并挂载到 App 和 AppDetail 中渲染,代码如下:
// ./src/index.js
// ...省略
import { Provider, useSelector } from 'react-redux'
// 新增
const User = () => {
// 用 redux 提供的钩子来获取 state
const user = useSelector(state => state.user);
return (
<div>
<span>{user.name}</span>
<span> says: {user.desc}</span>
</div>
)
}
// ./src/index.js
// ...省略
class App extends React.Component {
// ...省略
render() {
return (
<div>
<User /> // 新增:挂载 User
<List />
</div>
)
}
}
const AppDetail = () => {
return (
<h1>
Detail data
<User /> // 新增:挂载 User
</h1>
)
}
现在的效果:Jack says: Hello,world!
- 第三步,声明
reducer
函数来变更 state 数据。
// .src/store/index.js
// ...省略
reducers: { // 声明 reducer 函数的地方
// 新增
changeUserInfo(state, action) {
const { payload } = action;
switch(payload.state) {
case 'name':
return {
...state,
user: {
...state.user,
name: '杰克',
}
}
case 'desc':
return {
...state,
user: {
...state.user,
desc: '你好,世界!',
}
}
default:
return state;
}
}
}
changeUserInfo 函数解释:
- 根据 payload.state 即我们准备传参的数据来变更用户名 name 还是描述
desc - 遵循不破坏 state 原则,这里我们用扩展符来合并。
- 第四步,使用
dispatch(action)
来触发 reducer,完成变更效果。
// ./src/index.js
import { Provider, useSelector, useDispatch } from 'react-redux'
const User = () => {
const user = useSelector(state => state.user);
const dispatch = useDispatch(); // 引入触发 reducer 的钩子
return (
<div>
<span>{user.name}</span>
<span> says: {user.desc}</span>
// 以下是新增的
<button onClick={() => dispatch({
type: 'User/changeUserInfo',
payload: {
state: 'name'
}
})}>
更换名字
</button>
<button onClick={() => dispatch({
type: 'User/changeUserInfo',
payload: {
state: 'desc'
}
})}>
更换描述
</button>
</div>
)
}
现在来看看总体效果:
五、总结
不知不觉,我们已经用到了 React-dom & React-router& React-redux:
root.render( // react-dom
<Provider store={store}> // react-redux
<RouterProvider router={router} /> // react-router-dom
</Provider>
);
恭喜你已成功入门 React 全家桶,剩下就交给实践的时间来帮助我们熟能生巧。
有问题欢迎指出!
完!
案例已放在 github 上