React简介
React的特点
- 虚拟DOM
- 声明式
- 基于组件
- 支持服务器端渲染
- 快速、简单、易学
项目构建
react-scripts
- 打包构建项目:
npx react-scripts build
- 开发通过Webpack启动开发测试服务:
npx react-scripts start
相关名词和概念
state
- 需要一个特殊的变量,当这个变量被修改时,组件会重新渲染
- state相当于一个变量,只是变量在React中进行了注册
- React会监控这个变量的变化,当state发生变化时,会自动触发组件的重新渲染
- useState(obj,fn)
- obj:设置的值
- fn:状态改变的函数
- useState(obj,fn)
- state实际就是一个被React管理的变量
- 当我们使用setState()修改变量时,会触发组件的自动渲染
- 只有state值发生变化时才会触发渲染
- 当state的值是一个对象时,修改时时使用新的对象去替换已有对象
- 当通过setState修改一个state时,并不是表示修改当前的state,它修改的是组件下一次渲染的state值
- setState()会触发组件的重新渲染,它是异步的。
- 所以当调用setState()需要用旧state值时,一定要注意可能出现计算错误的情况
- 为了避免这种情况,可以通过为setState()传递回调函数的形式来修改state:
setCounter(prevState => prevState+1)
Ref
获取原生DOM对象
1.可以使用传统的document来对DOM进行操作
2.直接从React处获取DOM对象
- 步骤:
- 1.创建一个存储DOM对象的容器
- 使用useRef() 钩子函数
- 2.将容器设置为想要获取DOM对象元素的ref属性
<h1 id="header" ref={xxx} >...</h1>
React会自动将当前元素的DOM对象,设置为容器Current属性
3.钩子函数注意事项:
- 1.创建一个存储DOM对象的容器
- React中的钩子函数,只能用于函数组件和或自定义钩子。类组件不行
- 钩子函数只能直接在函数组件中调用
4.useRef() - 返回的就是一个普通的JS对象
- {current:undefined}
- 所以我们直接创建一个js对象,也可以代替useRef()
- 区别:
我们创建的对象,组件每次重新渲染都会创建一个新对象
useRef()创建的对象,可以确保每次渲染获取到的都是同一个对象
类组件
- 可以直接通过实例对象访问 this.props
- 类组件中state统一存储到了实例对象的state属性中
- 可以通过this.state来访问
- 通过this.setState()修改
- 通过this.setState()对其进行修改state时,只会修改设置了的属性(仅限于直接存储于state中)
- 函数组件中,响应函数直接以函数的形式定义在组件中,但是在类组件中,响应函数是以类的方法来定义
Portal
组件会默认作为父组件的后代渲染到页面中,但是有些情况下这种方式会带来一些问题 通过portal可以将组件渲染到页面指定的位置
用法
- 在index.html中添加一个新的元素
- 在组件中中通过ReactDOM.createPortal()将元素渲染到新建的元素中
- 参数:1.jsx 2.需要渲染到哪个元素
CSS Module
如果没有类名冲突的问题,外部CSS样式表不失为是一种非常好的编写样式的方式。为了解决这个问题React中还为我们提供了一中方式,CSS Module。
使用方式
CSS Module在React中已经默认支持了(前提是使用了react-scripts),所以无需再引入其他多余的模块。使用CSS Module时需要遵循如下几个步骤:
- 使用CSS Module编写的样式文件的文件名必须为
xxx.module.css
- 在组件中引入样式的格式为
import xxx from './xxx.module.css'
- 设置类名时需要使用
xxx.yyy
的形式来设置 如下:
- StyleDemo.module.css
.myDiv{
color: red;
background-color: #bfa;
font-size: 20px;
border-radius: 12px;
}
- StyleDemo.js
import styles from './StyleDemo.module.css';
const StyleDemo = () => {
return (
<div className={styles.myDiv}>
我是Div
</div>
);
};
export default StyleDemo;
Fragment
在React中,JSX必须有且只有一个根元素。这就导致了在有些情况下我们不得不在子元素的外部添加一个额外的父元素 实际上在React中已经为我们提供好了一个现成的组件帮助我们完成这个工作,这个组件可以通过React.Fragment使用
- 方式一:
import React, {Fragment} from 'react';
const MyComponent = () => {
return (
<Fragment>
<div>我是组件1</div>
<div>我是组件2</div>
<div>我是组件3</div>
</Fragment>
);
};
export default MyComponent;
- 方式二:
import React from 'react';
const MyComponent = () => {
return (
<>
<div>我是组件1</div>
<div>我是组件2</div>
<div>我是组件3</div>
</>
);
};
export default MyComponent;
Context
在React中组件间的数据通信是通过props进行的,父组件给子组件设置props,子组件给后代组件设置props,props在组件间自上向下(父传子)的逐层传递数据。但并不是所有的数据都适合这种传递方式,有些数据需要在多个组件中共同使用,如果还通过props一层一层传递,麻烦 Context为我们提供了一种在不同组件间共享数据的方式,它不再拘泥于props刻板的逐层传递,而是在外层组件中统一设置,设置后内层所有的组件都可以访问到Context中所存储的数据。换句话说,Context类似于JS中的全局作用域,可以将一些公共数据设置到一个同一个Context中,使得所有的组件都可以访问到这些数据
使用方法
- TestContext.js
/**
* Context相当于一个公共的存储空间
* 我们将多个组件中都需要访问的数据统一存储到一个Context中
*
* 通过React.createContext()创建context
*/import React from "react";
const TestContext = React.createContext({
name:'孙悟空',
age:18
})
export default TestContext
- 方式一:Test.js
import React from 'react';
import TestContext from "../store/TestContext";
/**
* 使用方式:
* 1.引入context
* 2.使用XXX.Consumer组件来创建元素
* Consumer的标签体需要一个回调函数
* 它会将context设置为会点函数的参数,通过参数就可以访问到context中存储的数据
*/
const Test = () => {
return (
<div>
<TestContext.Consumer>
{
(ctx)=>{
return <div>{ctx.name} - {ctx.age}</div>
}
}
</TestContext.Consumer>
</div>
);
};
export default Test;
- 方式二:Test2.js
import React, {useContext} from 'react';
import testContext from "../store/TestContext";
/**
* 使用方式2:
* 1.引入context
* 2.使用useContext()获取到context
*/
const Test2 = () => {
// 使用钩子函数获取context
// useContext()需要 传递context
const ctx = useContext(testContext);
return (
<div>
<div>{ctx.name} - {ctx.age}</div>
</div>
);
};
export default Test2;
- Provider译为生产者,和Consumer消费者对应。Provider会设置在外层组件中,通过value属性来指定Context的值。这个Context值在所有的Provider子组件中都可以访问。Context的搜索流程和JS中函数作用域类似,当我们获取Context时,React会在它的外层查找最近的Provider,然后返回它的Context值。如果没有找到Provider,则会返回Context模块中设置的默认值。
- App.js
<TestContext.Provider value={{name:'猪八戒',age: 28}}>
<>
<Test/>
<TestContext.Provider value={{name:'沙和尚',age: 38}}>
<Test2/>
</TestContext.Provider>
<Meals
mealsData={mealsData}
onAdd={addMealHandler}
onSub={subMealHandler}
/>
</>
</TestContext.Provider>
React.StrictMode
编写React组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了React.StrictMode
标签,那么React会非常“智能”的去检查你的组件中是否写有副作用的代码,当然这个智能是加了引号的,React官网的文档说明:
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
- Class component
constructor
,render
, andshouldComponentUpdate
methods - Class component static
getDerivedStateFromProps
method - Function component bodies
- State updater functions (the first argument to
setState
) - Functions passed to
useState
,useMemo
, oruseReducer
上文的关键字叫做“double-invoking”即重复调用,这句话是什么意思呢?大概意思就是,React并不能自动替你发现副作用,但是它会想办法让它显现出来,从而让你发现它。那么它是怎么让你发现副作用的呢?React的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了React严格模式时,这些函数会被调用两次:
类组件的的 constructor
, render
, 和 shouldComponentUpdate
方法
类组件的静态方法 getDerivedStateFromProps
函数组件的函数体
参数为函数的setState
参数为函数的useState
, useMemo
, or useReducer
重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个console.log
你会发现它会执行两次,如果你的浏览器中安装了React Developer Tools,第二次调用会显示为灰色。
如果你无法通过浏览器正常安装React Developer Tools可以通过点击这里下载。
Effect
函数组件中setState()的执行流程
- setCount() –> dispatchSetState() –> 会先判断,组件当前属于什么阶段:渲染阶段、非渲染阶段
- 如果处在渲染阶段,不会检查state值是否相同
- 如果渲染已经结束:它会检查state值是否相同
- 如果值不相同,重新渲染
- 如果值相同,不对组件重新渲染
- 如果值相同,React会在一些情况下继续执行当前组件的渲染,这个渲染不会触发其子组件的渲染,同时这个渲染不会触发实际效果
useEffect()
是一个钩子函数,需要一个函数作为参数,这个作为参数的函数,将会在组件渲染完毕后执行 useEffect语法:
useEffect(didUpdate);
useEffect()
需要一个函数作为参数,你可以这样写:
useEffect(()=>{
/* 编写那些会产生副作用的代码 */
});
useEffect()
中的回调函数会在组件每次渲染完毕之后执行,这也是它和写在函数体中代码的最大的不同,函数体中的代码会在组件渲染前执行,而useEffect()
中的代码是在组件渲染后才执行,这就避免了代码的执行影响到组件渲染。
限制Effect执行时机
组件每次渲染effect都会执行,这似乎并不总那么必要。因此在useEffect()
中我们可以限制effect的执行时机,在useEffect()
中可以将一个数组作为第二个参数传递,像是这样:
useEffect(()=>{
/* 编写那些会产生副作用的代码 */
return () => {
/* 这个函数会在下一次effect执行前调用 */
};
}, [a, b]);
上例中,数组中有两个变量a和b,设置以后effect只有在变量a或b发生变化时才会执行。这样即可限制effect的执行次数,也可以直接传递一个空数组,如果是空数组,那么effect只会执行一次。 通常会将Effect中使用的所有的局部变量都设置为依赖项,这样以来可以确保这些值发生变化时,会触发Effect的执行
- 如果依赖项设置了一个空数组,则组件只会在初始化中执行一次
清除Effect(返回函数)
组件的每次重新渲染effect都会执行,有一些情况里,两次effect执行会互相影响。比如,在effect中设置了一个定时器,总不能每次effect执行都设置一个新的定时器,所以我们需要在一个effect执行前,清除掉前一个effect所带来的影响。要实现这个功能,可以在effect中将一个函数作为返回值返回,像是这样:
useEffect(()=>{
/* 编写那些会产生副作用的代码 */
return () => {
/* 这个函数会在下一次effect执行前调用 */
};
});
effect返回的函数,会在下一次effect执行前调用,我们可以在这个函数中清除掉前一次effect执行所带来的影响。
延迟搜索(防抖)
- FilterMeals.js
const [keyword, setKeyword] = useState('');
/**
* 在开启一个定时器的同时,关掉上一个定时器
* 在Effect的回调函数中,可以指定一个函数作为返回值
* 这个函数叫清理函数,会在下次Effect执行前调用
*/
useEffect(() => {
// 后执行清除前面的定时器
const timer = setTimeout(() => {
props.onFilter(keyword);
}, 1000);
// 先执行
return () => {
clearTimeout(timer);
};
}, [keyword]);
const inputChangeHandler = e => {
setKeyword(e.target.value.trim());
};
Reducer
为了解决复杂State
带来的不便,React
为我们提供了一个新的使用State
的方式。Reducer
横空出世,reduce单词中文意味减少,而reducer我觉得可以翻译为“当你的state的过于复杂时,你就可以使用的可以对state进行整合的工具”。Reducer
可以翻译为“整合器”,它的作用就是将那些和同一个state
相关的所有函数都整合到一起,方便在组件中进行调用
使用方法如下:
App.js
import React, {useReducer} from 'react';
/**
* 为了避免reducer重发创建,通常reducer会定义到组件外部
*/
const countReducer = (prevState, action) => {
console.log('reducer执行了')
console.log('prevState:',prevState)
console.log('action:',action)
switch (action.type){
case 'ADD':
return prevState + 1
case 'SUB':
return prevState - 1
default:
return prevState
}
}
const App = () => {
/**
* useReducer(reducer, initialArg, init)
* reducer:整合函数
* 对于我们当前state的所有操作都应该在该函数定义
* initialArg: state的初始值,作用和useState()中的值是一样
* init:
* 返回值
* 数组:
* 第一个参数,state用来获取state的值
* 第二个参数,state 修改的派发器
* 通过派发器,可以发生操作state的命令,具体的修改行为将会有另外一个函数执行
*/
const [count, countDispatch] = useReducer(countReducer, 1);
return (
<div style={{fontSize:30,width:200,height:200,backgroundColor:'#bfa',margin:'100px auto',textAlign:'center'}}>
<button onClick={() => countDispatch({type:'SUB'})}>减少</button>
{count}
<button onClick={() => countDispatch({type:'ADD'})}>增加</button>
</div>
);
};
export default App;
React.Memo
React为我们提供了一个方法React.memo()
。该方法是一个高阶函数,可以用来根据组件的props对组件进行缓存,当一个组件的父组件发生重新渲染,而子组件的props没有发生变化时,它会直接将缓存中的组件渲染结果返回而不是再次触发子组件的重新渲染,这样一来就大大的降低了子组件重新渲染的次数。
组件缓存功能,包装过的组件,会具有缓存功能,只有组件的props发生变化,才会触发组件的重新渲染,否则返回缓存中的结果
useCallBack()
- useCallBack不会总在组件重新渲染时重新创建 ,只有依赖项变化时才会重新渲染
- 参数:
-
调函数
-
依赖项
const addHandler = useCallback(() => {
setCount(prevState => prevState + 1)
},[]);
Redux
A Predictable State Container for JS Apps是Redux官方对于Redux的描述,这句话可以这样翻译“一个专为JS应用设计的可预期的状态容器”,简单来说Redux是一个可预测的状态容器
Redux可以理解为是reducer和context的结合体,使用Redux即可管理复杂的state,又可以在不同的组件间方便的共享传递state。当然,Redux主要使用场景依然是大型应用,大型应用中状态比较复杂,如果只是使用reducer和context,开发起来并不是那么的便利,此时一个有一个功能强大的状态管理器就变得尤为的重要。
网页中使用redux的步骤:
-
1.引入redux核心包
-
2.创建reducer整合函数
-
3.通过reducer对象创建store
-
4.对store中的state进行订阅
-
5.通过dispatch派发state的操作指令
问题:
-
1.如果state过于复杂,将会非常难维护
-
- 通过对state分组来解决,通过创建多个reducer,然后将其合并为一个
-
2.如果state每次操作时,都需要对state进行复制,然后再去修改
-
3.case后面的常量维护起来麻烦
RTK(Redux Toolkit)
CreateSlice
createSlice是一个全自动的创建reducer切片的方法,在它的内部调用就是createAction和createReducer,之所以先介绍那两个也是这个原因。createSlice需要一个对象作为参数,对象中通过不同的属性来指定reducer的配置信息。 配置对象中的属性:
initialState
—— state的初始值name
—— reducer的名字,会作为action中type属性的前缀,不要重复reducers
—— reducer的具体方法,需要一个对象作为参数,可以以方法的形式添加reducer,RTK会自动生成action对象。 eg:
const stuSlice= createSlice({
name:'stu',
initialState:{
name: '孙悟空',
age: 18,
gender: '男',
address: '花果山'
},
reducers:{
setName(state, action){
state.name = action.payload
}
}
});
Actions
切片对象会根据我们对象中的reducers方法来自动创建action对象,这些action对象会存储到切片对象actions属性中:
stuSlice.actions; // {setName: ƒ}
上例中,我们仅仅指定一个reducer,所以actions中只有一个方法setName,可以通过解构赋值获取到切片中的action。
const {setName} = stuSlice.actions;
开发中可以将这些取出的action对象作为组件向外部导出,导出其他组件就可以直接导入这些action,然后即可通过action来触发reducer。
configureStore
切片的reducer属性是切片根据我们传递的方法自动创建生成的reducer,需要将其作为reducer传递进configureStore的配置对象中以使其生效:
const store = configureStore({
reducer: {
stu: stuSlice.reducer,
school: schoolReducer
}
});
总的来说,使用createSlice创建切片后,切片会自动根据配置对象生成action和reducer,action需要导出给调用处,调用处可以使用action作为dispatch的参数触发state的修改。reducer需要传递给configureStore以使其在仓库中生效。
RTKQ(Redux Toolkit Query)
官方文档:RTK Query Overview | Redux Toolkit (redux-toolkit.js.org) RTK不仅帮助我们解决了state的问题,同时,它还为我们提供了RTK Query用来帮助我们处理数据加载的问题。RTK Query是一个强大的数据获取和缓存工具。在它的帮助下,Web应用中的加载变得十分简单,它使我们不再需要自己编写获取数据和缓存数据的逻辑。 Web应用中加载数据时需要处理的问题:
- 根据不同的加载状态显示不同UI组件
- 减少对相同数据重复发送请求
- 使用乐观更新,提升用户体验
- 在用户与UI交互时,管理缓存的生命周期 这些问题,RTKQ都可以帮助我们处理。首先,可以直接通过RTKQ向服务器发送请求加载数据,并且RTKQ会自动对数据进行缓存,避免重复发送不必要的请求。其次,RTKQ在发送请求时会根据请求不同的状态返回不同的值,我们可以通过这些值来监视请求发送的过程并随时中止。
使用方法
- store/studentApi.js:
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
// 创建Api对象
// 用来创建RTKQ中的API对象
const studentApi = createApi({
reducerPath:'studentApi', // Api标识,不能和其它的api或reducer重复
baseQuery: fetchBaseQuery({
baseUrl:'http://localhost:1337/api'
}), // 指定查询的基础信息,发送请求的工具
endpoints(build) {
// build 是请求的构建器,通过build来设置请求的相关信息
return {
getStudents:build.query({
query() {
// 指定请求子路径
return '/students'
},
// transformResponse用来转换响应数据的格式
transformResponse(baseQueryReturnValue, meta, arg) {
return baseQueryReturnValue
}
}),
// getStudentById:build.query(),
// updateStudent:build.mutation()
};
} // 用来指定api中的各种功能,是一个方法。需要一个对象作为返回值
});
// api对象创建后,对象会根据各种方法自动生成对应的钩子函数
// 通过这些钩子函数,可以项服务器发送请求
// 钩子函数命名规则 useGetStudentsQuery
export const {useGetStudentsQuery} = studentApi
export default studentApi;
React Router
用法
component 用来指定路由匹配后被挂载的组件 component需要直接传递组件的类 通过component构建的组件,它会自动构建且会自动传递参数
- match – 匹配的信息
- isExact: 路径是否完全匹配
- params:请求参数
<Route path="/student/:id" component={Student}/>
- path:规则路径
- url:真实路径
- location – 地址信息
- search:查询字符串
- hash:哈希值
- state:传递状态
- pathname:真实路径
- history – 控制页面的跳转
- push() 跳转页面
- replace() 替换页面
...