一、React基础
1.React的特点
- 声明式编程:Vue也是这个模式,它允许我们只需要维护自己的状态,当状态改变的时候,React可以根据最新的状态去渲染我们的UI界面;
- 组件化开发;
- 多平台适配:ReactNative,ReactVR。
2.React的开发依赖
(1) 开发React必须依赖三个库:react、react-dom、babel;
- react:包含react所必须的核心代码;
- react-dom:react渲染到不同平台所需要的代码;
- babel:将jsx转化为react代码的工具。
(2) 在React 0.14版本之前是没有react-dom这个概念的,所有的功能都包含在react里面;至于为什么要拆分,主要是因为react-native,react包含了react web和react-native所共同拥有的核心代码;react-dom针对web和native所完成的事情不同:web端react-dom会将jsx最终渲染成真实的dom,显示在浏览器中;native端:react-dom会将jsx最终渲染成原生的控件(Android,ios)。
(3) React和Babel的关系:
默认情况下其实可以不使用babel,但前提是我们自己使用React.createElement来编写源代码,它的编写十分繁琐,可读性也很差;所以我们就是用jsx语法,babel就是将jsx转化为React.createElement。
3.React组件化开发
<div class="root"></div>
<script src="./lib/react.js"></script>
<script src="./lib/react-dom.js"></script>
<script src="./lib/babel.js"></script>
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
message: "hello world"
}
this.btnClick = this.btnClick.bind(this)
}
btnClick() {
this.setState({
message: "hello react"
})
}
render() {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={this.btnClick}>修改文本</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector(".root"))
root.render(<App />)
</script>
4.事件绑定
在类中定义一个函数,直接将其绑定在元素的onClick事件上,这个函数的this指向的是undefined:这是因为在正常的DOM操作中,监听点击,监听函数的this其实是节点对象(比如说button);React并不是直接渲染成真实的DOM,我们所编写的button其实只是一个语法糖,它的本质是React的Element对象;那么,在发生监听的时候,react在执行函数时并没有绑定this,默认情况下就是一个undefined。
我们在绑定的函数中,可能想要使用当前的象,比如执行this.setState函数,就必须拿到当前对象的this,我们就需要在传入函数时,给这个函数绑定this。
二、JSX
1.JSX是什么?
JSX是js的语法扩展,它用于描述我们的UI界面,并且完全可以和js融合在一起使用,它不同于vue的模板语法,不用单独学习模板语法的一些指令。
2.JSX的书写规范
(1) JSX的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div(或者使用Fragment);
(2) 我们通常在jsx的外层包裹一个小括号(),这样可以方便阅读,并且jsx可以进行换行书写;
(3) jsx中的标签可以是单标签也可以是双标签。
3.jsx的使用
(1) jsx的注释:
{ /* JSX的注释写法 */ }
(2) jsx嵌入变量作为子元素:
- 当变量是Number、String、Array类型时,可以直接显示;
- 当变量是null、undefined、Boolean,内容为空,如果要显示需要转化为字符串;
- Object类型不能作为子元素。
(3) jsx嵌入表达式:
- 运算表达式
- 三元表达式
- 执行一个函数
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
movies: ["梦幻西游", "大话西游", "西游记", "红楼梦"],
num: 10,
str: "yuan",
arr: [1, 2, 3, 4, 5],
aaa: null,
bbb: undefined,
ccc: true,
friend: {
name: "yuan"
}
}
}
render() {
const { num, str, arr, aaa, bbb, ccc, friend, movies } = this.state
return (
<div>
<h2>{num}</h2>
<h2>{str}</h2>
<h2>{arr}</h2>
<h2>{String(aaa)}</h2>
<h2>{bbb + ""}</h2>
<h2>{ccc.toString()}</h2>
<h2>{friend.name}</h2>
<h2>{Object.keys(friend)[0]}</h2>
<h2>{10 + 20}</h2>
<h2>{10 > 20 ? "yes" : "no"}</h2>
<ul>
{
movies.map(item => {
return <li key={item}>{item}</li>
})
}
</ul>
<ul>{this.getMovies()}</ul>
</div>
)
}
getMovies() {
const liEl = this.state.movies.map(movies => <li key={movies}>{movies}</li>)
return liEl
}
}
const root = ReactDOM.createRoot(document.querySelector(".root"))
root.render(<App />)
</script>
(4) jsx中绑定属性:
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
title: "哈哈哈",
imgUrl: "https://ts1.cn.mm.bing.net/th/id/R-C.95bc299c3f1f0e69b9eb1d0772b14a98?rik=W5QLhXiERW4nLQ&riu=http%3a%2f%2f20178405.s21i.faiusr.com%2f2%2fABUIABACGAAgoeLO-wUo4I3o2gEw8Qs4uAg.jpg&ehk=N7Bxe9nqM08w4evC2kK6yyC%2bxIWTjdd6HgXsQYPbMj0%3d&risl=&pid=ImgRaw&r=0",
href: "https://www.baidu.com",
isActive: true,
objStyle: { color: "red", fontSize: "30px" }
}
}
render() {
const { title, imgUrl, href, isActive, objStyle } = this.state
// 1.字符串拼接
const className = `abc cba ${isActive ? 'active' : ''}`
// 2.将所有的class放到数组中
const classList = ['abc', 'cba']
if (isActive) classList.push("active")
// 3.使用第三方库:classnames
return (
<div>
<img src={imgUrl} />
<h2 title={title}>我是h2元素</h2>
<a href={href}>百度一下</a>
<h2 style={objStyle}>你好</h2>
<h2 className={className}>className</h2>
<h2 className={classList.join(" ")}>哈哈哈哈</h2>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector(".root"))
root.render(<App />)
</script>
三、React事件绑定
前面已经说了事件绑定为什么要绑定this,这里就直接列举出事件绑定的三种方案:
(1) 在构造函数中显示绑定this;
(2) 声明函数的时候使用箭头函数;
(3) 事件监听的时候传入一个箭头函数;
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
counter: 100
}
this.btn1Click = this.btn1Click.bind(this)
}
btn1Click() {
this.setState({
counter: this.state.counter + 1
})
}
btn2Click = () => {
this.setState({
counter: this.state.counter + 1
})
}
btn3Click() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
const { counter } = this.state
return (
<div>
<h2>当前计数:{counter}</h2>
{/* 1.this绑定方式一: bind绑定 */}
<button onClick={this.btn1Click}>btn1</button>
{/* 2.this绑定方式二: ES6 class fields */}
<button onClick={this.btn2Click}>btn2</button>
{/* 3.this绑定方式三: 直接传入一个箭头函数(重要) */}
<button onClick={() => this.btn3Click()}>btn3</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector(".root"))
root.render(<App />)
</script>
(4) 事件参数的传递
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
}
}
btn1Click(event, name, age) {
console.log("event", event);
console.log(name, age);
}
render() {
return (
<div>
{/* 1.event参数的传递 */}
<button onClick={this.btn1Click.bind(this)}>btn1</button>
<button onClick={(event) => this.btn1Click(event)}>btn2</button>
{/* 1.额外参数的传递 */}
<button onClick={this.btn1Click.bind(this, "yuan", 18)}>btn3</button>
<button onClick={(event) => this.btn1Click(event, "yuan", 18)}>btn4</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector(".root"))
root.render(<App />)
</script>
四、React条件渲染
(1) 条件判断语句:适合逻辑较多的情况;
(2) 三元运算符:适合逻辑简单的情况;
(3) 与运算符&&:适合条件成立渲染组件,不成立什么内容也不显示。
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
message: "hello world",
flag: true,
friend: undefined
}
}
render() {
const { message, flag, friend } = this.state
// 1.条件判断方式一:使用if判断
let showEl = null
if (flag) {
showEl = <h2>要显示的内容1</h2>
} else {
showEl = <h2>要显示的内容2</h2>
}
return (
<div>
<div>{showEl}</div>
{/* 2.方式二: 三元运算符 */}
<div>{flag ? <button>按钮1</button> : <button>按钮2</button>}</div>
{/* 3.方式三: &&逻辑与运算 */}
<div>{friend && <div>朋友</div>}</div>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector(".root"))
root.render(<App />)
</script>
五、React列表渲染
使用map、filter等数组方法实现
<div className="list">
{
students.filter(item => item.score > 100).slice(0, 2).map(item => {
return (
<div className="item" key={item.id}>
<h2>学号: {item.id}</h2>
<h3>姓名: {item.name}</h3>
<h1>分数: {item.score}</h1>
</div>
)
})
}
</div>
六、React脚手架
create-react-app project-name
npm start启动项目
npm eject:将webpack相关的配置显示,此过程是不可逆的
七、React组件化开发
1.React组件的分类
(1) 根据组件的定义方式,可以分为函数组件和类组件;
(2) 根据组件内部是否有需要维护的状态,可以分为:无状态组件和有状态组件;
(3) 根据组件的职责不同,还可以分为展示型组件和容器型组件;
2.类组件
(1) 类组件的定义要求:
- 组件的名称必须是大写字符开头(无论是类组件还是函数组件);
- 类组件必须继承自React.Component;
- 类组件必须实现render函数。
(2) 使用class定义一个组件:
- constructor是可选的,我们可以在constructor中初始化一些数据;
- this.state中维护的是我们组件内部的数据;
- render()方法是组件中唯一必须要实现的方法;
import { Component } from "react";
class App extends Component {
constructor() {
super();
this.state = {};
}
render() {
return (
<>
<div>App</div>
</>
);
}
}
export default App;
3.函数组件
函数组件有自己的特点(后面的hooks就不一样了):没有生命周期,但是会被更新挂载;this关键字不能指向组件实例,因为没有组件实例;没有内部的状态(state)。
export default function App() {
return (
<div>
<h2>我是h2</h2>
<h2>hello func</h2>
</div>
);
}
4.组件的生命周期
(1) constructor():如果不初始化state或不进行方法的绑定,则不需要为React组件实现构造函数;constructor通常只做两件事:通过this.state赋值对象来初始化内部的state,为事件绑定实例(this)。
(2) componentDidMount():会在组件挂载后立即调用。
在componentDidMount中通常进行的操作:对DOM进行操作;发送网络请求(官方建议);在此处添加一些订阅(可以在componentWillUnMount取消订阅)。
(3) componentDidUpdate(prevProps,prevState,snapShot):会在更新后调用,初次渲染不会调用;在组件更新后,可以在此处对DOM进行操作;如果你对更新前后的props进行了比较,也可以在此处发送网络请求(如果props没有发生改变就不会发送网络请求)。
(4) componentWillUnMount():会在组件卸载及销毁之前调用:可以在此方法执行必要的清除操作,例如清除定时器、取消网络请求或者取消订阅。
(5) getDerivedStateFromProps():该方法返回一个对象来更新state。
(6) getSnapshotBeforeUpdate():在react更新DOM前回调的一个函数,可以获取到DOM更新前的一些数据。
(7) shouldComponentUpdate():必须返回true才能更新组件,可用于性能优化,PureComponent自动实现该方法。
挂载阶段(Mounting):
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
更新阶段(Updating):
- getDerivedStateFromProps
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
卸载阶段(UnMounting):
- componentWillUnMount
import React from "react";
class HelloWorld extends React.Component {
constructor(props) {
super(props);
this.state = {
message: "hello world",
name: "yuan",
};
console.log("constructor");
}
changeText() {
this.setState({ message: "hello react" });
}
render() {
console.log("render");
const { message, name } = this.state;
return (
<div>
<h2>
{message}--{name}
</h2>
<button onClick={() => this.changeText()}>修改文本</button>
</div>
);
}
componentDidMount() {
console.log("componentDidMount");
}
componentDidUpdate(prevProps, prevState, snapShot) {
console.log("componentDidUpdate", prevProps, prevState, snapShot);
}
shouldComponentUpdate() {
return true;
}
getSnapshotBeforeUpdate() {
console.log("getSnapshotBeforeUpdate");
return {
aihao: "xiao",
};
}
static getDerivedStateFromProps(props, state) {
console.log("getDerivedStateFromProps");
return {
// name: state.name,
};
}
componentWillUnmount() {
console.log("componentWillUnmount");
}
}
export default HelloWorld;
App:
import React from "react";
import HelloWorld from "./HelloWorld";
class App extends React.Component {
constructor() {
super();
this.state = {
showFlag: true,
name: "xiao",
};
}
btnClick() {
this.setState({
showFlag: !this.state.showFlag,
});
}
render() {
const { showFlag, name } = this.state;
return (
<div>
{showFlag && <HelloWorld name={name}></HelloWorld>}
<button onClick={() => this.btnClick()}>显示隐藏</button>
</div>
);
}
}
export default App;
除此之外,React还有一些过期的生命周期函数,这些周期函数已经不推荐使用了,如componentWillMount、componentWillUpdate。
5.组件之间的通信
(1) 父传子:父组件通过属性=值的形式传递给子组件数据,子组件通过props参数获取父组件传递过来的数据。
{/*类组件*/}
import React from "react";
class Children extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { name } = this.props;
return <div>{name}</div>;
}
}
export default Children;
{/*函数组件*/}
export default function ChildrenFunc(props) {
const { name } = props;
return (
<div>
<h2>{name}</h2>
</div>
);
}
{/*父组件*/}
import React from "react";
import Children from "./Children";
import ChildrenFunc from "./Children-func";
class Parent extends React.Component {
constructor() {
super();
this.state = {};
}
render() {
return (
<div>
<Children name="yuan"></Children>
<ChildrenFunc name="xiao"></ChildrenFunc>
</div>
);
}
}
export default Parent;
(2) 父传子-参数验证:propsTypes
如果你的项目中默认继承了flow或者ts,那么就可以直接进行类型的验证;如果没有,那可以使用prop-types库来进行参数验证;
import React from "react";
import PropTypes from "prop-types";
class Children extends React.Component {
// static defaultProps = {
// name: "默认姓名",
// };
constructor(props) {
super(props);
this.state = {};
}
render() {
const { name } = this.props;
return <div>{name}</div>;
}
}
Children.propTypes = {
name: PropTypes.string,
};
Children.defaultProps = {
name: "默认姓名",
};
export default Children;
(3) 子传父:在Vue中是通过自定义事件来完成的,在React中是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可。
{/*父组件*/}
import React from "react";
import AddCounter from "./AddCounter";
class App extends React.Component {
constructor() {
super();
this.state = {
counter: 0,
};
}
addCounter(count) {
this.setState({
counter: this.state.counter + count,
});
}
render() {
const { counter } = this.state;
return (
<div>
<h2>{counter}</h2>
<AddCounter addClick={(count) => this.addCounter(count)}></AddCounter>
</div>
);
}
}
export default App;
{/*子组件*/}
import React from "react";
class AddCounter extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
addCounter(count) {
this.props.addClick(count);
}
render() {
return (
<div>
<button onClick={(e) => this.addCounter(1)}>+1</button>
</div>
);
}
}
export default AddCounter;
6.React中的插槽
有两种方案可以实现:
- 组件的children子元素
- 通过props传递React元素
import React, { Component } from "react";
import NavBar from "./nav-bar";
import NavBarTwo from "./nav-bar-two";
export class App extends Component {
render() {
const btn = <button>按钮2</button>;
return (
<div>
{/* 1.使用children实现插槽 */}
{/* 只传一个就是children */}
<NavBar>
<button>按钮</button>
<h2>哈哈哈</h2>
<i>斜体文本</i>
</NavBar>
{/* 2.使用props实现插槽 */}
<NavBarTwo
leftSlot={btn}
centerSlot={<h2>呵呵呵</h2>}
rightSlot={<i>斜体2</i>}
/>
</div>
);
}
}
export default App;
{/*NavBar*/}
import React from "react";
class NavBar extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { children } = this.props;
return (
<div>
{children[0]}
{children[1]}
</div>
);
}
}
export default NavBar;
{/*TabBar*/}
import React from "react";
class TabBar extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { btn, divEl } = this.props;
return (
<div>
{btn}
{divEl}
</div>
);
}
}
export default TabBar;
7.非父子组件传值-context
(1) 如果组件的层级比较深,需要一层一层传递数据,这样代码是十分冗余的,因此React提供了一个API-Context来实现,Context设计的目的是为了共享那些对于一个组件树而言是全局的数据。
(2) Context相关的API:
-
React.createContext(defaultValue)
- 创建一个需要共享的Context对象;
- 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的Provider中读取到当前的Context值;
- defaultValue是组件在顶层的查找过程中没有找到对应的Provider,那么使用默认值。
-
Context.Provider
- 每一个Context对象都会返回一个Provider React组件,它允许组件订阅context的变化;
- Provider接受一个value属性,传递给消费组件;
- 一个Provider可以和多个消费组件有对应关系;
- 多个Provider也可以嵌套使用,里层的会覆盖外层的数据;
- 当Provider的value值发生改变的时候,它内部的所有消费组件都会重新渲染。
-
Class.contextType
- 挂载在class上的contextType属性会被重新赋值为一个由React.createContext()创建的Context对象,可以让你使用this.context来消费最近Context上的那个值;
- 你可以在任何一个生命周期中访问它,包括render函数中。
-
Context.Consumer
- 订阅Provider到context的变更,可以在函数式组件中使用;
- 这里需要函数作为子元素;
- 这个函数接受当前的context的值,返回一个React节点。
//theme-context.js
import React from "react";
const ThemeContext = React.createContext();
export default ThemeContext;
//user-context.js
import React from "react";
const UserContext = React.createContext();
export default UserContext;
{/*App*/}
import React from "react";
import Home from "./Home";
import UserContext from "./context/user-context";
import ThemeContext from "./context/theme-context";
class App extends React.Component {
constructor() {
super();
this.state = {
info: {
name: "yuan",
age: 30,
},
};
}
render() {
const { info } = this.state;
return (
<div>
<ThemeContext.Provider value={{ color: "red", fontSize: "30px" }}>
<UserContext.Provider value={{ nickname: "ayuan", aihao: "美女" }}>
<Home {...info}></Home>
</UserContext.Provider>
</ThemeContext.Provider>
</div>
);
}
}
export default App;
{/*Home*/}
import React from "react";
import UserContext from "./context/user-context";
import ThemeContext from "./context/theme-context";
import Func from "./Func";
class Home extends React.Component {
constructor() {
super();
this.state = {};
}
render() {
const { name, age } = this.props;
const { color, fontSize } = this.context;
console.log(this.context);
return (
<div>
<h2>
{name} - {age}
</h2>
<UserContext.Consumer>
{(value) => {
return (
<h2 style={{ color, fontSize }}>
{value.nickname}-{value.aihao}
</h2>
);
}}
</UserContext.Consumer>
<Func></Func>
</div>
);
}
}
Home.contextType = ThemeContext;
export default Home;
{/*Func*/}
import ThemeContext from "./context/theme-context";
export default function Func(props) {
return (
<div>
<ThemeContext.Consumer>
{(value) => {
return (
<div style={{ color: value.color, fontSize: value.fontSize }}>
你好,div
</div>
);
}}
</ThemeContext.Consumer>
</div>
);
}
8.非父子通信-eventbus
(1) npm i mitt
(2) 新建一个eventbus文件
import mitt from "mitt";
const emitter = mitt();
export default emitter;
(3) 使用
{/*User组件*/}
import React from "react";
import emitter from "./eventbus/index";
class User extends React.Component {
constructor() {
super();
this.state = {};
}
pub() {
emitter.emit("hello", { name: "yuan", age: 18 });
}
render() {
return (
<div>
<button onClick={() => this.pub()}>发布</button>
</div>
);
}
}
export default User;
{/*Home组件*/}
import React from "react";
import emitter from "./eventbus/index";
class Home extends React.Component {
constructor() {
super();
this.state = {};
}
render() {
return (
<div>
<h2>home</h2>
</div>
);
}
componentDidMount() {
emitter.on("hello", (value) => {
console.log(value);
});
}
}
export default Home;
9.setState的使用
(1) react18,setState设计为异步的,可以显著提高性能,如果每次调用setState都进行一次更新,那么render函数会被频繁调用,界面都要重新渲染,这样的效率是很低的,最好的办法就是获取到多个更新,之后进行批量更新。
(2) setState调用的三种方式:
import React from "react";
class App extends React.Component {
constructor() {
super();
this.state = {
message: "hello world",
};
}
btn1Click() {
// 1.普通调用
// this.setState({ message: "hello react" });
// 2.可以传入一个回调函数:可以在回调函数中写新的state逻辑;可以将之前的state和props传递进来
// this.setState((state, props) => {
// console.log(state, props);
// return {
// message: "hello react",
// };
// });
// 3.setState在react事件中是一个异步调用,如果希望在更新state后可以获取对应的结果,可以传入第二个参数callback
this.setState({ message: "hello react" }, () => {
console.log(this.state.message);
});
}
render() {
const { message } = this.state;
return (
<div>
<h2>{message}</h2>
<button onClick={() => this.btn1Click()}>按钮1</button>
</div>
);
}
}
export default App;
(3) 在react18之前,在组件的生命周期和合成事件中setState是异步的;在定时器或者原生的DOM事件中,setState是同步的。
(4) 如果我们希望代码同步,则需要执行特殊的flushSync操作
import { flushSync } from "react-dom";
flushSync(() => {
this.setState({ message: "hello react" });
});
console.log(this.state.message);
10.React性能优化SCU
(1) React渲染机制:jsx –> 虚拟DOM –> 真实DOM
(2) React更新机制:props/state改变 –> render函数重新执行 –> 产生新的DOM树 –> 新旧DOM树进行对比 –> 计算出差异进行更新 –> 更新到真实的DOM
(3) 在我们的开发中,我们只要修改的App中的数据,所有的组件都需要重新进行render,进行diff算法,性能必然会很低;事实上,我们很多的组件不需要重新render,它们调用render必然有一个前提,就是依赖的数据(state或者props)发生改变时,再调用自己的render方法。那么如何来控制render方法是否被调用呢?可以通过shouldComponentUpdate()。
(4) shouldComponentUpdate:
-
该方法有两个参数:
- 参数一:nextProps:最新的props属性
- 参数二:nextState:最新的state属性
-
该方法返回值是一个Boolean类型:
- 返回值为true,那么就需要调用render方法;
- 返回值为false,那么就不需要调用render方法;
- 默认返回的是true,也就是只要state发生改变,就会调用render方法。
shouldComponentUpdate(newProps, nextState) {
// 自己对比state是否发生改变: this.state和nextState
if (this.props.message !== newProps.message) {
return true
}
return false
}
(5) 如果所有的类都需要我们手动的实现shouldComponentUpdate会增加很多的工作量,react已经考虑到了这一点,所以react默认帮我们实现好了,我们只需要将class继承自PureComponent
{/*App组件*/}
import { PureComponent } from "react";
import Home from "./Home";
import Profile from "./Profile";
class App extends PureComponent {
constructor() {
super();
this.state = {
message: "hello world",
};
}
changeText() {
this.setState({ message: "hello home" });
}
render() {
const { message } = this.state;
return (
<>
<Home message={message}></Home>
<Profile></Profile>
<button onClick={() => this.changeText()}>修改home文本</button>
</>
);
}
}
export default App;
{/*Home组件*/}
import { PureComponent } from "react";
class Home extends PureComponent {
constructor(props) {
super(props);
this.state = {};
}
render() {
console.log("home render");
const { message } = this.props;
return (
<>
<h2>{message}</h2>
</>
);
}
}
export default Home;
{/*Profile组件*/}
import { PureComponent } from "react";
class Profile extends PureComponent {
constructor() {
super();
this.state = {};
}
render() {
console.log("Profile render");
return (
<div>
<h2>Profile</h2>
</div>
);
}
}
export default Profile;
(6) 针对类组件我们使用的是PureComponent,那么对于函数式组件我们使用的是高阶函数memo
import { memo } from "react"
const Profile = memo(function(props) {
console.log("profile render")
return <h2>Profile: {props.message}</h2>
})
export default Profile
11.ref获取DOM和组件
(1) 创建ref来获取对应的DOM,目前有三种方式:
- 方式一:传入一个字符串,使用this.refs.传入的字符串获取;
- 方式二:传入一个对象,对象是通过React.createRef()方式创建出来的,使用时获取到创建对象其中有一个current属性就是对应的元素;
- 方式三:传入一个函数:该函数会在DOM被挂载时进行回调,这个函数会传入一个元素对象,我们可以自己保存,使用时直接拿到之前保存的元素对象即可。
import React, { PureComponent, createRef } from "react";
class App extends PureComponent {
constructor() {
super();
this.state = {};
this.titleRef = createRef();
this.titleEl = null;
}
getNativeDom() {
// 1.方式一:在React元素上绑定一个ref字符串 (不推荐)
// console.log(this.refs.yuan);
// 2.提前创建好ref对象,createRef(),将创建出来的对象绑定到元素
// console.log(this.titleRef.current);
// 3.方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入
console.log(this.titleEl);
}
render() {
return (
<div>
<h1 ref="yuan">hello world</h1>
<h2 ref={this.titleRef}>hello yuan</h2>
<h3 ref={e => this.titleEl = e}>hello title</h3>
<button onClick={(e) => this.getNativeDom()}>获取DOM</button>
</div>
);
}
}
export default App;
(2) ref获取组件实例:
import React, { PureComponent, createRef } from "react";
class HelloWorld extends PureComponent {
test() {
console.log("test...");
}
render() {
return (
<div>
<h2>hell world</h2>
</div>
);
}
}
class App extends PureComponent {
constructor() {
super();
this.state = {};
this.hwRef = createRef();
}
getComponent() {
console.log(this.hwRef.current);
this.hwRef.current.test();
}
render() {
return (
<div>
<HelloWorld ref={this.hwRef}></HelloWorld>
<button onClick={(e) => this.getComponent()}>获取组件实例</button>
</div>
);
}
}
export default App;
(3) 函数式组件获取DOM:forwardRef
import React, { PureComponent, createRef, forwardRef } from "react";
const HelloWorld = forwardRef(function (props, ref) {
return (
<div>
<h1 ref={ref}>hello world</h1>
<p>啦啦啦</p>
</div>
);
});
class App extends PureComponent {
constructor() {
super();
this.state = {};
this.hwRef = createRef();
}
getComponent() {
console.log(this.hwRef.current);
}
render() {
return (
<div>
<HelloWorld ref={this.hwRef}></HelloWorld>
<button onClick={(e) => this.getComponent()}>获取组件实例</button>
</div>
);
}
}
export default App;
12.受控组件和非受控组件
(1) 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存一些内部的state。
-
比如下面的HTML表单元素:
这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
在React中,并没有禁止这个行为,它依然是有效的;
但是通常情况下会使用js函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
实现这种效果的标准方式是使用”受控组件”。
import React, { PureComponent } from "react";
class App extends PureComponent {
constructor() {
super();
this.state = {
username: "yuan",
};
}
inputChange(e) {
console.log(e.target.value);
this.setState({ username: e.target.value });
}
render() {
const { username } = this.state;
return (
<div>
{/* 受控组件 */}
<input
type="text"
value={username}
onChange={(e) => this.inputChange(e)}
/>
{/* 非受控组件 */}
<input type="text" />
<h2>username: {username}</h2>
</div>
);
}
}
export default App;
(2) 受控组件:
-
在HTML中,表单元素(如input,textarea和select)之类的表单元素通常自己维护state,并根据用户的输入进行更新;
-
在React中,可变状态通常保存在组件的state属性中,并且只能通过setState()来更新;
我们将两者结合起来,使React的state成为唯一的数据源;渲染表单的React组件还控制着用户输入过程中表单发生的操作;被React以这种方式控制取值的表单输入元素就叫做”受控组件”。
-
由于在表单元素上设置了value属性,因此显示的值始终为this.state.value,这使得React的state成为唯一的数据源;
-
由于inputChange在每次按键时都会执行并更新React的state,因此显示的值将随着用户输入而更新。
(3) 非受控组件:
-
React推荐大多数情况下使用受控组件来处理表单数据:
在受控组件中,表单数据是由React组件来管理的;另一种替代方案是使用非受控组件,这时表单数据将交由DOM节点来处理;
-
如果要使用非受控组件中的数据,那么我们需要使用ref来从DOM节点中获取表单数据;
-
在非受控组件中通常使用defaultValue来设置默认值;
-
checkbox、radio支持defaultChecked,select和textarea支持defaultValue。
import React, { PureComponent, createRef } from "react";
class App extends PureComponent {
constructor() {
super();
this.state = {
username: "",
password: "",
checked: false,
hobbies: [
{ value: "sing", text: "唱", isChecked: false },
{ value: "dance", text: "跳", isChecked: false },
{ value: "rap", text: "rap", isChecked: false },
],
fruit: ["orange"],
info: "哈哈哈",
};
this.info = createRef();
}
submitClick(e) {
// 1.阻止默认行为
e.preventDefault();
// 2.获取到所有表单的数据
console.log(
this.state.username,
this.state.password,
this.state.checked,
this.state.fruit,
this.state.info
);
const hobbies = this.state.hobbies
.filter((item) => item.isChecked)
.map((item) => item.value);
console.log("获取的爱好:" + hobbies);
// 3.发送网络请求
}
// inputChange(e) {
// this.setState({ username: e.target.value });
// }
inputChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
checkboxChange(e) {
this.setState({ checked: e.target.checked });
}
hobbiesChange(e, index) {
const hobbies = [...this.state.hobbies];
hobbies[index].isChecked = e.target.checked;
this.setState({ hobbies });
}
fruitChange(e) {
// console.log(e.target.selectedOptions);
const options = Array.from(e.target.selectedOptions);
const values = options.map((item) => item.value);
this.setState({ fruit: values });
}
render() {
const { username, password, checked, hobbies, fruit, info } = this.state;
return (
<div>
<form onSubmit={(e) => this.submitClick(e)}>
{/* input */}
<label htmlFor="username">
用户:
<input
id="username"
type="text"
name="username"
value={username}
onChange={(e) => this.inputChange(e)}
/>
</label>
<label htmlFor="password">
密码:
<input
id="password"
type="password"
name="password"
value={password}
onChange={(e) => this.inputChange(e)}
/>
</label>
{/* checkbox单选 */}
<label htmlFor="argee">
<input
type="checkbox"
id="argee"
checked={checked}
onChange={(e) => this.checkboxChange(e)}
/>
</label>
{/* checkbox多选 */}
<div>
您的爱好:
{hobbies.map((item, index) => {
return (
<label htmlFor={item.value} key={item.value}>
<input
type="checkbox"
id={item.value}
checked={item.isChecked}
onChange={(e) => this.hobbiesChange(e, index)}
/>
<span>{item.text}</span>
</label>
);
})}
{/* select */}
<select
value={fruit}
onChange={(e) => this.fruitChange(e)}
multiple
>
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
</div>
{/* 非受控组件 */}
<input type="text" defaultValue={info} ref={this.info} />
<button type="submit">注册</button>
</form>
</div>
);
}
}
export default App;
13.高阶组件
在学习高阶组件之前,我们先回顾一下高阶函数,什么是高阶函数呢?
高阶函数的维基百科定义:至少满足以下条件之一:
接受一个或多个函数作为输入;
输出一个函数。
js中常见的filter、map、reduce都是高阶函数。
那么什么是高阶组件呢?
官方定义:高阶组件是参数为组件,返回值为新组件的函数。
我们可以进行如下的解析:
首先,高阶函数本身不是一个组件,而是一个函数;
其次,这个函数的参数是一个组件,返回值也是一个组件。
(1) 高阶组件的定义:
定义类组件返回
// 定义一个高阶组件
// 1.高阶组件会接收一个组件作为参数
function hoc(Cpn) {
class NewCpn extends PureComponent {
}
// 2.并且返回一个新的组件
return NewCpn
}
定义函数组件返回
// 定义一个高阶组件
// 1.高阶组件会接收一个组件作为参数
function hoc(Cpn) {
function NewCpn() {
}
// 2.并且返回一个新的组件
return NewCpn
}
(2) 高阶组件的编写和调用过程:
import React, { PureComponent } from "react";
// 定义一个高阶组件
function hoc(Cpn) {
// 1.定义一个类组件
class NewCpn extends PureComponent {
render() {
return <Cpn></Cpn>;
}
}
return NewCpn;
}
class HelloWorld extends PureComponent {
render() {
return <h1>hello world</h1>;
}
}
const HelloWorldHOC = hoc(HelloWorld);
class App extends PureComponent {
render() {
return (
<div>
<HelloWorldHOC></HelloWorldHOC>
</div>
);
}
}
export default App;
(3) 组件的名称可以通过displayName来修改;高阶组件并不是React Api的一部分,它是基于React的组合特性而形成的设计模式。
(4) 高阶组件的应用场景:
应用一:props增强
import React, { PureComponent } from "react";
function enhancedProps(Cpn) {
class NewCpn extends PureComponent {
constructor(props) {
super(props);
this.state = {
name: "yuan",
age: 18,
};
}
render() {
return <Cpn {...this.state} {...this.props}></Cpn>;
}
}
return NewCpn;
}
export default enhancedProps;
import React, { PureComponent } from "react";
import enhancedProps from "./hoc/enhanced_props";
class Home extends PureComponent {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div>
<h2>
{this.props.name}-{this.props.age}-{this.props.sex}
</h2>
</div>
);
}
}
const HomeEnhancedProps = enhancedProps(Home);
class App extends PureComponent {
constructor() {
super();
this.state = {};
}
render() {
return (
<div>
<HomeEnhancedProps sex="男"></HomeEnhancedProps>
</div>
);
}
}
export default App;
应用二:共享context
import React from "react";
const theme = React.createContext();
export default theme;
import ThemeContext from "../context/theme";
function withTheme(Cpn) {
return (props) => {
return (
<ThemeContext.Consumer>
{(value) => {
return <Cpn {...value} {...props}></Cpn>;
}}
</ThemeContext.Consumer>
);
};
}
export default withTheme;
import React, { PureComponent } from "react";
import withTheme from "./hoc/with_theme";
import ThemeContext from "./context/theme";
class Product extends PureComponent {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { color, size, name } = this.props;
return (
<div>
<h2>
{color} - {size} - {name}
</h2>
</div>
);
}
}
const ProductEnhanced = withTheme(Product);
class App extends PureComponent {
constructor() {
super();
this.state = {};
}
render() {
return (
<div>
<ThemeContext.Provider value={{ color: "red", size: "30px" }}>
<ProductEnhanced name="yuan"></ProductEnhanced>
</ThemeContext.Provider>
</div>
);
}
}
export default App;
应用三:渲染判断鉴权
function loginAuth(Cpn) {
return (props) => {
const token = localStorage.getItem("token");
if (token) {
return <Cpn {...props}></Cpn>;
} else {
return <h2>请先登录</h2>;
}
};
}
export default loginAuth;
import React, { PureComponent } from "react";
import loginAuth from "./hoc/loginAuth";
class About extends PureComponent {
constructor() {
super();
this.state = {};
}
render() {
return (
<div>
<h2>about</h2>
</div>
);
}
}
const AboutEnhanced = loginAuth(About);
class App extends PureComponent {
constructor() {
super();
this.state = {};
// localStorage.setItem("token", "token");
}
render() {
return <AboutEnhanced></AboutEnhanced>;
}
}
export default App;
应用四:劫持生命周期
14.Portals的使用
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的);
Portals提供了一种将子节点渲染到存在于父组件以外的DOM节点的优秀的方案:
- 第一个参数是任何可渲染的React的子元素,例如一个元素、字符串或fragment;
- 第二个参数是一个DOM元素。
React.createPortals(child,container)
通常来讲,当你从组件的render方法返回一个元素时,该元素将被挂载到DOM节点中离其最近的父节点;
然而,有时候将子元素插入到DOM节点中的不同位置也是有好处的。
import React, { PureComponent } from "react";
import { createPortal } from "react-dom";
class App extends PureComponent {
render() {
return (
<div className="app">
<h1>app h1</h1>
{createPortal(<h2>app h2</h2>, document.querySelector("#yuan"))}
</div>
);
}
}
export default App;
15.Fragment
在之前的开发中,我们总是在一个组件返回内容时包裹一个div,我们希望可以不渲染这个div可以怎么做呢?
使用Fragment,Fragment允许你将子列表分组,而无需向DOM添加额外的节点;
Fragment的短语法:<></>,但是我们需要在Fragment添加key就不能使用短语法。
16.strictMode
严格默认:
-
识别不安全的生命周期函数;
-
检测过时的ref API的使用;
-
检查以外的副作用:
- 这个组件的constructor会被调用两次;
- 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;
- 在生产环境中,是不会被调用两次的;
-
检测使用废弃的findDOMNode方法:在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
-
检测过时的context API:早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的,目前这种方式已经不推荐使用
八、React动画的实现
1.react-transition-group的介绍
- 在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验。
- 当然,我们可以通过原生的CSS来实现这些过渡动画,但是React社区为我们提供了react-transition-group用来完成过渡动画。
- React曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-
group。
-
这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装:npm install react-transition-group
-
react-transition-group主要组件:
- Transition:该组件是一个和平台无关的组件(不一定要结合CSS);在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition;
- CSSTransition:在前端开发中,通常使用CSSTransition来完成过渡动画效果;
- SwitchTransition:两个组件显示和隐藏切换时,使用该组件;
- TransitionGroup: 将多个动画组件包裹在其中,一般用于列表中元素的动画。
2.CSSTransition
-
CSSTransition是基于Transition组件构建的;
-
CSSTransition执行过程中,有三个状态:appear、enter、exit;
-
它们有三种状态,需要定义对应的CSS样式:
- 开始状态:对于的类是-appear、-enter、exit;
- 执行动画:对应的类是-appear-active、-enter-active、-exit-active;
- 执行结束:对应的类是-appear-done、-enter-done、-exit-done;
-
CSSTransition常见对应的属性:
-
in:触发进入或者退出状态:
-
如果添加了unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉;
-
当in为true时,触发进入状态,会添加-enter、-enter-acitve的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
-
当in为false时,触发退出状态,会添加-exit、-exit-active的class开始执行动画,当动画执行结束后,会移除两个class,并
且添加-enter-done的class;
-
-
classNames:动画class的名称,决定了在编写css时,对应的class名称:比如card-enter、card-enter-active、card-enter-done;
-
timeout:过渡动画的时间
-
appear:是否在初次进入添加动画(需要和in同时为true)
-
unmountOnExit:退出后卸载组件
-
其他属性可以参考官网来学习:reactcommunity.org/react-trans…
-
CSSTransition对应的钩子函数:主要为了检测动画的执行过程,来完成一些JavaScript的操作
- onEnter:在进入动画之前被触发;
- onEntering:在应用进入动画时被触发;
- onEntered:在应用进入动画结束后被触发;
-
.yuan-appear,
.yuan-enter {
opacity: 0;
}
.yuan-appear-active,
.yuan-enter-active {
opacity: 1;
transition: opacity 2s ease;
}
.yuan-exit {
opacity: 1;
}
.yuan-exit-active {
opacity: 0;
transition: opacity 2s ease;
}
import React, { PureComponent, createRef } from "react";
import { CSSTransition } from "react-transition-group";
import "./App.css";
class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
isShow: true,
};
this.sectionRef = createRef();
}
render() {
const { isShow } = this.state;
return (
<div>
<button onClick={(e) => this.setState({ isShow: !isShow })}>
切换
</button>
<CSSTransition
nodeRef={this.sectionRef}
in={isShow}
unmountOnExit={true}
timeout={2000}
classNames="yuan"
appear
onEnter={(e) => console.log("开始进入动画")}
onEntering={(e) => console.log("执行进入动画")}
onEntered={(e) => console.log("执行进入结束")}
onExit={(e) => console.log("开始离开动画")}
onExiting={(e) => console.log("执行离开动画")}
onExited={(e) => console.log("执行离开结束")}
>
<div className="section" ref={this.sectionRef}>
<h2>哈哈哈</h2>
<p>我是内容,哈啊哈哈</p>
</div>
</CSSTransition>
</div>
);
}
}
export default App;
3.SwitchTransition
-
SwitchTransition可以完成两个组件之间切换的炫酷动画:
- 比如我们有一个按钮需要在on和off之间切换,我们希望看到on先从左侧退出,off再从右侧进入;
- 这个动画在vue中被称之为 vue transition modes;
- react-transition-group中使用SwitchTransition来实现该动画;
-
SwitchTransition中主要有一个属性:mode,有两个值:
- in-out:表示新组件先进入,旧组件再移除;
- out-in:表示就组件先移除,新组建再进入;
-
如何使用SwitchTransition呢?
-
SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件;
-
SwitchTransition里面的CSSTransition或Transition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是
key属性;
-
.login-enter {
opacity: 0;
transform: translateX(100px);
}
.login-enter-active {
transform: translateX(0);
opacity: 1;
transition: all 1s ease;
}
.login-exit {
transform: translateX(0);
opacity: 1;
}
.login-exit-active {
transform: translateX(-100px);
opacity: 0;
transform: all 1s ease;
}
import React, { PureComponent } from "react";
import { CSSTransition, SwitchTransition } from "react-transition-group";
import "./App.css";
class App extends PureComponent {
constructor() {
super();
this.state = {
isLogin: true,
};
}
render() {
const { isLogin } = this.state;
return (
<div>
<SwitchTransition mode="out-in">
<CSSTransition
key={isLogin ? "exit" : "login"}
classNames="login"
timeout={1000}
>
<button onClick={(e) => this.setState({ isLogin: !isLogin })}>
{isLogin ? "退出" : "登录"}
</button>
</CSSTransition>
</SwitchTransition>
</div>
);
}
}
export default App;
4.CSSTransition
当我们有一组动画时,需要将这些CSSTransition放入到一个TransitionGroup中来完成动画:
.book-enter {
transform: translateX(150px);
opacity: 0;
}
.book-enter-active {
transform: translateX(0);
opacity: 1;
transition: all 1s ease;
}
.book-exit {
transform: translateX(0);
opacity: 1;
}
.book-exit-active {
transform: translateX(150px);
opacity: 0;
transition: all 1s ease;
}
import React, { PureComponent } from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import "./App.css";
export class App extends PureComponent {
constructor() {
super();
this.state = {
books: [
{ id: 111, name: "你不知道JS", price: 99 },
{ id: 222, name: "JS高级程序设计", price: 88 },
{ id: 333, name: "Vuejs高级设计", price: 77 },
],
};
}
addNewBook() {
const books = [...this.state.books];
books.push({
id: new Date().getTime(),
name: "React高级程序设计",
price: 99,
});
this.setState({ books });
}
removeBook(index) {
const books = [...this.state.books];
books.splice(index, 1);
this.setState({ books });
}
render() {
const { books } = this.state;
return (
<div>
<h2>书籍列表:</h2>
<TransitionGroup component="ul">
{books.map((item, index) => {
return (
<CSSTransition key={item.id} classNames="book" timeout={1000}>
<li>
<span>
{item.name}-{item.price}
</span>
<button onClick={(e) => this.removeBook(index)}>删除</button>
</li>
</CSSTransition>
);
})}
</TransitionGroup>
<button onClick={(e) => this.addNewBook()}>添加新书籍</button>
</div>
);
}
}
export default App;
九、React中的CSS的编写方式
1.React中CSS的概述
前面说过,整个前端已经是组件化的天下,而CSS的设计就不是为组件化而生的,所以在目前组件化的框架中都在寻找一种合适的编写CSS的解决方案。
在组件化中选择合适的CSS解决方案应该符合以下条件:
可以编写局部css: 组件内部的css具备自己的局部作用域,不会随意污染其他组件内的元素;
可以编写动态的css: 可以获取当前组件的一些状态,根据状态的变化生成不同的css样式;
支持所有的css特性: 伪类、动画、媒体查询等;
编写起来简洁方便、最好符合一贯的css风格特点;
等等…
2.React中的CSS
事实上,css一直是React的痛点,也是被很多开发者吐槽、诟病的一个点。
在这一点上,Vue做的要好于React:
- Vue通过在.vue文件中编写 <style> 标签来编写自己的样式;
- 通过是否添加 scoped 属性来决定编写的样式是全局有效还是局部有效;
- 通过 lang 属性来设置你喜欢的 less、sass等预处理器;
- 通过内联样式风格的方式来根据最新状态设置和改变css;
- 等等…
Vue在CSS上虽然不能称之为完美,但是已经足够简洁、自然、方便了,至少统一的样式风格不会出现多个开发人员、多个项目采用不一样的样式风格。
相比而言,React官方并没有给出在React中统一的样式风格:
- 由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库;
- 大家一致在寻找最好的或者说最适合自己的CSS方案,但是到目前为止也没有统一的方案;
3.内联样式的写法
(1) 内联样式是官方推荐的一种css样式的写法:
- style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
- 并且可以引用state中的状态来设置相关的样式;
(2) 内联样式的优点:
- 样式之间不会有冲突;
- 可以动态获取当前state中的状态
(3) 内联样式的缺点:
- 写法上都需要使用驼峰标识;
- 某些样式没有提示;
- 大量的样式, 代码混乱;
- 某些样式无法编写(比如伪类/伪元素)
所以官方依然是希望内联合适和普通的css来结合编写。
4.普通的CSS
普通的css我们通常会编写到一个单独的文件,之后再进行引入。
这样的编写方式和普通的网页开发中编写方式是一致的:
- 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
- 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
- 但是普通的css都属于全局的css,样式之间会相互影响;
这种编写方式最大的问题是样式之间会相互层叠掉;
4.css modules
css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的。如果在其他项目中使用它,那么我们需要自己来进行配置,比如配置webpack.config.js中的modules: true等。
React的脚手架已经内置了css modules的配置:.css/.less/.scss 等样式文件都需要修改成 .module.css/.module.less/.module.scss 等;之后就可以引用并且进行使用了;
css modules确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案。
这种方案也有自己的缺陷:
- 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的;
- 所有的className都必须使用{style.className} 的形式来编写;
- 不方便动态来修改某些样式,依然需要使用内联样式的方式;
如果你觉得上面的缺陷还算OK,那么你在开发中完全可以选择使用css modules来编写,并且也是在React中很受欢迎的一种方式。
.title {
font-size: 32px;
color: green;
}
.content {
font-size: 22px;
color: orange;
}
import React, { PureComponent } from 'react'
import Home from './home/Home'
import Profile from './profile/Profile'
import appStyle from "./App.module.css"
export class App extends PureComponent {
render() {
return (
<div>
<h2 className={appStyle.title}>我是标题</h2>
<p className={appStyle.content}>我是内容, 哈哈哈哈</p>
<Home/>
<Profile/>
</div>
)
}
}
export default App
5.less
6.css in js
(1) 认识css in js:
“CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义;注意此功能并不是 React 的一部分,而是由第三方库提供;
事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态;
(2) 目前比较流行的CSS-in-JS的库有哪些呢?
styled-components、emotion、glamorous
目前可以说styled-components依然是社区最流行的CSS-in-JS库,所以我们以styled-components为主;
(3) 安装styled-components:npm i styled-components@5.3.5
(4) ES6模板字符串:
ES6中增加了模板字符串的语法,这个对于很多人来说都会使用。
但是模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。
正常情况下,我们都是通过 函数名() 方式来进行调用的,其实函数还有另外一种调用方式,就是标签模板字符串。
function foo(...args) {
console.log(args);
}
const yuan = "yuan";
foo(1, 2, 3); //[ 1, 2, 3 ]
foo`hello world`; //[ [ 'hello world' ] ]
foo`hello ${yuan}`; // [ [ 'hello ', '' ], 'yuan' ]
在styled-component中,就是通过这种方式来解析模块字符串,最终生成我们想要的样式的
(5) styled-components的使用
import styled from "styled-components";
// 1.基本使用
export const AppWrapper = styled.div`
.footer {
border: 1px red solid;
}
`;
export const SectionWrapper = styled.div.attrs((props) => ({
tColor: props.color || "blue",
}))`
border: 1px yellow solid;
.title {
font-size: ${(props) => props.size}px;
color: ${(props) => props.tColor};
}
`;
import React, { PureComponent } from "react";
import { AppWrapper, SectionWrapper } from "./style";
class App extends PureComponent {
constructor() {
super();
this.state = {
size: 30,
color: "red",
};
}
render() {
const { size, color } = this.state;
return (
<AppWrapper>
<SectionWrapper size={size} color={color}>
<h2 className="title">我是h2</h2>
<p className="content">我是p</p>
</SectionWrapper>
<div className="footer">
<p>免责声明</p>
<p>版权声明</p>
</div>
</AppWrapper>
);
}
}
export default App;
7.React中添加class
使用classnames库
<h2 className={`aaa ${isbbb ? 'bbb': ''} ${isccc ? 'ccc': ''}`}>哈哈哈</h2>
<h2 className={classname}>呵呵呵</h2>
<h2 className={classNames("aaa", { bbb:isbbb, ccc:isccc })}>嘿嘿嘿</h2>
<h2 className={classNames(["aaa", { bbb: isbbb, ccc: isccc }])}>嘻嘻嘻</h2>
十、redux
1.为什么需要redux?
(1) JavaScript开发的应用程序,已经变得越来越复杂了:
-
JavaScript需要管理的状态越来越多,越来越复杂;
-
这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示
加载动效,当前分页;
(2) 管理不断变化的state是非常困难的:
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
- 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
(3) React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:
- 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;
- React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定;
(4) Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;
(5) Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)
2.redux的核心理念
(1) store:
-
Redux的核心理念非常简单;
-
比如我们有一个朋友列表需要共享到多个页面进行管理:
- 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;
- 比如页面的某处通过friends.push的方式增加了一条数据;
- 比如另一个页面通过friends[0].age = 25修改了一条数据
-
整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化;
因此我们可以定义一些数据, 将数据存放在store中供其他页面使用
(2) action:
-
Redux要求我们通过action来更新数据;
所有数据的变化,必须通过派发(dispatch)action来更新;
action是一个普通的JavaScript对象,用来描述这次更新的type和content;
强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;
当然,目前我们的action是固定的对象;
真实应用中,我们会通过函数来定义,返回一个action;
(3) reducer:
-
如何将state和action联系在一起呢? 答案就是通过reducer
reducer是一个纯函数;
reducer做的事情就是将传入的state和action结合起来生成一个新的state;
3.redux的三大原则
(1) 单一数据源:
整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:
Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;
单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;
(2) state是只读的:
唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:
这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;
(3) 使用纯函数来执行修改:
通过reducer将旧state和actions联系在一起,并且返回一个新的State:
随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;
但是所有的reducer都应该是纯函数,不能产生任何的副作用;
5.redux的基本使用过程
(1) redux测试项目的搭建:
-
创建一个新的项目文件夹:learn-redux
- 执行初始化操作 npm init -y 或 yarn init -y
- 安装redux: npm i redux 或 yarn add redux
-
创建src目录,在src目录下创建一个store文件夹,并且在该文件夹下创建index.js文件
-
修改package.json用于执行index.js,也可以不配置,直接使用node命令运行
(2) redux的基本使用步骤:
-
创建一个对象,作为我们要保存的状态state
-
创建一个store来存储这个state:
- 创建store时必须创建reducer;
- 我们可以通过store.getState()来访问state;
-
通过action来修改state:
- 通过dispatch来派发action;
- 通常action中都会有type属性,
-
修改reducer中的处理代码,reducer是一个纯函数
-
可以在派发action之前,监听store的变化
// index.js
const { createStore } = require("redux");
// 初始化数据
const initialState = {
name: "yuan",
age: 18,
};
// 定义reducer函数:纯函数
// 两个参数:
// 参数一:store中目前保存的state
// 参数二:本次需要更新的action
// 返回值:它的返回值会作为之后存储的state
function reducer(state = initialState, action) {
switch (action.type) {
case "change_name":
return { ...state, name: action.name };
case "change_age":
return { ...state, age: action.age };
default:
return state;
}
}
// 创建store
const store = createStore(reducer);
module.exports = store;
// test.js
const store = require("./store/index copy");
// 使用state中的数据
console.log(store.getState());
// 修改state中的数据
const nameAction = { type: "change_name", name: "xiao" };
store.dispatch(nameAction);
const ageAction = { type: "change_age", age: 23 };
store.dispatch(ageAction);
console.log(store.getState());
(3) redux结构的划分
-
index.js
const { createStore } = require("redux"); const reducer = require("./reducer"); const store = createStore(reducer); module.exports = store;
-
constants.js
const CHANGE_NAME = "change_name"; const CHANGE_AGE = "change_age"; module.exports = { CHANGE_NAME, CHANGE_AGE, };
-
reducer.js
const { CHANGE_NAME, CHANGE_AGE } = require("./constants"); const initialState = { name: "yuan", age: 18, }; function reducer(state = initialState, action) { switch (action.type) { case CHANGE_NAME: return { ...state, name: action.name }; case CHANGE_AGE: return { ...state, age: action.age }; default: return state; } } module.exports = reducer;
-
actionCreator.js
const { CHANGE_NAME, CHANGE_AGE } = require("./constants"); const changeNameAction = (name) => ({ type: CHANGE_NAME, name, }); const changeAgeAction = (age) => ({ type: CHANGE_AGE, age, }); module.exports = { changeNameAction, changeAgeAction, };
6.在React中直接使用redux
开始之前需要强调一下,redux和react没有直接的关系,你完全可以在React, Angular, Ember, jQuery, or vanilla JavaScript中使用Redux。
尽管这样说,redux依然是和React库结合的更好,因为他们是通过state函数来描述界面的状态,Redux可以发射状态的更新, 让他们作出相应; 目前redux在react中使用是最多的,所以我们需要将之前编写的redux代码,融入到react当中去。
这里我创建了两个组件:
Home组件:其中会展示当前的counter值,并且有一个+1和+5的按钮;
Profile组件:其中会展示当前的counter值,并且有一个-1和-5的按钮;
(1) 安装redux,创建store
store/index.js
import reducer from "./reducer";
const { createStore } = require("redux");
const store = createStore(reducer);
export default store;
store/constants.js
export const ADD_NUM = "add_num";
export const SUB_NUM = "sub_num";
store/reducer.js
import { ADD_NUM, SUB_NUM } from "./constants";
const initialState = {
counter: 100,
};
function reducer(state = initialState, action) {
switch (action.type) {
case ADD_NUM:
return { ...state, counter: state.counter + action.num };
case SUB_NUM:
return { ...state, counter: state.counter - action.num };
default:
return state;
}
}
export default reducer;
store/actionCreator.js
import { ADD_NUM, SUB_NUM } from "./constants";
export const addNumAction = (num) => ({
type: ADD_NUM,
num,
});
export const subNumAction = (num) => ({
type: SUB_NUM,
num,
});
(2) Home组件:
import React, { PureComponent } from "react";
import store from "../store";
import { addNumAction } from "../store/actionCreator";
class Home extends PureComponent {
constructor() {
super();
this.state = {
counter: store.getState().counter,
};
}
// 核心一:在componentDidMount中监听store的变化,当数据发生变化时重新设置counter
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
this.setState({ counter: state.counter });
});
}
// 核心二:当发生点击事件时,调用store的dispatch来派发对应的action
addNum(num) {
store.dispatch(addNumAction(num));
}
render() {
const { counter } = this.state;
return (
<>
<h2>home</h2>
<h2>{counter}</h2>
<button onClick={(e) => this.addNum(5)}>+5</button>
</>
);
}
}
export default Home;
(3) Profile组件
import React, { PureComponent } from "react";
import store from "../store";
import { subNumAction } from "../store/actionCreator";
class Profile extends PureComponent {
constructor() {
super();
this.state = {
counter: store.getState().counter,
};
}
// 核心一:在componentDidMount中监听store的变化,当数据发生变化时重新设置counter
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
this.setState({ counter: state.counter });
});
}
// 核心二:当发生点击事件时,调用store的dispatch来派发对应的action
subNum(num) {
store.dispatch(subNumAction(num));
}
render() {
const { counter } = this.state;
return (
<>
<h2>Profile</h2>
<h2>{counter}</h2>
<button onClick={(e) => this.subNum(5)}>-5</button>
</>
);
}
}
export default Profile;
我们发现Home组件和Profile组件中的代码是大同小异的, 所以这不是我们最终编写的代码, 后面还会对代码进行优化。
7.通过react-redux库连接React和Redux
(1) 安装react-redux库:npm i react-redux
(2) 在index.js中为App根组件提供store:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./16_React-redux/App";
// 引入Provider和store
import { Provider } from "react-redux";
import store from "./16_React-redux/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
// 在Provider中声明要提供的store
<Provider store={store}>
<App />
</Provider>
);
(3) 在组件中使用react-redux提供的connect高阶函数将store与组件连接起来,connect接受两个参数,第一个参数的含义是将state映射到props,第二个参数的含义是将dispatch映射到props上。
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addNumAction } from "../store/actionCreator";
class About extends PureComponent {
addNum(num) {
this.props.changeNum(num);
}
render() {
const { counter } = this.props;
return (
<>
<h2>about</h2>
<h2>{counter}</h2>
<button onClick={(e) => this.addNum(6)}>+6</button>
</>
);
}
}
const mapStateToProps = (state) => ({
counter: state.counter,
});
const mapDispatchToProps = (dispatch) => ({
changeNum(num) {
dispatch(addNumAction(num));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(About);
8.redux中进行异步操作(网络请求)的方案
(1) 在之前简单的案例中,redux中保存的counter是一个本地定义的数据。
我们可以直接通过同步的操作来dispatch action,state就会被立即更新。
但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。
发生网络请求我们有两种方案, 可以直接在组件的钩子函数中发送网络请求, 再将数据存放到store中; 也可以直接在store中发生网络请求
(2) 组件中进行异步操作
网络请求可以在class组件的生命周期函数componentDidMount中发送。
(3) 安装redux-thunk库引入中间件:npm i redux-thunk
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer";
// 导入中间件
import thunk from "redux-thunk";
// 应用中间件
const store = createStore(reducer, applyMiddleware(thunk))
export default store
应用之后,
store.dispatch()
就可以派发函数了
(4) 定义一个返回函数的action
import { CHANGE_BANNERS, CHANGE_RECOMMENDS } from "./constants";
import axios from "axios";
export const changeBannersAction = (banners) => ({
type: CHANGE_BANNERS,
banners,
});
export const changeRecommendsAction = (recommends) => ({
type: CHANGE_RECOMMENDS,
recommends,
});
export const fetchHomeDataAction = () => {
return (dispatch, getState) => {
axios.get("http://123.207.32.32:8000/home/multidata").then((res) => {
console.log(res);
const banners = res.data.data.banner.list;
const recommends = res.data.data.recommend.list;
dispatch(changeBannersAction(banners));
dispatch(changeRecommendsAction(recommends));
});
};
};
自动执行action中的返回的函数时, 会传给这个函数一个dispatch函数和getState函数;
dispatch函数: 用于我们之后再次派发action;
getState函数: 考虑到我们之后的一些操作需要依赖原来的状态,调用getState函数可以让我们可以获取之前的一些状态;
我们就可以在返回的该函数中, 编写异步的网络请求相关代码
(5) 派发action
import React, { PureComponent } from "react";
import axios from "axios";
import { connect } from "react-redux";
import {
changeBannersAction,
changeRecommendsAction,
fetchHomeDataAction,
} from "../store/actionCreator";
class Category extends PureComponent {
constructor() {
super();
this.state = {};
}
// componentDidMount() {
// axios.get("http://123.207.32.32:8000/home/multidata").then((res) => {
// // console.log(res);
// const banners = res.data.banner.list;
// const recommends = res.data.recommend.list;
// this.props.changeBanners(banners);
// this.props.changeRecommends(recommends);
// });
// }
componentDidMount() {
this.props.fetchHomeData();
}
render() {
return (
<>
<h2>category</h2>
</>
);
}
}
const mapDispatchToprops = (dispatch) => ({
// changeBanners(banners) {
// dispatch(changeBannersAction(banners));
// },
// changeRecommends(recommends) {
// dispatch(changeRecommendsAction(recommends));
// },
fetchHomeData() {
dispatch(fetchHomeDataAction());
},
});
export default connect(null, mapDispatchToprops)(Category);
(6) 数据的展示
import React, { PureComponent } from "react";
import { connect } from "react-redux";
class Show extends PureComponent {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { banners, recommends } = this.props;
return (
<>
<h2>show</h2>
<div>
{banners.map((item) => {
return <h2 key={item.title}>{item.title}</h2>;
})}
</div>
<div>
{recommends.map((item) => {
return <h2 key={item.title}>{item.title}</h2>;
})}
</div>
</>
);
}
}
const mapStateToProps = (state) => ({
banners: state.banners,
recommends: state.recommends,
});
export default connect(mapStateToProps)(Show);
9.redux-devtools
利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等。
(1) 在对应的浏览器中安装相关的插件(比如chrome中在扩展商店搜索Redux DevTools);
(2) 在redux中继承devtools的中间件
import reducer from "./reducer";
import thunk from "redux-thunk";
import { createStore, applyMiddleware, compose } from "redux";
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose;
const enhancer = composeEnhancer(applyMiddleware(thunk));
const store = createStore(reducer, enhancer);
export default store;
10.reducer的拆分
将不同模块需要共享的数据拆分到不同的reducer里
import { createStore, compose, combineReducers } from "redux"
import { log, thunk, applyMiddleware } from "./middleware"
// import thunk from "redux-thunk"
import counterReducer from "./counter"
import homeReducer from "./home"
import userReducer from "./user"
// 正常情况下 store.dispatch(object)
// 想要派发函数 store.dispatch(function)
// 将两个reducer合并在一起
const reducer = combineReducers({
counter: counterReducer,
home: homeReducer,
user: userReducer
})
// combineReducers实现原理(了解)
// function reducer(state = {}, action) {
// // 返回一个对象, store的state
// return {
// counter: counterReducer(state.counter, action),
// home: homeReducer(state.home, action),
// user: userReducer(state.user, action)
// }
// }
// redux-devtools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
const store = createStore(reducer)
applyMiddleware(store, log, thunk)
export default store
store目录:
十一、redux-toolkit
1.redux toolkit的介绍
Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。
在前面我们学习Redux的时候应该已经发现,redux的编写逻辑过于的繁琐和麻烦。
并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);
Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题;
在很多地方为了称呼方便,也将之称为“RTK”;
2.安装Redux Toolkit:
npm install @reduxjs/toolkit react-redux
3.Redux Toolkit的核心API
configureStore: 包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供 的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。
createSlice: 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions。
createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分派动作类型的 thunk
4.Redux Toolkit基本使用
(1) 我们先对counter的reducer进行重构:通过createSlice创建一个slice
createSlice主要包含如下几个参数:
name:用户标记slice的名词,在之后的redux-devtools中会显示对应的名词;
initialState:初始化值,第一次初始化的值;
reducers:相当于之前的reducer函数:对象类型,可以添加很多的函数;函数类似于原来reducer中的case语句;函数的参数:第一个参数state,第二个参数action,action有两个属性,一个是自动生成的type,还有一个是传递的参数放在payload中
store/features/counter.js
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: {
counter: 888,
},
reducers: {
addNumber(state, { payload }) {
state.counter = state.counter + payload;
},
subNumber(state, { payload }) {
state.counter = state.counter - payload;
},
},
});
export const { addNumber, subNumber } = counterSlice.actions;
export default counterSlice.reducer;
(2) store的创建
configureStore用于创建store对象,常见参数如下:
reducer:将slice中的reducer组成一个对象传入此处;
middware:可以使用参数,传入其他的中间件;
devTools:是否配置devTools工具,默认为true
store/index.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counter";
const store = configureStore({
reducer: {
counter: counterReducer,
},
devTools: false,
});
export default store;
(3) index.js
import App from "./17_react-toolkit/App";
import { Provider } from "react-redux";
import store from "./17_react-toolkit/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
(4) About.jsx
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addNumber, subNumber } from "../store/features/counter";
class About extends PureComponent {
constructor() {
super();
this.state = {};
}
render() {
return (
<>
<h2>About</h2>
<h2>About counter: {this.props.counter}</h2>
<button
onClick={(e) => {
this.props.addNumber(1);
}}
>
+1
</button>
<button
onClick={(e) => {
this.props.subNumber(1);
}}
>
-1
</button>
</>
);
}
}
const mapStateToProps = (state) => ({
counter: state.counter.counter,
});
const mapDispatchToProps = (dispatch) => ({
addNumber(num) {
dispatch(addNumber(num));
},
subNumber(num) {
dispatch(subNumber(num));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(About);
5.Redux Toolkit异步操作
在之前的开发中,我们通过redux-thunk中间件让dispatch中可以进行异步操作, 其实Redux Toolkit工具包默认已经给我们集成了Thunk相关的功能, 我们可以通过createAsyncThunk
函数创建一个异步的action。
createAsyncThunk函数有参数:
参数一: 传入事件类型type
参数二: 传入一个函数, 该函数可以执行异步操作, 甚至可以直接传入一个异步函数
(1) 方式一:
在home.js中, 通过createAsyncThunk函数创建一个异步的action
再在extraReducers中监听这个异步的action的状态, 当他处于fulfilled状态时, 获取到网络请求的数据, 并修改原来state中的数据
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const fetchHomeDataAction = createAsyncThunk("fetch/home", async () => {
// 1.发送网络请求
const res = await axios.get("http://123.207.32.32:8000/home/multidata");
// 返回结果,那么action状态会变成fulfilled状态
return res.data;
});
const homeSlice = createSlice({
name: "home",
initialState: {
banners: [],
recommends: [],
},
reducers: {
changeBanners(state, { payload }) {
state.banners = payload;
},
changeRecommends(state, { payload }) {
state.recommends = payload;
},
},
// extraReducers中针对异步action, 监听它的状态
// extraReducers: {
// [fetchHomeDataAction.pending](state, action) {
// console.log("pending");
// },
// [fetchHomeDataAction.fulfilled](state, { payload }) {
// state.banners = payload.data.banner.list;
// },
// [fetchHomeDataAction.rejected](state, action) {
// console.log("reject");
// },
// },
extraReducers: (builder) => {
builder
.addCase(fetchHomeDataAction.pending, (state, action) => {
console.log("pending");
})
.addCase(fetchHomeDataAction.fulfilled, (state, { payload }) => {
state.banners = payload.data.banner.list;
});
},
});
export const { changeBanners, changeRecommends } = homeSlice.actions;
export default homeSlice.reducer;
componentDidMount() {
this.props.fetchHomeData();
}
const mapDispatchToProps = (dispatch) => ({
fetchHomeData() {
dispatch(fetchHomeDataAction());
},
});
方式二:
如果我们不想通过在extraReducers在监听状态, 再修改state这种方法的话, 还有另外的一种做法
我们创建的fetchHomeMultidataAction这个异步action是接受两个参数的
参数一, extraInfo: 在派发这个异步action时, 如果有传递参数, 会放在extraInfo里面
参数二, store: 第二个参数将store传递过来
这样我们获取到结果后, 通过dispatch修改store中的state, 无需再监听异步action的状态
export const fetchHomeDataAction = createAsyncThunk(
"fetch/home",
async (extraInfo, { dispatch, getState }) => {
console.log(extraInfo, dispatch, getState);
// 1.发送网络请求
const res = await axios.get("http://123.207.32.32:8000/home/multidata");
// 2.取出数据, 并且在此处直接dispatch操作(可以不做)
const banners = res.data.data.banner.list;
const recommends = res.data.data.recommend.list;
dispatch(changeBanners(banners));
dispatch(changeRecommends(recommends));
// 返回结果,那么action状态会变成fulfilled状态
// return res.data;
}
);
componentDidMount() {
this.props.fetchHomeData();
}
const mapDispatchToProps = (dispatch) => ({
fetchHomeData() {
dispatch(fetchHomeDataAction({ name: "yuan", age: 19 }));
},
});
不管是哪种方式, 都需要在页面的componentDidMount生命周期中, 通过派发异步的action发送网络请求
十二、react-router
1.认识react-router
目前前端流行的三大框架, 都有自己的路由实现:
Angular的ngRouter
React的ReactRouter
Vue的vue-router
React Router在最近两年版本更新的较快,并且在最新的React Router6.x版本中发生了较大的变化。
目前React Router6.x已经非常稳定,我们可以放心的使用;
说明一下, Router4.x和Router5.x的区别是不大的, 而Router6.x就有些区别, 所以Router系列的文章主要针对Router6.x进行讲解, 当有与4或5版本不同的地方时会单独强调
安装React Router:
安装时,我们选择安装react-router-dom,
npm install react-router-dom
;因为react-router会包含一些react-native的内容,web开发并不需要;
2.Router的组件API
react-router最主要的API是给我们提供的一些组件:
(1) BrowserRouter或HashRouter:
Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;
BrowserRouter使用history模式;
import React from "react"
import ReactDOM from "react-dom/client"
import { BrowserRouter } from "react-router-dom"
import App from "./App"
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(
<BrowserRouter>
<App/>
</BrowserRouter>
)
HashRouter使用hash模式
import React from "react"
import ReactDOM from "react-dom/client"
import { HashRouter } from "react-router-dom"
import App from "./App"
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(
<HashRouter>
<App/>
</HashRouter>
)
(2) Router的映射配置:
Routes:包裹所有的Route,在其中匹配一个路由
Router6.x使用的是Routes组件
Router5.x使用的是Switch组件
Route:Route用于路径的匹配
Router6.x不允许Router组件单独存在
path属性: 用于设置匹配到的路径;
element属性: 设置匹配到路径后,渲染的组件
Router5.x使用的是component属性
exact: 精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
- Router6.x不再支持该属性
<div className='app'>
<div className='header'>header</div>
<div className='counter'>
<Routes>
<Route path='/' element={<Home/>}/>
<Route path='/about' element={<About/>}/>
<Route path='/profile' element={<Profile/>}/>
</Routes>
</div>
<div className='footer'>footer</div>
</div>
(3) Router配置和跳转
Link组件:
通常路径的跳转是使用Link组件,这个组件最终会被渲染成a元素;
NavLink是在Link基础之上增加了一些样式属性(后续会讲解);
to属性: Link中最重要的属性,用于设置跳转到的路径
<div className='app'>
<div className='header'>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
</div>
<div className='counter'>
<Routes>
<Route path='/' element={<Home/>}/>
<Route path='/about' element={<About/>}/>
<Route path='/profile' element={<Profile/>}/>
</Routes>
</div>
<div className='footer'>footer</div>
</div>
NavLink组件:
需求:路径选中时,对应的a元素的文字变为红色
这个时候,我们要使用NavLink组件来替代Link组件, NavLink组件选中时, 有添加一个类(这个组件了解即可, 不如自己控制更方便):
事实上在默认匹配成功时,NavLink就会动态的添加上一个class:
active
, 选中的时候就会添加;所以我们也可以直接编写样式
当然,如果你担心这个class在其他地方被使用了,出现样式的层叠,也可以自定义class和动态添加样式:
style属性: 传入一个函数,函数的参数接收一个对象,该对象包含isActive属性
<NavLink to="/" style={({ isActive }) => ({color: isActive ? "red" : ""})}>首页</NavLink>
<NavLink to="/about" style={({ isActive }) => ({color: isActive ? "red" : ""})}>关于</NavLink>
<NavLink to="/profile" style={({ isActive }) => ({color: isActive ? "red" : ""})}>我的</NavLink>
className:传入一个函数,函数的参数接受一个对象,该对象包含isActive属性
<NavLink to="/" className={({ isActive }) => isActive ? "my-class" : ""}>首页</NavLink>
<NavLink to="/about" className={({ isActive }) => isActive ? "my-class" : ""}>关于</NavLink>
<NavLink to="/profile" className={({ isActive }) => isActive ? "my-class" : ""}>我的</NavLink>
Navigate组件使用:
Navigate用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:
<Routes>
{/* 当默认路径 / 时, 重定向到home页面 */}
<Route path='/' element={<Navigate to="/home"/>}></Route>
<Route path='/home' element={<Home/>}/>
<Route path='/about' element={<About/>}/>
<Route path='/profile' element={<Profile/>}/>
</Routes>
Not Found页面配置:
开发一个Not Found页面;
配置对应的Route,并且设置path为*即可;
<Routes>
{/* 当默认路径 / 时, 重定向到home页面 */}
<Route path='/' element={<Navigate to="/home"/>}></Route>
<Route path='/home' element={<Home/>}/>
<Route path='/about' element={<About/>}/>
<Route path='/profile' element={<Profile/>}/>
{/* 当上面路径都没有匹配到时, 显式Notfound组件 */}
<Route path='*' element={<Notfound/>}/>
</Routes>
3.路由的嵌套
<Routes>
<Route path='/' element={<Navigate to="/home"/>}></Route>
{/* 配置二级路由 */}
<Route path='/home' element={<Home/>}>
<Route path='/home' element={<Navigate to="/home/recommend"/>}/>
<Route path='/home/recommend' element={<HomeRecommend/>}/>
<Route path='/home/ranking' element={<HomeRanking/>}/>
</Route>
<Route path='/about' element={<About/>}/>
<Route path='/profile' element={<Profile/>}/>
<Route path='*' element={<Notfound/>}/>
</Routes>
组件用于在父路由元素中作为子路由的占位元素, 也就是子路由的展示位置(必须写)。
// home组件
import { Link, Outlet } from 'react-router-dom'
export class Home extends PureComponent {
render() {
return (
<div>
<h2>Home</h2>
<Link to="/home/recommend">推荐</Link>
<Link to="/home/ranking">排行</Link>
<Outlet/>
</div>
)
}
}
4.手动实现路由的跳转
// 修改为函数组件, 类组件无法使用hook
export function App() {
// 使用hook
const navigate = useNavigate()
function navigateTo(path) {
navigate(path)
}
return (
<div className='app'>
<div className='header'>
<Link to="/home">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
{/* 点击时将路径传入到navigate中 */}
<button onClick={() => navigateTo("/category")}>分类</button>
<span onClick={() => navigateTo("/order")}>订单</span>
</div>
<div className='counter'>
<Routes>
{/* 当默认路径 / 时, 重定向到home页面 */}
<Route path='/' element={<Navigate to="/home"/>}></Route>
{/* 配置二级路由 */}
<Route path='/home' element={<Home/>}>
<Route path='/home' element={<Navigate to="/home/recommend"/>}/>
<Route path='/home/recommend' element={<HomeRecommend/>}/>
<Route path='/home/ranking' element={<HomeRanking/>}/>
</Route>
<Route path='/about' element={<About/>}/>
<Route path='/profile' element={<Profile/>}/>
<Route path='/category' element={<Category/>}/>
<Route path='/order' element={<Order/>}/>
{/* 当上面路径都没有匹配到时, 显式Notfound组件 */}
<Route path='*' element={<Notfound/>}/>
</Routes>
</div>
<div className='footer'>footer</div>
</div>
)
}
那么如果是一个函数式组件,我们可以直接调用它提供的hooks的写法,但是如果是一个类组件呢?
Router6.x确实是没有提供类组件的API, 如果我们确实想要在类组件中使用, 需要再使用高阶组件对类组件进行增强(通过高阶组件增强向类组件中传入navigate);
如果是Router5.x, 是有提供withRouter这样一个高阶组件的, 但是Router6.x中, 我们需要自己实现这样的高阶组件
封装高阶函数方法如下, 由于其他地方也可能使用高阶组件, 所以我是在一个单独的文件中进行封装
import { useState } from "react";
import {
useLocation,
useNavigate,
useParams,
useSearchParams,
} from "react-router-dom";
function withRouter(Cpn) {
return function (props) {
// 1.导航
const navigate = useNavigate();
// 2.动态路由传参数:/detail/:id
const params = useParams();
// 查询字符串参数:/user?name=yuan&age=18
const location = useLocation();
const [searchParams] = useSearchParams();
const query = Object.fromEntries(searchParams);
const router = { navigate, params, location, query };
return <Cpn {...props} router={router}></Cpn>;
};
}
export default withRouter;
这样我们引入自己封装的高阶组件, 通过高阶组件的增强, 就可以在类组件的props中获取到navigate
export class App extends PureComponent {
navigateTo(path) {
// 经过高阶组件增强的组件中, 可以在props中拿到navigate
const { naviagte } = this.props.router
// 调用navigate
naviagte(path)
}
render() {
return (
<div className='app'>
<div className='header'>
<Link to="/home">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
{/* 发生点击事件时, 将路劲传递过去 */}
<button onClick={() => this.navigateTo("/category")}>分类</button>
<span onClick={() => this.navigateTo("/order")}>订单</span>
</div>
<div className='counter'>
<Routes>
{/* 当默认路径 / 时, 重定向到home页面 */}
<Route path='/' element={<Navigate to="/home"/>}></Route>
{/* 配置二级路由 */}
<Route path='/home' element={<Home/>}>
<Route path='/home' element={<Navigate to="/home/recommend"/>}/>
<Route path='/home/recommend' element={<HomeRecommend/>}/>
<Route path='home/ranking' element={<HomeRanking/>}/>
</Route>
<Route path='/about' element={<About/>}/>
<Route path='/profile' element={<Profile/>}/>
<Route path='/category' element={<Category/>}/>
<Route path='/order' element={<Order/>}/>
{/* 当上面路径都没有匹配到时, 显式Notfound组件 */}
<Route path='*' element={<Notfound/>}/>
</Routes>
</div>
<div className='footer'>footer</div>
</div>
)
}
}
// 使用高阶组件对App组件进行增强
export default withRouter(App)
5.路由的配置文件
目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。
但是这样的方式会让路由变得非常混乱,我们希望像vue-router那样, 将所有的路由配置放到一个单独的文件进行集中管理:
在早期的时候,Router并且没有提供相关的API,我们需要借助于react-router-config完成;
在Router6.x中,为我们提供了useRoutes API可以完成相关的配置;
例如我们将下面的映射关系配置到一个单独的文件中:
<div className='counter'>
<Routes>
{/* 当默认路径 / 时, 重定向到home页面 */}
<Route path='/' element={<Navigate to="/home"/>}></Route>
{/* 配置二级路由 */}
<Route path='/home' element={<Home/>}>
<Route path='/home' element={<Navigate to="/home/recommend"/>}/>
<Route path='/home/recommend' element={<HomeRecommend/>}/>
<Route path='home/ranking' element={<HomeRanking/>}/>
</Route>
<Route path='/about' element={<About/>}/>
<Route path='/profile' element={<Profile/>}/>
<Route path='/category' element={<Category/>}/>
<Route path='/order' element={<Order/>}/>
<Route path='/detail/:id' element={<Detail/>}/>
<Route path='/user' element={<User/>} />
{/* 当上面路径都没有匹配到时, 显式Notfound组件 */}
<Route path='*' element={<Notfound/>}/>
</Routes>
</div>
首先, 使用useRoutes这个API替代原来的Routes和Route组件, useRoutes可以当成一个函数直接使用, 但是只能在函数组件中使用
<div className='counter'>
{useRoutes(routes)}
</div>
再在route/index.js中对映射关系进行配置
import { Navigate } from "react-router-dom"
import Home from '../pages/Home'
import About from '../pages/About'
import Profile from '../pages/Profile'
import Notfound from '../pages/Notfound'
import HomeRecommend from '../pages/HomeRecommend'
import HomeRanking from '../pages/HomeRanking'
import Category from '../pages/Category'
import Order from '../pages/Order'
import Detail from '../pages/Detail'
import User from '../pages/User'
const routes = [
{
path: "/",
element: <Navigate to="/home"/>
},
{
path: "/home",
element: <Home/>,
children: [
{
path: "/home",
element: <Navigate to="/home/recommend" />
},
{
path: "/home/recommend",
element: <HomeRecommend/>
},
{
path: "/home/ranking",
element: <HomeRanking/>
}
]
},
{
path: "/about",
element: <About/>
},
{
path: "/profile",
element: <Profile/>
},
{
path: "/category",
element: <Category/>
},
{
path: "/order",
element: <Order/>
},
{
path: "detail/:id",
element: <Detail/>
},
{
path: "/user",
element: <User/>
},
{
path: "*",
element: <Notfound/>
}
]
export default routes
如果我们对某些组件进行了异步加载(懒加载, 分包处理),那么需要使用Suspense进行包裹:
例如我们对Detail和User进行懒加载(分包处理)
// import Detail from '../pages/Detail'
// import User from '../pages/User'
const Detail = React.lazy(() => import("../pages/Detail"))
const User = React.lazy(() => import("../pages/User"))
并且还需要使用Suspense对组件进行包裹
root.render(
<HashRouter>
<Suspense fallback={<h3>loading</h3>}>
<App/>
</Suspense>
</HashRouter>
)
十三、react hooks
1.为什么需要hook
Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期);
我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:
- class组件可以定义自己的state,用来保存组件自己内部的状态;函数式组件不可以,因为函数每次调用都会产生新的临时变量;
- class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑; 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
- class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
- 所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。
2.class组件存在的问题
(1) 复杂组件变得难以理解:
- 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
- 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除);
- 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
(2) 难以理解的class:
- 很多人发现学习ES6的class是学习React的一个障碍。
- 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
- 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
(3) 组件复用状态很难:
- 在前面为了一些状态的复用我们需要通过高阶组件;
- 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
- 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
- 这些代码让我们不管是编写和设计上来说,都变得非常困难;
Hook的出现就可以解决上面的问题
3.简单总结一下Hooks
(1) 它可以让我们在不编写class的情况下使用state以及其他的React特性;
(2) 我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
(3) Hook的使用场景:
- Hook的出现基本可以代替我们之前所有使用class组件的地方;
- 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
- Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;
4.useState
import React, { memo, useState } from "react";
const App = memo((props) => {
const [counter, setCounter] = useState(100);
const addNum = (num) => {
setCounter(counter + num);
};
return (
<>
<h2>counter:{counter}</h2>
<button onClick={() => addNum(1)}>+1</button>
</>
);
});
export default App;
useState来自react,需要从react中导入,是一个hook函数, 官方中也将它成为State Hook, 它与class组件里面的 this.state 提供的功能完全相同;
一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
useState只有一个参数: 接收一个初始化状态的值(设置初始值),在
第一次组件被调用时
使用来作为初始化值(如果不设置则默认为undefined);useState的返回值: 返回一个数组,数组包含两个元素:
- 元素一: 当前状态的值(第一次调用为初始化值);
- 元素二: 是一个设置状态值变化的函数;
- 不过我们如果总是使用索引来获取这两个元素总是不方便的, 因此在开发中我们通常是会对数组进行解构(当然要取什么名字是自定义的);
- 例如上面代码:
const [counter, setCounter] = useState(100)
hook的使用规则:
只能在函数组件的
顶层
调用 Hook。不能在循环语句、条件判断语句或者子函数中调用。只能在 React 的
函数组件
和自定义hook
中调用 Hook。不能在其他 JavaScript 函数中调用。
5.useEffect
我们知道在类组件中是可以有生命周期函数的, 那么如何在函数组件中定义类似于生命周期这些函数呢?
Effect Hook 可以让你来完成一些类似于class中生命周期的功能;
事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);
所以对于完成这些功能的Hook被称之为 Effect Hook;
(1) useEffect的基本使用:
假如我们现在有一个需求:页面中的title总是显示counter的数字,分别使用class组件和Hook实现:
class的实现
import React, { PureComponent } from "react";
class App extends PureComponent {
constructor() {
super();
this.state = {
counter: 100,
};
}
componentDidMount() {
document.title = this.state.counter;
}
componentDidUpdate() {
document.title = this.state.counter;
}
render() {
const { counter } = this.state;
return (
<>
<div>counter: {counter}</div>
<button onClick={(e) => this.setState({ counter: counter + 1 })}>
+1
</button>
</>
);
}
}
export default App;
hook的实现
import React, { memo, useState, useEffect } from "react";
const App = memo((props) => {
const [counter, setCounter] = useState(100);
useEffect(() => {
document.title = counter;
});
const addNum = (num) => {
setCounter(counter + num);
};
return (
<>
<h2>counter: {counter}</h2>
<button onClick={() => addNum(1)}>+1</button>
</>
);
});
export default App;
(2) useEffect的解析
- 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
- useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
- 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;
(3) 清除副作用(Effect):
在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除:
比如我们之前的事件总线或Redux中手动调用subscribe;
都需要在componentWillUnmount有对应的取消订阅;
Effect Hook通过什么方式来模拟componentWillUnmount呢?
useEffect传入的回调函数A
本身可以有一个返回值,这个返回值是另外一个回调函数B
:
type EffectCallback = () => (void | (() => void | undefined));
为什么要在 effect 中返回一个函数?
这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
如此可以将
添加和移除
订阅的逻辑放在一起;它们都属于 effect 的一部分;
React 何时清除 effect?
React 会在组件更新和卸载的时候执行清除操作, 将上一次的监听取消掉, 只留下当前的监听 ;
正如之前学到的,effect 在每次渲染的时候都会执行;
import React, { memo, useEffect } from 'react'
const App = memo(() => {
useEffect(() => {
// 监听store数据发生改变
const unsubscribe = store.subscribe(() => {
})
// 返回值是一个回调函数, 该回调函数在组件重新渲染或者要卸载时执行
return () => {
// 取消监听操作
unsubscribe()
}
})
return (
<div>
<h2>App</h2>
</div>
)
})
export default App
(4) 使用多个effect
使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:
比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;
一个函数组件中可以使用多个Effect Hook,我们可以将逻辑分离到不同的useEffect中:
import React, { memo, useEffect } from 'react'
const App = memo(() => {
// 监听的useEffect
useEffect(() => {
console.log("监听的代码逻辑")
return () => {
console.log("取消的监听代码逻辑")
}
})
// 发送网络请求的useEffect
useEffect(() => {
console.log("网络请求的代码逻辑")
})
// 操作DOM的useEffect
useEffect(() => {
console.log("操作DOM的代码逻辑")
})
return (
<div>
App
</div>
)
})
export default App
Hook允许我们按照代码的用途分离它们, 而不是像生命周期函数那样, 将很多逻辑放在一起:
React将按照 effect 声明的顺序
依次调用
组件中的每一个 effect;
(5) useEffect性能优化
默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:
某些代码我们只是希望执行一次即可(比如网络请求, 组件第一次渲染中执行一次即可, 不需要执行多次),类似于类组件中的componentDidMount和componentWillUnmount中完成的事情;
另外,多次执行也会导致一定的性能问题;
我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?
useEffect实际上有两个参数:
- 参数一: 执行的回调函数, 这个参数我们已经使用过了不再多说;
- 参数二: 是一个数组类型, 表示 该useEffect在哪些state发生变化时,才重新执行;(受谁的影响才会重新执行)
import React, { memo, useEffect, useState } from 'react'
const App = memo(() => {
const [counter, setCounter] = useState(100)
// 发送网络请求的useEffect, 只有在counter发生改变时才会重新执行
useEffect(() => {
console.log("网络请求的代码逻辑")
}, [counter])
return (
<div>
<h2 onClick={() => setCounter(counter+1)}>{counter}</h2>
</div>
)
})
export default App
但是,如果一个函数我们不希望依赖任何的内容时
,也可以传入一个空的数组 []:
那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了;
import React, { memo, useEffect, useState } from 'react'
const App = memo(() => {
const [counter, setCounter] = useState(100)
// 传入空数组表示不受任何数据依赖
useEffect(() => {
// 此时传入的参数一这个回调函数: 相当于componentDidMount
console.log("监听的代码逻辑")
// 参数一这个回调函数的返回值: 相当于componentWillUnmount
return () => {
console.log("取消的监听代码逻辑")
}
}, [])
return (
<div>
<h2 onClick={() => setCounter(counter+1)}>{counter}</h2>
</div>
)
})
export default App
6.useCallback和useMemo性能优化
(1) useCallback的使用:
useCallback实际的目的是为了性能优化
useCallback进行什么样的优化呢?
例如下面这个计数器的案例, 我们点击按钮时, counter数据会发生变化, App函数组件就会重新渲染, 意味着increment函数就会被重新定义一次, 每点击一次按钮, increment函数就会重新被定义; 虽然每次定义increment函数, 垃圾回收机制会将上一次定义的increment函数回收, 但是这种不必要的重复定义是会影响性能的
import React, { memo, useState } from 'react'
const App = memo(() => {
const [counter, setCounter] = useState(10)
function increment() {
setCounter(counter + 1)
}
return (
<div>
<h2>{counter}</h2>
<button onClick={() => increment()}>+1</button>
</div>
)
})
export default App
如何进行性能的优化呢?
调用useCallback会返回一个 memoized(有记忆的) 的回调函数;
在依赖不变的情况下,多次定义的时候,返回的回调函数是相同的;
参数一: 传入一个回调函数, 如果依赖发生改变会定义一个新的该回调函数使用, 如果依赖没有发生改变, 依然使用原来的回调函数
参数二: 用于控制依赖的, 第二个参数要求传入一个数组, 数组中可以传入依赖, 传空数组表示没有依赖
useCallback拿到的结果是函数
const memoizedCallback = useCallback(
() => {
doSomething(a, b)
},
[a, b]
)
useCallback的作用:
通常使用useCallback的目的是在向子组件传递函数时, 将要传递的函数进行优化在传递给子组件, 避免子组件进行多次渲染;
并不是为了函数不再重新定义, 也不是对函数定义做优化
定义一个子组件Test, 并将increment函数传递到子组件中, 我们在子组件中可以拿到increment方法修改App组件中的counter;
由于counter发生改变, 就会重新定义一个新的increment函数, 因此我们只要修改了counter, 就会传递一个新的increment函数到Test组件中; Test组件中的props就会发生变化, Test组件会被重新渲染
如果此时App组件中再定义一个方法changeMessage用来修改message;
我们会发现当message发生改变时, 子组件Test也会被重新渲染; 这是因为message发生改变, App组件会重新渲染, 那么就会重新定义一个新的increment函数, 将新的increment函数传递到Test组件, Test组件的props发生改变就会重新渲染
import React, { memo, useState } from "react";
const Test = memo((props) => {
console.log("test被重新渲染");
return (
<>
<button onClick={props.increment}>add</button>
</>
);
});
const App = memo((props) => {
const [counter, setCounter] = useState(0);
const [message, setMessage] = useState("hello react");
const addNum = (num) => {
setCounter(counter + num);
};
return (
<>
<div>app</div>
<h2>counter: {counter}</h2>
<h2>message: {message}</h2>
<button onClick={(e) => addNum(1)}>+1</button>
<button onClick={(e) => setMessage("hello callback")}>修改文本</button>
<Test increment={(e) => addNum(1)}></Test>
</>
);
});
export default App;
但是如果我们使用useCallback, 就可以避免App组件中message发生改变时, Test组件重新渲染
因为message组件发生改变, 但是我们下面的useCallback函数是依赖counter的, 在依赖没有发生改变时, 多次定义返回的值是相同的(也就是修改message重新渲染App组件时, increment并没有重新定义, 依然是之前的); 就意味着Test组件中的props没有改变, 因此Test组件不会被重新渲染
如果是counter值发生改变, 因为useCallback函数是依赖counter的, 所以会定义一个新的函数给increment; 当向Test组件传递新的increment时, Test组件的props就会改变, Test依然会重新渲染, 这也是我们想要实现的效果
import React, { memo, useState, useCallback } from "react";
const Test = memo((props) => {
console.log("test被重新渲染");
return (
<>
<button onClick={props.increment}>add</button>
</>
);
});
const App = memo((props) => {
const [counter, setCounter] = useState(0);
const [message, setMessage] = useState("hello react");
const addNum = useCallback(() => {
setCounter(counter + 1);
}, [counter]);
return (
<>
<div>app</div>
<h2>counter: {counter}</h2>
<h2>message: {message}</h2>
<button onClick={addNum}>+1</button>
<button onClick={(e) => setMessage("hello callback")}>修改文本</button>
<Test increment={addNum}></Test>
</>
);
});
export default App;
还可以再进一步的进行优化:
现在我们的代码是counter发生变化时, useCallback会重新定义一个新的函数返回给increment; 但是我们想做到, counter发生变化, 依然使用原来的函数, 不需要重新定义一个新的函数;
可能会有小伙伴想, 直接将依赖改为一个空数组, 但是如果是这样的话就会产生闭包陷阱; 我们修改counter时确实不会重新生成一个新的函数, 但是原来的函数中使用的counter永远是之前的值, 也就是0; 这是因为我们旧的函数在定义的那一刻, counter的值是0; 由于修改counter依然使用旧的函数, 这样无论我们修改多少次counter, 页面展示的数据永远是 0 + 1 的结果
const increment = useCallback(() => {
setCounter(counter + 1)
}, [])
这个时候我们就需要结合使用另一个hook: useRef
useRef函数在组件多次进行渲染时, 返回的是同一个值; 我们就可以将最新的counter储存到useRef返回的对象的current属性中; 这样做的好处就是, counter发生改变时, 也不会重新定义一个函数, 意味着修改counter也不会导致Test组件重新渲染
import React, { memo, useState, useCallback, useRef } from "react";
const Test = memo((props) => {
console.log("test被重新渲染");
return (
<>
<button onClick={props.increment}>add</button>
</>
);
});
const App = memo((props) => {
const [counter, setCounter] = useState(0);
const [message, setMessage] = useState("hello react");
const counterRef = useRef();
counterRef.current = counter;
const addNum = useCallback(() => {
setCounter(counterRef.current + 1);
}, []);
return (
<>
<div>app</div>
<h2>counter: {counter}</h2>
<h2>message: {message}</h2>
<button onClick={addNum}>+1</button>
<button onClick={(e) => setMessage("hello callback")}>修改文本</button>
<Test increment={addNum}></Test>
</>
);
});
export default App;
(2) useMemo的解析
useMemo实际的目的也是为了进行性能的优化, 例如下面这个例子
我们定义一个计算累加的函数calcNumTotal, 在App组件中调用这个函数计算结果
但是counter改变时, App组件就会重新渲染, 那么calcNumTotal函数又会重新计算; 但是counter的改变和calcNumTotal函数并没有关系, 却要重新渲染; 这种类似的场景我们就可以使用useMemo进行性能优化
import React, { memo, useState } from "react";
function calcNumTotal(num) {
let total = 0;
for (let i = 0; i <= num; i++) {
total += num;
}
return total;
}
const App = memo((props) => {
const [counter, setCounter] = useState(10);
return (
<>
<div>app</div>
<h2>counter: {counter}</h2>
<h2>{calcNumTotal(100)}</h2>
<button onClick={(e) => setCounter(counter + 1)}>+1</button>
</>
);
});
export default App;
如何使用 useMemo进行性能的优化呢?
useMemo返回的也是一个 memoized(有记忆的) 值; 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
参数一: 传入一个回调函数
参数二: 传入一个数组, 表示依赖, 什么都不依赖传入空数组; 如果不传则该函数什么都不会做, 无意义
const memoizedValue = useMemo( () => { computeExpensiveValue(a, b) }, [a, b] )
这样我们就可以对上面的代码进行优化了, 实现counter发生变化, 而calcNumTotal函数不需要重新计算结果:
import React, { memo, useState, useMemo } from "react";
function calcNumTotal(num) {
console.log("函数被调用");
let total = 0;
for (let i = 0; i <= num; i++) {
total += num;
}
return total;
}
const App = memo((props) => {
const [counter, setCounter] = useState(10);
const result = useMemo(() => {
return calcNumTotal(100);
}, []);
return (
<>
<div>app</div>
<h2>counter: {counter}</h2>
<h2>{result}</h2>
<button onClick={(e) => setCounter(counter + 1)}>+1</button>
</>
);
});
export default App;
(3) useMemo与useCallback的区别:
useMemo拿到的传入回调函数的返回值, useCallback拿到的传入的回调函数本身;
简单来说useMemo是对函数的返回值做优化, useCallback是对函数做优化;
7.useRef和useImperativeHandle
(1) useRef:useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变
最常用的ref两种用法:
用法一: 用来获取DOM(或者组件,但是需要是class组件)元素, 操作DOM;
import React, { memo, useRef } from "react";
const App = memo((props) => {
const ref1 = useRef();
const inputRef = useRef();
function getDom() {
console.log(ref1.current);
inputRef.current.focus();
}
return (
<>
<div ref={ref1}>hello world</div>
<input type="text" ref={inputRef} />
<button onClick={getDom}>获取dom</button>
</>
);
});
export default App;
用法二: 保存一个数据,这个对象在整个生命周期中可以保存不变;
import React, { memo, useRef } from 'react'
import { useCallback } from 'react'
import { useState } from 'react'
let obj = null
const App = memo(() => {
const [count, setCount] = useState(0)
const nameRef = useRef()
console.log(obj === nameRef)
obj = nameRef
// 通过useRef解决闭包陷阱
const countRef = useRef()
countRef.current = count
const increment = useCallback(() => {
setCount(countRef.current + 1)
}, [])
return (
<div>
<h2>Hello World: {count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<button onClick={increment}>+1</button>
</div>
)
})
export default App
(2) useImperativeHandle:
通过forwardRef可以将ref转发到子组件;
子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;
父组件中就可以获取到子组件中绑定了ref的元素
import React, { memo, forwardRef, useRef } from "react";
const HelloWorld = memo(
forwardRef((props, ref) => {
return (
<>
<input type="text" ref={ref} />
</>
);
})
);
const App = memo((props) => {
const inputRef = useRef();
function getDom() {
inputRef.current.focus();
console.log(inputRef.current);
}
return (
<>
<HelloWorld ref={inputRef}></HelloWorld>
<button onClick={getDom}>获取子组件焦点</button>
</>
);
});
export default App;
forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:
直接暴露给父组件带来的问题是某些情况的不可控;
父组件可以拿到DOM后进行任意的操作;
但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作
例如修改元素内容的操作inputRef.current.value = “aaa”等等, 我们希望可以限制它的操作;
通过useImperativeHandle可以值暴露固定的操作(对操作进行限制):
useImperativeHandle要求传入两个参数:
- 参数一: 传入一个ref
- 参数二: 传入一个回调函数, 要求该回调函数返回一个对象, 该对象会绑定到传入的ref的current属性中
通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起; 所以在父组件中,使用 inputRef.current时,实际上使用的是参数二返回的对象;
import React, { memo, forwardRef, useRef, useImperativeHandle } from "react";
const HelloWorld = memo(
forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
};
});
return (
<>
<input type="text" ref={inputRef} />
</>
);
})
);
const App = memo((props) => {
const inputRef = useRef();
function getDom() {
inputRef.current.focus();
console.log(inputRef.current);
}
return (
<>
<HelloWorld ref={inputRef}></HelloWorld>
<button onClick={getDom}>获取子组件焦点</button>
</>
);
});
export default App;
8.useContext和useReducer
(1)useContext的使用:
新建context/index.js
import { createContext } from "react";
const UserContext = createContext();
const ThemeContext = createContext();
export { UserContext, ThemeContext };
在index.js注入context
root.render(
<UserContext.Provider value={{ name: "yuan", age: 18 }}>
<ThemeContext.Provider value={{ color: "red", size: "30px" }}>
<App></App>
</ThemeContext.Provider>
</UserContext.Provider>
);
新建App.jsx
import React, { memo, useContext } from "react";
import { UserContext, ThemeContext } from "./context";
const App = memo((props) => {
const userInfo = useContext(UserContext);
const themeInfo = useContext(ThemeContext);
return (
<>
<div>
{userInfo.name} - {userInfo.age}
</div>
<div>
{themeInfo.color} - {themeInfo.size}
</div>
</>
);
});
export default App;
(2) useReducer
很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是。
useReducer仅仅是useState的一种替代方案:
在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
或者这次修改的state需要依赖之前的state时,也可以使用;
import React, { memo, useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "add_num":
return { ...state, counter: state.counter + action.num };
case "sub_num":
return { ...state, counter: state.counter - action.num };
default:
return state;
}
}
const App = memo((props) => {
const [state, dispatch] = useReducer(reducer, { counter: 0 });
return (
<>
<div>app</div>
<h2>counter: {state.counter}</h2>
<button onClick={() => dispatch({ type: "add_num", num: 1 })}>+1</button>
<button onClick={() => dispatch({ type: "sub_num", num: 1 })}>-1</button>
</>
);
});
export default App;
数据是不会共享的,它们只是使用了相同的counterReducer的函数而已。
所以,useReducer只是useState的一种替代品,并不能替代Redux。
9.useLayoutEffect
useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
- useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
- useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
10.自定义hook
import { useState, useEffect } from "react"
function useScrollPosition() {
const [ scrollX, setScrollX ] = useState(0)
const [ scrollY, setScrollY ] = useState(0)
useEffect(() => {
function handleScroll() {
// console.log(window.scrollX, window.scrollY)
setScrollX(window.scrollX)
setScrollY(window.scrollY)
}
window.addEventListener("scroll", handleScroll)
return () => {
window.removeEventListener("scroll", handleScroll)
}
}, [])
return [scrollX, scrollY]
}
export default useScrollPosition
11.redux hooks
// 1.使用useSelector将redux中store的数据映射到组件内
const { count } = useSelector((state) => ({
count: state.counter.count
}))
// 2.使用dispatch直接派发action
const dispatch = useDispatch()
function addNumberHandle(num, isAdd = true) {
if (isAdd) {
dispatch(addNumberAction(num))
} else {
dispatch(subNumberAction(num))
}
}