在探索和学习React的过程中,我逐渐领悟到了前端开发的魅力与挑战。React,作为Facebook推出的开源JavaScript库,以其独特的思维方式和强大的功能,引领着前端开发的潮流。在这篇文章中,我将分享我的React学习心得,总结笔记,以期帮助那些同样在前端领域摸索的开发者们。
1.Hello React 案例
<div id="root"></div>;
// 类组件和函数式组件
class App extends React.Component {
// 组件数据
constructor() {
super();
this.state = {
message: "Hello World",
};
// 对需要绑定的方法, 提前绑定好this 或者方法写成箭头函数上下文指向React组件
this.btnClick = this.btnClick.bind(this);
}
// 组件方法(实例方法)
btnClick() {
// 1.将state中message值修改掉 2.自动重新执行render函数函数
this.setState({
message: "Hello React",
});
}
// 渲染内容 render方法
render() {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={this.btnClick}>修改文本</button>
</div>
);
}
}
// 将组件渲染到界面上
const root = ReactDOM.createRoot(document.querySelector("#root"));
// App根组件
root.render(<App />);
如果上下文中没有定义
this
,那么this
默认会指向全局对象,在浏览器中这个全局对象通常是window
。在 React 的类组件中,果没有显示绑定方法的this
,那么调用方法时this
指向会失去组件实例的上下文,进而变成undefined
,从而导致运行时错误。为了避免这种问题,可以使用
.bind()
或箭头函数显式绑定方法的this
,或者在构造函数中使用箭头函数定义方法,从而确保方法的this
指向是组件实例。
2. 列表渲染
....
// 封装App组件
class App extends React.Component {
constructor() {
super()
this.state = {
data: ["Vue", "React", "Angular"]
}
}
render() {
// 1.对data进行for循环
// const liEls = []
// for (let i = 0; i < this.state.data.length; i++) {
// const item = this.state.movies[i]
// const liEl = <li>{item}</li>
// liEls.push(liEl)
// }
// 2.data数组 => liEls数组
// const liEls = this.state.data.map(movie => <li>{movie}</li>)
return (
<div>
<h2>电影列表</h2>
<ul>
{this.state.movies.map(movie => <li>{movie}</li>)}
</ul>
</div>
)
}
}
......
3.JSX 语法规则
3.1 插入内容
- jsx 中的注释
- JSX 嵌入变量作为子元素
- 情况一:当变量是 Number、String、Array 类型时,可以直接显示
- 情况二:当变量是 null、undefined、Boolean 类型时,内容为空;
- 如果希望可以显示 null、undefined、Boolean,那么需要转成字符串;
- 转换的方式有很多,比如 toString 方法、和空字符串拼接,String(变量)等方式;
- 情况三:Object 对象类型不能作为子元素(not valid as a React child)
- JSX 嵌入表达式
- 运算表达式
- 三元运算符
- 行一个函数**
// jsx语法规则
class App extends React.Component {
constructor() {
super();
this.state = {
message: "Hello World",
count: 1000,
names: ["紫陌", "张三", "王五"],
aaa: undefined,
bbb: null,
ccc: true,
friend: { name: "zimo" },
firstName: "zimo",
lastName: "紫陌",
age: 20,
list: ["aaa", "bbb", "ccc"],
};
}
render() {
// 解构拿到值
const { message, names, count, aaa, bbb, ccc, friend, firstName, lastName, age } = this.state;
// 对内容进行运算后显示(插入表示)
const ageText = age >= 18 ? "成年人" : "未成年人";
const liEls = this.state.list.map((item) => <li>{item}</li>);
return (
<div>
{/* 1.Number / String / Array直接显示出来*/}
<h2>{message}</h2>
<h2>{count}</h2>
<h2>{names}</h2>
{/* 2.undefined/null/Boolean 默认不展示,三种方法都可以显示出来*/}
<h2>{String(aaa)}</h2>
<h2>{bbb + ""}</h2>
<h2>{ccc.toString()}</h2>
{/* 3.Object类型不能作为子元素展示 */}
<h2>{friend.name}</h2> {/* zimo */}
<h2>{Object.keys(friend)[0]}</h2> {/* name */}
{/* 4.可以插入对应表达式 */}
<h2>{10 + 20}</h2>
<h2>{firstName + "" + lastName}</h2>
{/* 5.插入三元表达式 */}
<h2>{ageText}</h2>
<h2>{age >= 18 ? "成年人" : "未成年人"}</h2>
{/* 6.可以调用方法获取结果 */}
<ul>{liEls}</ul>
<ul>
{this.state.list.map((item) => (
<li>{item}</li>
))}
</ul>
<ul>{this.getList()}</ul>
</div>
);
}
getList() {
const liEls = this.state.list.map((item) => <li>{item}</li>);
return liEls;
}
}
3.2 绑定属性
// 1.定义App根组件
class App extends React.Component {
constructor() {
super();
this.state = {
title: "紫陌",
imgURL: "https:*****",
href: "https://*****",
isActive: true,
objStyle: { color: "red", fontSize: "30px" },
};
}
render() {
const { title, imgURL, href, isActive, objStyle } = this.state;
// 需求: isActive: true -> active
// 1.class绑定的写法一: 字符串的拼接
const className = `abc cba ${isActive ? "active" : ""}`;
// 2.class绑定的写法二: 将所有的class放到数组中
const classList = ["abc", "cba"];
if (isActive) classList.push("active");
// 3.class绑定的写法三: 第三方库classnames -> npm install classnames
return (
<div>
{/* 1.基本属性绑定 */}
<h2 title={title}>我是h2元素</h2>
<img src={imgURL} alt="" />
<a href={href}>百度一下</a>
{/* 2.绑定class属性: 最好使用className */}
<h2 className={className}>哈哈哈哈</h2>
<h2 className={classList.join(" ")}>哈哈哈哈</h2>
{/* 3.绑定style属性: 绑定对象类型 */}
<h2 style={{ color: "red", fontSize: "30px" }}>紫陌YYDS</h2>
<h2 style={objStyle}>紫陌</h2>
</div>
);
}
}
jsx 语法规则:
1.定义虚拟 DOM 时,不要写引号。
2.标签中混入 JS 表达式时要用{}。
3.样式的类名指定不要用 class,要用 className。
5.只有一个根标签
6.标签必须闭合
7.标签首字母
(1).若小写字母开头,则将该标签转为 html 中同名元素,若 html 中无该标签对应的同名元素,则报错。
(2).若大写字母开头,react 就去渲染对应的组件,若组件没有定义,则报错。
3.3 事件绑定
- React 事件的命名采用小驼峰式(camelCase),而不是纯小写;
- 我们需要通过{}传入一个事件处理函数,这个函数会在事件发生时被执行;
class App extends React.Component {
constructor() {
super();
this.state = {
count: 10,
};
this.btn1Click = this.btn1Click.bind(this);
}
btn1Click() {
console.log("btn1", this);
this.setState({ count: this.state.count + 1 });
}
btn2Click = () => {
console.log("btn2", this);
this.setState({ count: this.state.count + 1 });
};
btn2Click = () => {
console.log("btn2", this);
this.setState({ count: this.state.count + 1 });
};
btn3Click() {
console.log("btn2", this);
this.setState({ count: this.state.count + 1 });
}
render() {
const { count } = this.state;
return (
<div>
<h2>当前计数:{count}</h2>
{/* 1.this绑定方式一:bind绑定 */}
<button onClick={this.btn1Click}>按钮1</button>
{/* 2.this.绑定方式二:ES6 class fields */}
<button onClick={this.btn2Click}>按钮2</button>
{/* 3.this绑定方式三:直接传入一个箭头函数(重要) */}
<button onClick={() => console.log("btn3Click")}>按钮3</button>
<button onClick={() => this.btn3Click()}>按钮4</button>
</div>
);
}
}
3.4 事件参数传递
- 在执行事件函数时,有可能我们需要获取一些参数信息:比如 event 对象、其他参数
- 情况一:获取 event 对象
- 很多时候我们需要拿到 event 对象来做一些事情(比如阻止默认行为)
- 那么默认情况下,event 对象有被直接传入,函数就可以获取到 event 对象;
- 情况二:获取更多参数
- 有更多参数时,我们最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数;
class App extends React.Component {
constructor() {
super();
this.state = {
message: "Hello World",
};
}
btnClick(event, name, age) {
console.log("btnClick", event, this);
console.log("name,age", name, age);
}
render() {
return (
<div>
{/* 1.event参数传递 */}
<button onClick={this.btnClick.bind(this)}>按钮1</button>
<button
onClick={(event) => {
this.btnClick(event);
}}
>
按钮2
</button>
{/* 额外参数传递 */}
<button onClick={this.btnClick.bind(this, "zimo", 18)}>按钮3(不推荐)</button>
<button onClick={(event) => this.btnClick(event, "zimo", 18)}>按钮4</button>
</div>
);
}
}
3.5 条件渲染
// 1.定义App根组件
class App extends React.Component {
constructor() {
super();
this.state = {
isShow: false,
flag: undefined,
};
}
render() {
const { isShow, flag } = this.state;
// 1.条件判断方式一: 使用if进行条件判断
let showElement = null;
if (isShow) {
showElement = <h2>显示紫陌</h2>;
} else {
showElement = <h1>隐藏紫陌</h1>;
}
return (
<div>
{/* 1.方式一: 根据条件给变量赋值不同的内容 */}
<div>{showElement}</div>
{/* 2.方式二: 三元运算符 */}
<div>{isShow ? <h2>显示</h2> : <h2>隐藏</h2>}</div>
{/* 3.方式三: &&逻辑与运算 */}
{/* 场景: 当某一个值, 有可能为undefined时, 使用&&进行条件判断 */}
<div> {flag && <h2>{flag}</h2>} </div>
</div>
);
}
}
3.6 条件列表渲染
- 在 React 中,展示列表最多的方式就是使用数组的map 高阶函数;
- 很多时候我们在展示一个数组中的数据之前,需要先对它进行一些处理 比如过滤掉一些内容:filter 函数
- 比如截取数组中的一部分内容:slice 函数
class App extends React.Component {
constructor() {
super();
this.state = {
students: [
{ id: 111, name: "zimo", score: 199 },
{ id: 112, name: "aniy", score: 98 },
{ id: 113, name: "james", score: 199 },
{ id: 114, name: "curry", score: 188 },
],
};
}
render() {
const { students } = this.state;
/* 第一种分别写 */
//分数大于100学生进行展示
const filterStudent = students.filter((item) => {
return item.score > 100;
});
//分数大于100前两名信息
const sliceStudents = filterStudent.slice(0, 2);
return (
<div>
<h2>列表数据</h2>
<div>
{/* 第二种方式链式调用 */}
{students
.filter((item) => item.score > 100)
.slice(0, 2)
.map((item) => {
return (
<div key={item.id}>
<h2>学号: {item.id}</h2>
<h3>姓名: {item.name}</h3>
<h4>分数:{item.score}</h4>
</div>
);
})}
</div>
</div>
);
}
}
3.7 JSX 的本质
-
JSX 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。 所有的 jsx 最终都会被转换成 React.createElement 的函数调用。
-
createElement 需要传递三个参数:
-
参数一:type
- 当前 ReactElement 的类型;
- 如果是标签元素,那么就使用字符串表示 “div”;
- 如果是组件元素,那么就直接使用组件的名称;
-
参数二:config
./images -
所有 jsx 中的属性都在 config 中以对象的属性和值的形式存储
-
比如传入 className 作为元素的 class;
-
参数三:children
- 存放在标签中的内容,以 children 数组的方式进行存储;
- 源码分析
- babel 转换
- JSX 是通过 babel 帮我们进行语法转换的,写的 jsx 代码都需要依赖 babel。
- 可以在 babel 的官网中快速查看转换的过程:https://babeljs.io/repl/#?presets=react
babel 转换过来的代码可以直接运行。界面依然是可以正常的渲染
3.组件化开发
3.1 render 函数的返回值
- 当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
- React 元素:
- 通常通过 JSX 创建。
- 例如,
会被 React 渲染为 DOM 节点,
<MyComponent />
会被 React 渲染为自定义组件; - 无论是
还是
<MyComponent />
均为 React 元素。
- 数组或 fragments:使得 render 方法可以返回多个元素。
- Portals:可以渲染子节点到不同的 DOM 子树中。
- 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
- 布尔类型或 null:什么都不渲染。
3.2 函数组件
- 函数组件是使用 function 来进行定义的函数,只是这个函数会返回和类组件中 render 函数返回一样的内容。
- 函数组件有自己的特点(hooks 就不一样了):
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
- this 关键字不能指向组件实例(因为没有组件实例);
- 没有内部状态(state);
// 函数式组件
function App(props) {
// 返回值: 和类组件中render函数返回的是一致
return <h1>Hello zimo</h1>;
}
export default App;
4.React 生命周期
4.1 常用生命周期函数图
- 全部生命周期函数
4.2 生命周期函数例子
父组件:APP 组件
import React, { Component } from "react";
import Hello from "./Hello";
class App extends Component {
constructor() {
super();
this.state = {
isShow: true,
};
}
isShowFun() {
this.setState({
isShow: !this.state.isShow,
});
}
render() {
const { isShow } = this.state;
return (
<div>
我是父组件
{isShow && <Hello />}
<button onClick={() => this.isShowFun()}>显示隐藏子组件</button>
</div>
);
}
}
export default App;
子组件:Hello
import React, { Component } from "react";
class Hello extends Component {
constructor() {
// 1.构造方法: constructor
console.log("1. Hello constructor");
super();
this.state = {
message: "你好紫陌",
};
}
updateText() {
this.setState({
message: "你好啊,zimo~",
});
}
// 2.执行render函数
render() {
console.log("2. Hello render");
const { message } = this.state;
return (
<div>
<h2>{message}</h2>
<p>{message}这是子组件</p>
<button onClick={() => this.updateText()}>修改文本</button>
</div>
);
}
// 3.组件被渲染到DOM:被挂载到DOM
componentDidMount() {
console.log("Hello组件挂载完成");
}
// 4. 组件的DOM被更新完成:DOM发生更新
componentDidUpdate() {
console.log("Hello组件更新完成");
}
// 5.组件从DOM中卸载掉:从DOM移除掉
componentWillUnmount() {
console.log("Hello组件卸载完成");
}
// 不常用的生命周期函数
shouldComponentUpdate() {
return true; //false 不会更新数据
}
// 等等
}
export default Hello;
运行效果
4.3 不常用生命周期函数
- pgetSnapshotBeforeUpdate:在 React 更新 DOM 之前回调的一个函数,可以获取 DOM 更新前的一些信息(比如说滚动位置);
- pgetDerivedStateFromProps:state 的值在任何时候都依赖于 props 时使用;该方法返回一个对象来更新 state;
- 等等
4.4 总结:新旧版生命周期函数
旧版生命周期函数:
- 初始化阶段: 由 ReactDOM.render()触发—初次渲染 1. constructor() 2. componentWillMount() 3. render() 4. componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息- 更新阶段: 由组件内部 this.setSate()或父组件 render 触发 1. shouldComponentUpdate() 2. componentWillUpdate() 3. render() =====> 必须使用的一个 4. componentDidUpdate()
- 卸载组件: 由 ReactDOM.unmountComponentAtNode()触发 1. componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
新版生命周期函数:
- 初始化阶段: 由 ReactDOM.render()触发—初次渲染 1. constructor() 2. getDerivedStateFromProps 3. render() 4. componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息- 更新阶段: 由组件内部 this.setSate()或父组件重新 render 触发 1. getDerivedStateFromProps 2. shouldComponentUpdate() 3. render() 4. getSnapshotBeforeUpdate 5. componentDidUpdate()
- 卸载组件: 由 ReactDOM.unmountComponentAtNode()触发 1. componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
5.组件通信
5.1 父传子(props)
- 父组件通过 属性**=值** 的形式来传递给子组件数据;
- 子组件通过 props 参数获取父组件传递过来的数据;
父组件:Main:
export class Main extends Component {
constructor() {
super();
this.state = {
banners: [],
productList: [],
};
}
componentDidMount() {
axios.get("http://123.207.32.32:8000/home/multidata").then((res) => {
const banners = res.data.data.banner.list;
const recommend = res.data.data.recommend.list;
this.setState({
banners,
productList: recommend,
});
});
}
render() {
const { banners, productList } = this.state;
return (
<div className="main">
<div>Main</div>
<MainBanner banners={banners} title="紫陌" />
<MainBanner />
<MainProductList productList={productList} />
</div>
);
}
}
子组件:MainBanner
import React, { Component } from "react";
import PropTypes from "prop-types";
export class MainBanner extends Component {
/* es2022的语法等同于下面 MainBanner.defaultProps */
// static defaultProps = {
// banners:[],
// title:"默认标题"
// }
render() {
// console.log(this.props)
const { title, banners } = this.props;
return (
<div className="banner">
<h2>封装一个轮播图: {title}</h2>
<ul>
{banners?.map((item) => {
return <li key={item.acm}>{item.title}</li>;
})}
</ul>
</div>
);
}
}
//MainBanner传入props类型进行验证
MainBanner.propTypes = {
banners: PropTypes.array,
title: PropTypes.string,
};
//MainBanner传入的props的默认值
MainBanner.defaultProps = {
banners: [],
title: "默认标题",
};
export default MainBanner;
props 类型验证:
- 从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
- 更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
- 比如验证数组,并且数组中包含哪些元素;
- 比如验证对象,并且对象中包含哪些 key 以及 value 是什么类型;
- 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired
- 如果没有传递,我们希望有默认值呢?
- 我们使用 defaultProps 就可以了
禹神案例:
1. props限制案例:
----------------------------------------------------------------------------------------------------------
//创建组件
class Person extends React.Component{
render(){
// console.log(this);
const {name,age,sex} = this.props
//props是只读的
//this.props.name = 'jack' //此行代码会报错,因为props是只读的
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age+1}</li>
</ul>
)
}
}
//对标签属性进行类型、必要性的限制
Person.propTypes = {
name:PropTypes.string.isRequired, //限制name必传,且为字符串
sex:PropTypes.string,//限制sex为字符串
age:PropTypes.number,//限制age为数值
speak:PropTypes.func,//限制speak为函数
}
//指定默认标签属性值
Person.defaultProps = {
sex:'男',//sex默认值为男
age:18 //age默认值为18
}
.............
function speak(){
console.log('我说话了');
}
2.props的简写方式也就是利用es2022语法
---------------------------------------------------------------------------------------------------
//创建组件
class Person extends React.Component{
constructor(props){
//构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
// console.log(props);
super(props)
console.log('constructor',this.props);
}
//对标签属性进行类型、必要性的限制
static propTypes = {
name:PropTypes.string.isRequired, //限制name必传,且为字符串
sex:PropTypes.string,//限制sex为字符串
age:PropTypes.number,//限制age为数值
}
//指定默认标签属性值
static defaultProps = {
sex:'男',//sex默认值为男
age:18 //age默认值为18
}
render(){
// console.log(this);
const {name,age,sex} = this.props
//props是只读的
//this.props.name = 'jack' //此行代码会报错,因为props是只读的
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age+1}</li>
</ul>
)
}
}
-----------------------------------------------------------------------------------------------------
3. 函数式组件传递props
//创建组件
function Person (props){
const {name,age,sex} = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
Person.propTypes = {
name:PropTypes.string.isRequired, //限制name必传,且为字符串
sex:PropTypes.string,//限制sex为字符串
age:PropTypes.number,//限制age为数值
}
//指定默认标签属性值
Person.defaultProps = {
sex:'男',//sex默认值为男
age:18 //age默认值为18
}
//渲染组件到页面
ReactDOM.render(<Person name="jerry"/>,document.getElementById('test1'))
5.2 子传父
- 在 vue 中是通过自定义事件来完成的;
- 在 React 中是通过 props 传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
案例:
父组件:APP
export class App extends Component {
constructor() {
super();
this.state = {
counter: 100,
};
}
//调用修改变量
changeCounter(count) {
this.setState({
counter: this.state.counter + count,
});
}
render() {
const { counter } = this.state;
return (
<div>
<h2>当前计数为:{counter}</h2>
<AddCounter addClick={(count) => this.changeCounter(count);}/>
</div>
);
}
}
子组件:AddCounter
export class AddCounter extends Component {
addCount(count) {
//拿到父组件定义的函数传变量
this.props.addClick(count);
}
render() {
return (
<div>
<button
onClick={(e) => {
this.addCount(10);
}}
>
点我加10
</button>
</div>
);
}
}
图解:
6.React 中的插槽
- Vue 中有一个固定的做法是通过 slot 来完成插槽效果
- React 对于这种需要插槽的情况非常灵活,有两种方案可以实现:
- 组件的 children 子元素;
- props 属性传递 React 元素;
-
第一种方式:每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。
children 实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;
父组件:APP
export class App extends Component {
render() {
return (
<div>
{/* 1.使用children实现插槽 */}
<NavBar>
<button>按钮</button>
<h2>zimo</h2>
<span>紫陌</span>
</NavBar>
</div>
);
}
}
子组件:NavBar
export class NavBar extends Component {
render() {
const { children } = this.props;
return (
<div className="nav-bar">
<div className="left">{children[0]}</div>
<div className="center">{children[1]}</div>
<div className="right">{children[2]}</div>
</div>
);
}
}
2.第二种方式:另外一个种方案就是使用 props 实现:通过具体的属性名,可以让我们在传入和获取时更加的精准;
父组件:APP
export class App extends Component {
render() {
const spanEl = <span>紫陌</span>;
return (
<div>
{/* 2.使用props实现插槽 */}
<NavBar leftSlot={<button>按钮2</button>} centerSlot={<h2>zimo</h2>} rightSlot={spanEl} />
</div>
);
}
}
子组件:NavBar
export class NavBar extends Component {
render() {
const { leftSlot, centerSlot, rightSlot } = this.props;
return (
<div className="nav-bar">
<div className="left">{leftSlot}</div>
<div className="center">{centerSlot}</div>
<div className="right">{rightSlot}</div>
</div>
);
}
}
react 作用域插槽效果实现
tabs 案例演示:
7. 非父子通信(context)
- Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
7.1context 相关 API
-
React.createContext
-
创建一个需要共享的 Context 对象:
-
如果一个组件订阅了 Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的 context 值;
-
defaultValue 是组件在顶层查找过程中没有找到对应的 Provider,那么就使用默认值
-
// 1.创建一个Context const userContext = React.createContext(defaultValue);
-
-
Context.Provider
-
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
-
Provider 接收一个 value 属性,传递给消费组件;
-
一个 Provider 可以和多个消费组件有对应关系;
-
多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
-
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
-
<MyContext.Provider value={/* 某个值*/}
-
-
Class.contextType
-
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
-
这能让你使用 this.context 来消费最近 Context 上的那个值;
-
你可以在任何生命周期中访问到它,包括 render 函数中;
-
//设置组件的contextType为某一个Context //class.contextType = ThemeContext HomeInfo.contextType = ThemeContext;
- Context.Consumer
-
这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
-
这里需要 函数作为子元素(function as child)这种做法;
-
这个函数接收当前的 context 值,返回一个 React 节点;
-
<UserContext.Consumer> {(value) => { return <h2>Info User: {value.nickname}</h2>; }} </UserContext.Consumer>
-
什么时候使用Context.Consumer 呢?
当使用 value 的组件是一个函数式组件时;
当组件中需要使用多个 Context 时;
案例一:
APP 父组件:
theme-context.jsx
--------------------------------------------------------------------------------------------------------
import React from "react"
// 1.创建一个Context
const ThemeContext = React.createContext({ color: "blue", size: 10 })
export default ThemeContext
user-context.jsx
---------------------------------------------------------------------------------------------------------
import React from "react"
// 1.创建一个Context
const UserContext = React.createContext()
export default UserContext
APP.jsx
----------------------------------------------------------------------------------------------------------
import React, { Component } from 'react'
import Home from './Home'
import ThemeContext from "./context/theme-context"
import UserContext from './context/user-context'
import Profile from './Profile'
export class App extends Component {
render() {
return (
<div>
<h2>App</h2>
{/* 通过ThemeContext中Provider中value属性为后代提供数据 */}
<UserContext.Provider value={{nickname: "kobe", age: 30}}>
<ThemeContext.Provider value={{color: "red", size: "30"}}>
<A/>
</ThemeContext.Provider>
</UserContext.Provider>
<Profile/>
</div>
)
}
}
A 组件:
export class Home extends Component {
render() {
return (
<div>
<B />
<C />
</div>
);
}
}
B 组件:类组件
import ThemeContext from "./context/theme-context";
import UserContext from "./context/user-context";
export class B extends Component {
render() {
// 4.第四步操作: 获取数据, 并且使用数据
console.log(this.context);
return (
<div>
{/*获取最近的context */}
<h2>HomeInfo: {this.context.color}</h2>
{/*获取指定的context */}
<UserContext.Consumer>
{(value) => {
return <h2>Info User: {value.nickname}</h2>;
}}
</UserContext.Consumer>
</div>
);
}
}
// 3.第三步操作: 设置组件的contextType为某一个Context
HomeInfo.contextType = ThemeContext;
C 组件:函数组件
import ThemeContext from "./context/theme-context";
function C() {
return (
<div>
{/* 函数式组件中使用Context共享的数据 */}
<ThemeContext.Consumer>
{(value) => {
return <h2> Banner theme:{value.color}</h2>;
}}
</ThemeContext.Consumer>
</div>
);
}
export default HomeBanner;
案例二:
import React, { Component } from "react";
//创建Context对象
const MyContext = React.createContext();
const { Provider, Consumer } = MyContext;
export default class A extends Component {
state = { username: "tom", age: 18 };
render() {
const { username, age } = this.state;
return (
<div className="parent">
<h3>我是A组件</h3>
<h4>我的用户名是:{username}</h4>
<Provider value={{ username, age }}>
<B />
</Provider>
</div>
);
}
}
class B extends Component {
render() {
return (
<div className="child">
<h3>我是B组件</h3>
<C />
</div>
);
}
}
class C extends Component {
//声明接收context
static contextType = MyContext;
render() {
const { username, age } = this.context;
return (
<div className="grand">
<h3>我是C组件</h3>
<h4>
我从A组件接收到的用户名:{username},年龄是{age}
</h4>
</div>
);
}
}
function C() {
return (
<div className="grand">
<h3>我是C组件</h3>
<h4>
我从A组件接收到的用户名:
<Consumer>{(value) => `${value.username},年龄是${value.age}`}</Consumer>
</h4>
</div>
);
}
在应用开发中一般不用context, 一般都它的封装react插件;
8.setState 地使用
- 开发中我们并不能直接通过修改 state 的值来让界面发生更新:
- 因为我们修改了 state 之后,希望 React 根据最新的 State 来重新渲染界面,但是这种方式的修改 React 并不知道数据发生了变化;
- React 并没有实现类似于 Vue2 中的 Object.defineProperty 或者 Vue3 中的 Proxy 的方式来监听数据的变化;
- 我们必须通过 setState 来告知 React 数据已经发生了变化;
8.1 setState 三种用法
案例一:
export class App extends Component {
constructor(props) {
super(props);
this.state = {
message: "Hello Zimo",
};
}
changeText() {
// 1.基本使用
this.setState({
message: "你好啊, 紫陌",
});
// 2.setState可以传入一个回调函数
// 好处一: 可以在回调函数中编写新的state的逻辑
// 好处二: 当前的回调函数会将之前的state和props传递进来
this.setState((state, props) => {
// 1.编写一些对新的state处理逻辑
// 2.可以获取之前的state和props值
console.log(this.state.message, this.props);
return {
message: "你好啊, 紫陌",
};
});
// 3.setState在React的事件处理中是一个异步调用
// 如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码
// 那么可以在setState中传入第二个参数: callback
this.setState({ message: "你好啊, 紫陌" }, () => {
console.log("++++++:", this.state.message); //后打印,setState异步
});
console.log("------:", this.state.message); //先打印
}
render() {
const { message } = this.state;
return (
<div>
<h2>message: {message}</h2>
<button onClick={(e) => this.changeText()}>修改文本</button>
</div>
);
}
}
案例二:宇哥
import React, { Component } from "react";
export default class Demo extends Component {
state = { count: 0 };
add = () => {
//对象式的setState
//1.获取原来的count值
const { count } = this.state;
//2.更新状态
this.setState({ count: count + 1 }, () => {
console.log(this.state.count);
});
//console.log('12行的输出',this.state.count); //0
//函数式的setState
this.setState((state) => ({ count: state.count + 1 }));
};
render() {
return (
<div>
<h1>当前求和为:{this.state.count}</h1>
<button onClick={this.add}>点我+1</button>
</div>
);
}
}
8.2 为什么 setState 设计为什么异步?
- setState 设计为异步,可以显著的提升性能
- 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的;
- 最好的办法应该是获取到多个更新,之后进行批量更新;
- 如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步;
- state 和 props 不能保持一致性,会在开发中产生很多的问题;
案例:
import React, { Component } from "react";
function Hello(props) {
return <h2>{props.message}</h2>;
}
export class App extends Component {
constructor(props) {
super(props);
this.state = {
message: "Hello Zimo",
counter: 0,
};
}
changeText() {
this.setState({ message: "你好啊,紫陌" });
console.log(this.state.message);
}
increment() {
console.log("------");
//以下三个最终是counter:1
// this.setState({
// counter: this.state.counter + 1
// })
// this.setState({
// counter: this.state.counter + 1
// })
// this.setState({
// counter: this.state.counter + 1
// })
//以下结果是counter:3
// this.setState((state) => {
// return {
// counter: state.counter + 1
// }
// })
// this.setState((state) => {
// return {
// counter: state.counter + 1
// }
// })
// this.setState((state) => {
// return {
// counter: state.counter + 1
// }
// })
}
render() {
const { message, counter } = this.state;
console.log("render被执行");
return (
<div>
<h2>message: {message}</h2>
<button onClick={(e) => this.changeText()}>修改文本</button>
<h2>当前计数: {counter}</h2>
<button onClick={(e) => this.increment()}>counter+1</button>
<Hello message={message} />
</div>
);
}
}
8.3setState 一定是异步吗
- React18 之前
- 分成两种情况:
- 在组件生命周期或 React 合成事件中,setState 是异步;
- 在 setTimeout 或者原生 dom 事件中,setState 是同步;
- React18 之后
setState 默认是异步的
在*React18 之后,默认所有的操作都被放到了批处理中(异步处理)
如果希望代码可以同步会拿到,则需要执行特殊的 flushSync 操作
import { flushSync } from 'react-dom'
changeText() {
setTimeout(() => {
// 在react18之前, setTimeout中setState操作, 是同步操作
// 在react18之后, setTimeout中setState异步操作(批处理)
flushSync(() => {
this.setState({ message: "你好啊, 李银河" })
})
console.log(this.state.message)
}, 0);
}
9. render 函数优化(PureComponent,memo)
- 修改了 App 中的数据,所有的组件都需要重新 render,进行 diff 算法, 性能必然是很低的,很多的组件没有必须要重新 render。它们调用 render 应该有一个前提,就是依赖的数据(state、 props)发生改变时,再调用自己的 render 方法;
- 如何来控制 render 方法是否被调用呢?
- 通过 shouldComponentUpdate 方法即可;
9.1shouldComponentUpdate
-
React 给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为 SCU),这个方法接受参数,并且需要有 返回值:
-
该方法有两个参数:
-
参数一:nextProps 修改之后,最新的 props 属性
-
参数二:nextState 修改之后,最新的 state 属性
-
该方法返回值是一个 boolean 类型:
-
返回值为 true,那么就需要调用 render 方法;
-
返回值为 false,那么久不需要调用 render 方法
-
默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法;
9.2PureComponent
- 如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
- 我们来设想一下 shouldComponentUpdate 中的各种判断的目的是什么?
- props 或者 state 中的数据是否发生了改变,来决定 shouldComponentUpdate 返回 true 或者 false;
- 事实上 React 已经考虑到了这一点,所以 React 已经默认帮我们实现好了
- 将 class 继承自 PureComponent。
9.3 高阶组件 memo
- 函数式组件我们在 props 没有改变时,也是不希望其重新渲染其 DOM 树结构的
- 我们需要使用一个高阶组件 memo:
案例
APP组件------------------------------------------------------------
import React, { PureComponent } from 'react'
import Home from './Home'
import Recommend from './Recommend'
import Profile from './Profile'
export class App extends PureComponent {
constructor() {
super()
this.state = {
message: "Hello World",
counter: 0
}
}
// shouldComponentUpdate(nextProps, newState) {
// // App进行性能优化的点
// if (this.state.message !== newState.message || this.state.counter !== newState.counter) {
// return true
// }
// return false
// }
changeText() {
this.setState({ message: "你好啊,紫陌!" })
// this.setState({ message: "Hello World" })
}
increment() {
this.setState({ counter: this.state.counter + 1 })
}
render() {
console.log("App render")
const { message, counter } = this.state
return (
<div>
<h2>App-{message}-{counter}</h2>
<button onClick={e => this.changeText()}>修改文本</button>
<button onClick={e => this.increment()}>counter+1</button>
<Home message={message}/>
<Recommend counter={counter}/>
<Profile message={message}/>
</div>
)
}
}
export default App
Home组件----------------------------------------------------------------------------------------
import React, { PureComponent } from 'react'
export class Home extends PureComponent {
constructor(props) {
super(props)
this.state = {
friends: []
}
}
// shouldComponentUpdate(newProps, nextState) {
// // 自己对比state是否发生改变: this.state和nextState
// if (this.props.message !== newProps.message) {
// return true
// }
// return false
// }
render() {
console.log("Home render")
return (
<div>
<h2>Home Page: {this.props.message}</h2>
</div>
)
}
}
export default Home
Profile组件----------------------------------------------------------------------------------------------------
import { memo } from "react"
const Profile = memo(function(props) {
console.log("profile render")
return <h2>Profile: {props.message}</h2>
})
export default Profile
10.数据不可变(state)
修改 state 数据必须把 state 数据拷贝出来替换掉原来的数据,PureComponent 才可以调用 render 函数。
性能优化 – React (reactjs.org) 数据不可变
import React, { PureComponent } from "react";
export class App extends PureComponent {
constructor() {
super();
this.state = {
books: [
{ name: "你不知道JS", price: 99, count: 1 },
{ name: "JS高级程序设计", price: 88, count: 1 },
{ name: "React高级设计", price: 78, count: 2 },
{ name: "Vue高级设计", price: 95, count: 3 },
],
friend: {
name: "kobe",
},
message: "Hello World",
};
}
addNewBook() {
const newBook = { name: "Angular高级设计", price: 88, count: 1 };
// 1.直接修改原有的state, 重新设置一遍
// 在PureComponent是不能重新渲染(render)
/* this.state.books.push(newBook)
this.setState({ books: this.state.books }) */
// 2.赋值一份books, 在新的books中修改, 设置新的books保证不是同一份books才会刷新render
const books = [...this.state.books];
books.push(newBook);
this.setState({ books: books });
}
addBookCount(index) {
// this.state.books[index].count++ 这个也不能重新渲染render
const books = [...this.state.books];
books[index].count++;
this.setState({ books: books });
}
render() {
const { books } = this.state;
return (
<div>
<h2>数据列表</h2>
<ul>
{books.map((item, index) => {
return (
<li key={index}>
<span>
name:{item.name}-price:{item.price}-count:{item.count}
</span>
<button onClick={(e) => this.addBookCount(index)}>+1</button>
</li>
);
})}
</ul>
<button onClick={(e) => this.addNewBook()}>添加新书籍</button>
</div>
);
}
}
export default App;
11.ref 获取 DOM
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
- 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性;
- 你不能在函数组件上使用 ref 属性,因为他们没有实例;可以通过 React.forwardRef
11.1ref 获取 DOM
- 方式一:传入字符串
- 使用时通过 this.refs.传入的字符串格式获取对应的元素;
- 方式二:传入一个对象
- 对象是通过 React.createRef() 方式创建出来的;
- 使用时获取到创建的对象其中有一个 current 属性就是对应的元素;
- 方式三:传入一个函数
- 该函数会在 DOM 被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;
- 使用时,直接拿到之前保存的元素对象即可;
import React, { PureComponent, createRef } from "react";
export class App extends PureComponent {
constructor() {
this.titleRef = createRef();
this.titleEl = null;
}
getNativeDOM() {
// 1.方式一: 在React元素上绑定一个ref字符串
// console.log(this.refs.why)
// 2.方式二: 提前创建好ref对象, createRef(), 将创建出来的对象绑定到元素
// console.log(this.titleRef.current)
// 3.方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入
console.log(this.titleEl);
}
render() {
return (
<div>
<h2 ref="why">Hello 紫陌</h2>
<h2 ref={this.titleRef}>你好zimo</h2>
<h2 ref={(el) => (this.titleEl = el)}>你好啊紫陌</h2>
<button onClick={(e) => this.getNativeDOM()}>获取DOM</button>
</div>
);
}
}
export default App;
11.2 ref 获取类组件实例
import React, { PureComponent, createRef } from "react";
class HelloWorld extends PureComponent {
test() {
console.log("test------");
}
render() {
return <h1>Hello World</h1>;
}
}
export class App extends PureComponent {
constructor() {
super();
this.hwRef = createRef();
}
getComponent() {
console.log(this.hwRef.current);
this.hwRef.current.test();
}
render() {
return (
<div>
<HelloWorld ref={this.hwRef} />
<button onClick={(e) => this.getComponent()}>获取组件实例</button>
</div>
);
}
}
export default App;
11.3 ref 获取函数组实例
import React, { PureComponent, createRef, forwardRef } from "react";
const HelloWorld = forwardRef(function (props, ref) {
return (
<div>
<h1 ref={ref}>Hello World</h1>
<p>哈哈哈</p>
</div>
);
});
export class App extends PureComponent {
constructor() {
super();
this.hwRef = createRef();
}
getComponent() {
console.log(this.hwRef.current);
}
render() {
return (
<div>
<HelloWorld ref={this.hwRef} />
<button onClick={(e) => this.getComponent()}>获取组件实例</button>
</div>
);
}
}
export default App;
12.受控和非受控组件
-
表单元素通常自己维护 state,并根据用户输入进 行更新。
-
在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
-
我们将两者结合起来,使 React 的 state 成为“唯一数据源”; 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;
-
被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;
-
由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。
-
类似 vue 的双向绑定
12.1 受控组件
class App extends PureComponent {
constructor(props) {
super();
this.state = {
name: "zimo",
};
}
inputChange(e) {
console.log(e.target.value);
this.setState({
name: e.target.value,
});
}
render() {
const { name } = this.state;
return (
<div>
{/* 受控组件 */}
<input type="text" value={name} onChange={(e) => this.inputChange(e)} />
<h2>{name}</h2>
{/* 非受控组件 */}
<input type="text" />
</div>
);
}
}
各种表单收集数据例子
class App extends PureComponent {
constructor(props) {
super();
this.state = {
username: "",
password: "",
isAGree: false,
hobbies: [
{ value: "sing", text: "唱", isChecked: false },
{ value: "dance", text: "跳", isChecked: false },
{ value: "rap", text: "rap", isChecked: false },
],
fruit: ["orange"],
};
}
handleSubmitClick(event) {
// 1.阻止默认行为
event.preventDefault();
// 2.获取到所有的表单数据, 对数据进行组织
console.log("获取所有的输入内容");
console.log(this.state.username, this.state.password);
const hobbies = this.state.hobbies.filter((item) => item.isChecked).map((item) => item.value);
console.log("获取爱好: ", hobbies);
// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
}
handleInputChage(event) {
console.log(event);
this.setState({
[event.target.name]: event.target.value,
});
}
handleAgreeChange(event) {
this.setState({
isAGree: event.target.checked,
});
}
handleHobbiesChange(evnet, index) {
const hobbies = [...this.state.hobbies];
hobbies[index].isChecked = evnet.target.checked;
this.setState({ hobbies });
}
handleFruitChange(event) {
const options = Array.from(event.target.selectedOptions);
const values = options.map((item) => item.value);
this.setState({ fruit: values });
// 效果同上
const values2 = Array.from(event.target.selectedOptions, (item) => item.value);
console.log(values2);
}
render() {
const { username, password, isAGree, hobbies, fruit } = this.state;
return (
<div>
{/* 表单 */}
<form onSubmit={(e) => this.handleSubmitClick(e)}>
{/* 1.用户名密码 */}
<div>
<label htmlFor="username">
用户名:
<input
type="text"
id="username"
value={username}
name="username"
onChange={(e) => this.handleInputChage(e)}
/>
</label>
<label htmlFor="password">
密码:
<input
type="password"
id="password"
value={password}
name="password"
onChange={(e) => this.handleInputChage(e)}
/>
</label>
</div>
{/* 2.checkbox 单选*/}
<label htmlFor="agree">
<input type="checkbox" id="agree" checked={isAGree} onChange={(e) => this.handleAgreeChange(e)} />
同意协议
</label>
{/* 3.checkbox */}
<div>
你的爱好:
{hobbies.map((item, index) => {
return (
<label htmlFor={item.value} key={item.value}>
<input
type="checkbox"
value={item.value}
checked={item.isChecked}
id={item.value}
onChange={(e) => this.handleHobbiesChange(e, index)}
/>
<span>{item.text}</span>
</label>
);
})}
</div>
{/* 4.select 多选*/}
<select value={fruit} onChange={(e) => this.handleFruitChange(e)} multiple>
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
{/* 提交按钮 */}
<button type="submit">注册</button>
</form>
</div>
);
}
}
export default App;
12.2 非受控组件
- React 推荐大多数情况下使用 受控组件 来处理表单数据: 一个受控组件中,表单数据是由 React 组件来管理的;
- 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
- 如果要使用非受控组件中的数据,那么我们需要使用 ref 来从 DOM 节点中获取表单数据。
- 使用 ref 来获取 input 元素, 在非受控组件中通常使用来设置默认
- 开发非受控组件用的比较少
import React, { PureComponent, createRef } from "react";
class App extends PureComponent {
constructor(props) {
super();
this.state = {
intro: "紫陌yyds",
};
this.introRef = createRef();
}
handleSubmitClick(event) {
// 1.阻止默认行为
event.preventDefault();
// 2.获取到所有的表单数据, 对数据进行组织
console.log("ref", this.introRef.current.value);
// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
}
render() {
const { intro } = this.state;
return (
<div>
{/* 非受控组件 */}
<input type="text" defaultValue={intro} ref={this.introRef} />
{/* 提交按钮 */}
<button type="submit">注册</button>
</div>
);
}
}
13.高阶组件
- 高阶函数的定义至少满足以下条件之一:
- 接受一个或多个函数作为输入;
- 输出一个函数;
- JavaScript 中比较常见的 filter、map、reduce 都是高阶函数。
- 高阶组件的英文是 Higher-Order Components,简称为 HOC;
- 官方的定义:高阶组件是参数为组件,返回值为新组件的函数;
**高阶组件 本身不是一个组件,而是一个函数,这个函数的参数是一个组件,返回值也是一个组件 **
13.1 高阶组件的定义和作用
import React, { PureComponent } from "react";
// 定义一个高阶组件
function hoc(Cpn) {
// 定义类组件
class NewCpn extends PureComponent {
render() {
return <Cpn name="zimo" />;
}
}
return NewCpn;
}
class Hello extends PureComponent {
render() {
return <div>你好啊,紫陌</div>;
}
}
const HelloHOC = hoc(Hello);
export class App extends PureComponent {
render() {
return (
<div>
<HelloHOC />
</div>
);
}
}
export default App;
13.2 高阶组件应用-props 增强
APP 组件
import React, { PureComponent } from "react";
import enhancedUserInfo from "./hoc/enhanced_props";
import About from "./pages/About";
const Home = enhancedUserInfo(function (props) {
return (
<h1>
Home: {props.name}-{props.level}-{props.banners}
</h1>
);
});
const Profile = enhancedUserInfo(function (props) {
return (
<h1>
Profile: {props.name}-{props.level}
</h1>
);
});
const HelloFriend = enhancedUserInfo(function (props) {
return (
<h1>
HelloFriend: {props.name}-{props.level}
</h1>
);
});
export class App extends PureComponent {
render() {
return (
<div>
<Home banners={["轮播图1", "轮播图2"]} />
<Profile />
<HelloFriend />
<About />
</div>
);
}
}
export default App;
enhancedUserInfo 高阶组件:
import { PureComponent } from "react";
// 定义组件: 给一些需要特殊数据的组件, 注入props
function enhancedUserInfo(OriginComponent) {
class NewComponent extends PureComponent {
constructor(props) {
super(props);
this.state = {
userInfo: {
name: "zimo",
level: 99,
},
};
}
render() {
return <OriginComponent {...this.props} {...this.state.userInfo} />;
}
}
return NewComponent;
}
export default enhancedUserInfo;
About 组件
import React, { PureComponent } from "react";
import enhancedUserInfo from "../hoc/enhanced_props";
export class About extends PureComponent {
render() {
return <div>About: {this.props.name}</div>;
}
}
export default enhancedUserInfo(About);
13.3 高阶组件应用-Context 共享
定义 context theme_context.js
import { createContext } from "react";
const ThemeContext = createContext();
export default ThemeContext;
定义 HOC 函数 with_theme.js
import ThemeContext from "../context/theme_context";
function withTheme(OriginComponent) {
return (props) => {
return (
<ThemeContext.Consumer>
{(value) => {
return <OriginComponent {...value} {...props} />;
}}
</ThemeContext.Consumer>
);
};
}
export default withTheme;
Product 组件:
import React, { PureComponent } from "react";
import withTheme from "../hoc/with_theme";
export class Product extends PureComponent {
render() {
const { color, size } = this.props;
return (
<div>
<h2>
Product:{color} - {size}
</h2>
</div>
);
}
}
export default withTheme(Product);
APP 组件:
import React, { PureComponent } from "react";
import ThemeContext from "./context/theme_context";
import Product from "./pages/Product";
export class App extends PureComponent {
render() {
return (
<div>
<ThemeContext.Provider value={{ color: "red", size: 50 }}>
<Product />
</ThemeContext.Provider>
</div>
);
}
}
export default App;
13.4 渲染判断鉴权
- 在开发中,我们可能遇到这样的场景:
- 某些页面是必须用户登录成功才能进行进入;
- 如果用户没有登录成功,那么直接跳转到登录页面;
- 就可以使用高阶组件来完成鉴权操作:
function loginAuth(OriginComponent) {
return (props) => {
// 从localStorage中获取token
const token = localStorage.getItem("token");
if (token) {
return <OriginComponent {...props} />;
} else {
return <h2>请先登录, 再进行跳转到对应的页面中</h2>;
}
};
}
export default loginAuth;
13.5 生命周期劫持
可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:
下面是高阶组件是计算渲染时间:
import { PureComponent } from "react";
function logRenderTime(OriginComponent) {
return class extends PureComponent {
UNSAFE_componentWillMount() {
this.beginTime = new Date().getTime();
}
componentDidMount() {
this.endTime = new Date().getTime();
const interval = this.endTime - this.beginTime;
console.log(`当前${OriginComponent.name}页面花费了${interval}ms渲染完成!`);
}
render() {
return <OriginComponent {...this.props} />;
}
};
}
export default logRenderTime;
13.6 高阶组件的意义
- 高阶组件可以针对某些 React 代码进行更加优雅的处理。
- 早期的 React 有提供组件之间的一种复用方式是 mixin,目前已经不再建议使用:
- Mixin 可能会相互依赖,相互耦合,不利于代码维护;
- 不同的 Mixin 中的方法可能会相互冲突;
- Mixin 非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;
- HOC 也有缺陷:
- HOC 需要在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难;
- HOC 可以劫持 props,在不遵守约定的情况下也可能造成冲突;
- Hooks 的出现,是开创性的,它解决了很多 React 之前的存在的问题
- 比如 this 指向问题、比如 hoc 的嵌套复杂度问题等等;
14. Portals 的使用
- 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的 DOM 元素中(默认都是挂载到 id 为 root 的 DOM 元素上的)。
- Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
- 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
- 第二个参数(container)是一个 DOM 元素;
createPortal(
App H2
, document.querySelector(“#zimo”))
index.html-------------------------------------------------------------------------------------------------
<div id="root"></div>
<div id="zimo"></div>
App组件:---------------------------------------------------------------------------------------------------
import React, { PureComponent } from 'react'
import { createPortal } from "react-dom"
export class App extends PureComponent {
render() {
return (
<div className='app'>
<h1>App H1</h1>
{/*元素放到zimo容器 */}
{
createPortal(<h2>App H2</h2>, document.querySelector("#zimo"))
}
</div>
)
}
}
export default App
15.Fragment
- 开发中,我们总是在一个组件中返回内容时包裹一个 div 元素:
- 我们又希望可以不渲染这样一个 div,使用 Fragment (类似于 Vue 中的 template)
- Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;
- React 还提供了 Fragment 的短语法:
- 它看起来像空标签 <></> ;
- 如果我们需要在 Fragment 中添加 key,那么就不能使用短语法
import React, { PureComponent, Fragment } from 'react'
export class App extends PureComponent {
constructor() {
super()
this.state = {
sections: [
{ title: "哈哈哈", content: "我是内容, 哈哈哈" },
{ title: "呵呵呵", content: "我是内容, 呵呵呵" },
{ title: "嘿嘿嘿", content: "我是内容, 嘿嘿嘿" },
{ title: "嘻嘻嘻", content: "我是内容, 嘻嘻嘻" },
]
}
}
render() {
const { sections } = this.state
return (
<>
<h2>我是App的标题</h2>
<p>我是App的内容, 哈哈哈哈</p>
<hr />
{
sections.map(item => {
return (
{/*这里不能使用简写 因为key*/}
<Fragment key={item.title}>
<h2>{item.title}</h2>
<p>{item.content}</p>
</Fragment>
)
})
}
</>
)
}
}
export default App
16. StrictMode
- StrictMode 是一个用来突出显示应用程序中潜在问题的工具:
- 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
- 它为其后代元素触发额外的检查和警告
- 严格模式检查仅在开发模式下运行;它们不会影响生产构建;
例子:
- 可以为应用程序的任何部分启用严格模式
- 不会对 Profile 组件运行严格模式检查;
- 但是,Home 以及它们的所有后代元素都将进行检查;
import React, { PureComponent, StrictMode } from "react";
import Home from "./pages/Home";
import Profile from "./pages/Profile";
export class App extends PureComponent {
render() {
return (
<div>
<StrictMode>
<Home />
</StrictMode>
<Profile />
</div>
);
}
}
export default App;
16.1 严格模式检查的是什么
- 识别不安全的生命周期:
- 使用过时的 ref API
- .检查意外的副作用
- 这个组件的 constructor 会被调用两次;
- 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;
- 在生产环境中,是不会被调用两次的;
- 使用废弃的 findDOMNode 方法
- 在之前的 React API 中,可以通过 findDOMNode 来获取 DOM,
- 检测过时的 context API
- 早期的 Context 是通过 static 属性声明 Context 对象属性,通过 getChildContext 返回 Context 对象等方式来使用 Context 的; 目前这种方式已经不推荐使用,
17. React 过渡动画实现
- React 曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-group。
- 这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装
- react-transition-group 本身非常小,不会为我们应用程序增加过多的负担。
npm
npm install react-transition-group --save
yarn
yarn add react-transition-group
17.1 react-transition-group 主要组件
- react-transition-group 主要包含四个组件:
- Transition
- 该组件是一个和平台无关的组件(不一定要结合 CSS);
- 在前端开发中,我们一般是结合 CSS 来完成样式,所以比较常用的是 CSSTransition;
- CSSTransition
- 在前端开发中,通常使用 CSSTransition 来完成过渡动画效果
- SwitchTransition
- 两个组件显示和隐藏切换时,使用该组件
- TransitionGroup
- 将多个动画组件包裹在其中,一般用于列表中元素的动画;
17.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 名称:比如 zimoenter、zimo-enter-active、zimo-enter-done;
- timeout:
- 过渡动画的时间
- appear:
- 是否在初次进入添加动画(需要和 in 同时为 true)
- unmountOnExit:退出后卸载组件
- 其他属性可以参考官网来学习: https://reactcommunity.org/react-transition-group/transition
- CSSTransition 对应的钩子函数:主要为了检测动画的执行过程,来完成一些 JavaScript 的操作
- onEnter:在进入动画之前被触发;
- onEntering:在应用进入动画时被触发;
- onEntered:在应用进入动画结束后被触发;
APP 组件:
import React, { PureComponent } from "react";
import { CSSTransition } from "react-transition-group";
import "./style.css";
export class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
isShow: true,
};
}
render() {
const { isShow } = this.state;
return (
<div>
<button onClick={(e) => this.setState({ isShow: !isShow })}>切换</button>
{/* 无动画 */}
{/* {isShow && <h2>紫陌YYDS</h2>} */}
{/* 有动画 */}
<CSSTransition
// 以下为属性
in={isShow}
unmountOnExit={true}
timeout={2000}
classNames={"zimo"}
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">
<h2>紫陌YYDS</h2>
<p>zimo学前端</p>
</div>
</CSSTransition>
</div>
);
}
}
export default App;
CSS:
.zimo-appear,
.zimo-enter {
opacity: 0;
}
.zimo-appear-active,
.zimo-enter-active {
opacity: 1;
transition: opacity 2s ease;
}
/* 离开动画 */
.zimo-exit {
opacity: 1;
}
.zimo-exit-active {
opacity: 0;
transition: opacity 2s ease;
}
17.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 属性;
-
当 key 值改变时,CSSTransition 组件会重新渲染,也就会触发动画
APP 组件:
import React, { PureComponent } from "react";
import { SwitchTransition, CSSTransition } from "react-transition-group";
import "./style.css";
export class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
isLogin: true,
};
}
render() {
const { isLogin } = this.state;
return (
<div>
<SwitchTransition mode="out-in">
<CSSTransition
// 当key值改变时,CSSTransition组件会重新渲染,也就会触发动画
key={isLogin ? "login" : "exit"}
classNames={"login"}
timeout={1000}
>
<button onClick={(e) => this.setState({ isLogin: !isLogin })}>{isLogin ? "退出" : "登录"}</button>
</CSSTransition>
</SwitchTransition>
</div>
);
}
}
export default App;
CSS :
.login-enter {
transform: translateX(100px);
opacity: 0;
}
.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;
transition: all 1s ease;
}
17.4 TransitionGroup 动画
当我们有一组动画时,需要将这些 CSSTransition 放入到一个 TransitionGroup 中来完成动画
APP 组件:
import React, { PureComponent } from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import "./style.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}</span>
<button onClick={(e) => this.removeBook(index)}>删除</button>
</li>
</CSSTransition>
);
})}
</TransitionGroup>
<button onClick={(e) => this.addNewBook()}>添加新书籍</button>
</div>
);
}
}
export default App;
CSS:
.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;
}
18. React 中的 CSS
- css 一直是 React 的痛点,也是被很多开发者吐槽、诟病的一个点。
- 在这一点上,Vue 做的要好于 React
- React 官方并没有给出在 React 中统一的样式风格: 普通的 css,到 css modules,再到 css in js,有几十种不同的解决方案,上百个不同的库; 大家一致在寻找最好的或者说最适合自己的 CSS 方案,但是到目前为止也没有统一的方案;
18.1 内联样式
- 内联样式是官方推荐的一种 css 样式的写法:
- style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
- 并且可以引用 state 中的状态来设置相关的样式;
- 内联样式的优点:
- 内联样式, 样式之间不会有冲突
- 可以动态获取当前 state 中的状态
- 内联样式的缺点:
- 写法上都需要使用驼峰标识
- 某些样式没有提示
- 大量的样式, 代码混乱
- 某些样式无法编写(比如伪类/伪元素)
export class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
size: 12,
};
}
addTitleSize() {
this.setState({
size: this.state.size + 2,
});
}
render() {
const { size } = this.state;
return (
<div>
<button onClick={(e) => this.addTitleSize()}>字体变大</button>
<h2 style={{ fontSize: `${size}px` }}>我是紫陌</h2>
<h2 style={{ color: "yellowGreen" }}>我是紫陌</h2>
</div>
);
}
}
18.2 普通的 css
- 普通的 css 我们通常会编写到一个单独的文件,之后再进行引入。
- 这样的编写方式和普通的网页开发中编写方式是一致的:
- 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
- 但是普通的 css都属于全局的 css,样式之间会相互影响;
- 这种编写方式最大的问题是样式之间会相互层叠掉;
18.3 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 中很受欢迎的一种方式。
Home.module.css
.section {
border: 1px solid skyblue;
}
.title {
color: purple;
}
Home.jsx
import React, { PureComponent } from "react";
import homeStyle from "./Home.module.css";
export class Home extends PureComponent {
render() {
return (
<div className={homeStyle.section}>
<div className={homeStyle.title}>Home的标题</div>
</div>
);
}
}
export default Home;
18.4 CSS in JS
-
官方文档也有提到过 CSS in JS 这种方案:
- “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义
- 注意此功能并不是 React 的一部分,而是由第三方库提供;
- React 对样式如何定义并没有明确态度;
-
在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。
- React 的思想中认为逻辑本身和 UI 是无法分离的,所以才会有了 JSX 的语法。
- CSS-in-JS 的模式就是一种将样式(CSS)也写入到 JavaScript 中的方式,并且可以方便的使用 JavaScript 的状态;
- 所以 React 有被人称之为 All in JS;
-
认识 styled-components
- CSS-in-JS 通过 JavaScript 来为 CSS 赋予一些能力,包括类似于CSS 预处理器一样的样式嵌套、函数定义、逻辑复用、动态修 改状态等等; 虽然 CSS 预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;
- 所以,目前可以说 CSS-in-JS 是 React 编写 CSS 最为受欢迎的一种解决方案;
-
目前比较流行的 CSS-in-JS 的库
- styled-components
- emotion
- glamorous
目前可以说 styled-components 依然是社区最流行的 CSS-in-JS 库
-
props、attrs 属性
-
props 可以被传递给 styled 组件 获取 props 需要通过${}传入一个插值函数,props 会作为该函数的参数;
-
这种方式可以有效的解决动态样式的问题;
-
添加 attrs 属性
-
export const SectionWrapper = styled.div.attrs((props) => ({ // 可以通过attrs给标签模板字符串中提供属性 tColor: props.color || "blue", }))` xxx xxx `;
-
-
案例
APP 组件:
import React, { PureComponent } from "react";
import { AppWrapepr, SectionWrapper } from "./style";
export class App extends PureComponent {
constructor() {
super();
this.state = {
size: 30,
color: "yellow",
};
}
render() {
const { size, color } = this.state;
return (
<AppWrapepr>
<SectionWrapper size={size} color={color}>
<h2 className="title">我是紫陌</h2>
<p className="content">我是zimo</p>
<button onClick={(e) => this.setState({ color: "skyblue" })}>修改颜色</button>
</SectionWrapper>
<div className="footer">
<h2>这是底部</h2>
<p>zimo@yyds</p>
</div>
</AppWrapepr>
);
}
}
export default App;
style.js 文件
import styled from "styled-components";
import { primaryColor, largeSize } from "./style/variables";
// 基本使用
export const AppWrapepr = styled.div`
.footer {
border: solid 1px blue;
}
`;
// 子元素单独抽取到一个样式组件
export const SectionWrapper = styled.div.attrs((props) => ({
// 可以通过attrs给标签模板字符串中提供属性
tColor: props.color || "blue",
}))`
border: 1px solid red;
.title {
/* 可以接受外部传入的props */
font-size: ${(props) => props.size}px;
color: ${(props) => props.tColor};
&:hover {
background-color: purple;
}
}
/* 从一个单独文件中引入变量 */
.content {
font-size: ${largeSize}px;
color: ${primaryColor};
}
`;
variables.js 文件 (变量文件)
export const primaryColor = "#ff8822";
export const secondColor = "#ff7788";
export const smallSize = "12px";
export const middleSize = "14px";
export const largeSize = "18px";
- styled 高级特性
styled 设置主题,支持样式的继承
index.jsx(根组件传主题变量)
<ThemeProvider theme={{ color: "purple", size: "50px" }}>
<App />
</ThemeProvider>
APP.jsx
import React, { PureComponent } from "react";
import { HomeWrapper, ButtonWrapper } from "./style";
export class Home extends PureComponent {
render() {
return (
<HomeWrapper>
<h2 className="header">商品列表</h2>
<ButtonWrapper>哈哈哈</ButtonWrapper>
</HomeWrapper>
);
}
}
export default Home;
style.js
import styled from "styled-components";
const Button = styled.button`
border: 1px solid red;
border-radius: 5px;
`;
// 继承
export const ButtonWrapper = styled(Button)`
background-color: #0f0;
color: #fff;
`;
// 主题
export const HomeWrapper = styled.div`
.header {
color: ${(props) => props.theme.color};
font-size: ${(props) => props.theme.size};
}
`;
18.5 React 中添加 class(classnames 库)
React 在 JSX 给了我们开发者足够多的灵活性,你可以像编写 JavaScript 代码一样,通过一些逻辑来决定是否添加某些 class
用于动态添加 classnames 的一个库
yarn add classnames
import React, { PureComponent } from "react";
import classNames from "classnames";
export class App extends PureComponent {
constructor() {
super();
this.state = {
isbbb: true,
isccc: true,
};
}
render() {
const { isbbb, isccc } = this.state;
const classList = ["aaa"];
if (isbbb) classList.push("bbb");
if (isccc) classList.push("ccc");
const classname = classList.join(" ");
return (
<div>
{/* 不推荐 */}
<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>
</div>
);
}
}
语法:
classNames(' foo','bar'); //=>'foo bar'
classNames('foo',{bar: true });//=>·'foo bar'
classNames({'foo-bar': false });//=>''
classNames({ foo: true },{ bar: true });//=>'foo bar'
classNames({ foo: true, bar: true }); //=>.'foo bar'
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); //=>'foo bar baz quux'
classNames(null,false,'bar',undefined,0,1,{baz:null}, '') // 'bar 1'
19.纯函数理解
-
函数式编程中有一个非常重要的概念叫纯函数,JavaScript 符合函数式编程的范式,所以也有纯函数的概念;
-
在 react 开发中纯函数是被多次提及的,比如 react 中组件就被要求像是一个纯函数(为什么是像,因为还有 class 组件),redux 中有一个 reducer 的概念,也是要求 必须是一个纯函数;
-
简单概要纯函数:
- 确定的输入,一定会产生确定的输出;
- 函数在执行过程中,不能产生副作用;
-
纯函数副作用概念:
- 表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响, 比如修改了全局变量,修改参数或者改变外部的存储;
-
纯函数的作用和优势
- 可以安心的编写和安心的使用; 保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的 外部变量是否已经发生了修改; 用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
- React 中就要求我们无论是函数还是 class 声明一个组件,这个组件都必须像纯函数一样,保护它们的 props 不被修改,reducer也被要求是一个纯函数
20. Redux
20.1 Redux 的三大原则
- 单一数据源
- 整个应用程序的state 被存储在一颗 object tree 中,并且这个 object tree只存储在一个 store 中:
- Redux 并没有强制让我们不能创建多个 Store,但是那样做并不利于数据的维护;
- 单一的数据源可以让整个应用程序的 state 变得方便维护、追踪、修改;
- State 是只读的
- 唯一修改 State 的方法一定是触发 action,不要试图在其他地方通过任何的方式来修改 State:
- 这样就确保了 View 或网络请求都不能直接修改 state,它们只能通过 action 来描述自己想要如何修改 state;
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心 race condition(竟态)的问题;
- 使用纯函数来执行修改
- 通过 reducer 将 旧 state 和 actions 联系在一起,并且返回一个新的 State:
- 随着应用程序的复杂度增加,我们可以将 reducer 拆分成多个小的 reducers,分别操作不同 state tree 的一部分;
- 但是所有的 reducer 都应该是纯函数,不能产生任何的副作用;
20.2 Redux 的使用过程
- 创建 Store 来存储这个 state
- 创建 store 时必须创建 reducer; -
- 通过 store.getState 来获取当前的 state;
- 通过 action 来修改 state
- 通过 dispatch 来派发 action;
- 通常 action 中都会有 type 属性,也可以携带其他的数据;
- 修改 reducer 中的处理代码
- reducer 是一个纯函数,不需要直接修改 state;
- 可以在派发 action 之前,监听 store 的变化
20.3Redux 结构划分
- 创建 store/index.js 文件:
- 创建 store/reducer.js 文件:
- 创建 store/actionCreators.js 文件:
- 创建 store/constants.js 文件:
20.4 脱离 React 的 redux 代码
redux 和 react 没有直接的关系,完全可以在 React, Angular, Ember, jQuery, or vanilla JavaScript 中使用 Redux
store/index.js
const { createStore } = require("redux");
const reducer = require("./reducer.js");
// 创建的store
const store = createStore(reducer);
module.exports = store;
reducer.js
const { ADD_NUMBER, CHANGE_NAME } = require("./constants");
// 初始化数据
const initialState = {
name: "张三",
counter: 100,
};
function reducer(state = initialState, action) {
switch (action.type) {
case CHANGE_NAME:
return { ...state, name: action.name };
case ADD_NUMBER:
return { ...state, counter: state.counter + action.num };
}
}
module.exports = reducer;
constants.js
const ADD_NUMBER = "add_number";
const CHANGE_NAME = "change_name";
module.exports = {
ADD_NUMBER,
CHANGE_NAME,
};
actionCreators.js
const { CHANGE_NAME, ADD_NUMBER } = require("./constants.js");
const changeNameAction = (name) => ({
type: CHANGE_NAME,
name,
});
const addNumberAction = (num) => ({
type: ADD_NUMBER,
num,
});
module.exports = {
changeNameAction,
addNumberAction,
};
上面四模块就是 store 的结构
组件派发 Action
const store = require("./store1/index.js");
const { addNumberAction, changeNameAction } = require("./actionCreator.js");
const unsubscribe = store.subscribe(() => {
console.log("订阅数据的变化:", store.getSatte());
});
store.dispatch(changeNameAction("zimo"));
store.dispatch(changeNameAction("紫陌"));
store.dispatch(changeNameAction("yy"));
store.dispatch(addNumberAction(13));
store.dispatch(addNumberAction(167));
store.dispatch(addNumberAction(137));
20.5React 中的 Redux
Store 目录结构代码略过。。。。同上。直接看组件
APP 组件:
import React, { Component } from "react";
import Home from "./pages/home";
import Profile from "./pages/profile";
import "./style.css";
import store from "./store";
class App extends Component {
constructor() {
super();
this.state = {
counter: store.getState().counter,
};
}
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
this.setState({ counter: state.counter });
});
}
render() {
const { counter } = this.state;
return (
<div>
<h2>App counter: {counter}</h2>
<div className="pages">
<Home />
<Profile />
</div>
</div>
);
}
}
export default App;
Home 组件:
import React, { PureComponent } from "react";
import store from "../store";
import { addNumberAction } from "../store/actionCreators";
export class Home extends PureComponent {
constructor() {
super();
this.state = {
counter: store.getState().counter,
};
}
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
this.setState({ counter: state.counter });
});
}
addNumber(num) {
store.dispatch(addNumberAction(num));
}
render() {
const { counter } = this.state;
return (
<div>
<h2>Home counterer: {counter}</h2>
<div>
<button onClick={(e) => this.addNumber(1)}>+1</button>
<button onClick={(e) => this.addNumber(5)}>+5</button>
<button onClick={(e) => this.addNumber(8)}>+8</button>
</div>
</div>
);
}
}
export default Home;
Profile 组件
import React, { PureComponent } from "react";
import store from "../store";
import { addNumberAction } from "../store/actionCreators";
export class profile extends PureComponent {
constructor() {
super();
this.state = {
counter: store.getState().counter,
};
}
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
this.setState({ counter: state.counter });
});
}
addNumber(num) {
store.dispatch(addNumberAction(num));
}
render() {
const { counter } = this.state;
return (
<div>
<h2>Profile Counter: {counter}</h2>
<div>
<button onClick={(e) => this.addNumber(1)}>+1</button>
<button onClick={(e) => this.addNumber(5)}>+5</button>
<button onClick={(e) => this.addNumber(8)}>+8</button>
</div>
</div>
);
}
}
export default profile;
上面就是基本使用。但是有个问题。每次写一个组件就要写一个一套相似的逻辑如图:
20.6 Redux–connect 高阶函数
-
connect 高阶函数 实际上 redux 官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。
安装 react-redux:yarn add react-redux
-
在 index.js 文件中:context 的全局注入一个 store
import App from "./App"; import { Provider } from "react-redux"; import store from "./store"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <Provider store={store}> <App /> </Provider> );
-
About 组件:有加减逻辑
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addNumberAction, subNumberAction } from "../store/actionCreators";
export class About extends PureComponent {
calcNumber(num, isAdd) {
if (isAdd) {
console.log("加", num);
this.props.addNumber(num);
} else {
console.log("减", num);
this.props.subNumber(num);
}
}
render() {
const { counter } = this.props;
return (
<div>
<h2>About Page: {counter}</h2>
<div>
<button onClick={(e) => this.calcNumber(6, true)}>+6</button>
<button onClick={(e) => this.calcNumber(88, true)}>+88</button>
<button onClick={(e) => this.calcNumber(6, false)}>-6</button>
<button onClick={(e) => this.calcNumber(88, false)}>-88</button>
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
counter: state.counter,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addNumber: (num) => {
dispatch(addNumberAction(num));
},
subNumber(num) {
dispatch(subNumberAction(num));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(About);
- connect()函数是一个高阶函数接收两个参数,connect 函数返回一个高阶组件。最后一个括号是放组件的
- 使用了 connect 函数。mapStateToProps 把 store 上的数据映射到了 props。组件通过 props 拿到。action 也是如此。类似 vue 中的 mapState 辅助函数,mapAction 辅助函数
20.7 派发异步的 action
-
redux-thunk 是可以发送异步的请求**,默认情况下的 dispatch(action),action 需要是一个 JavaScript 的对象**;
-
redux-thunk 可以让 dispatch(action 函数),action 可以是一个函数;
-
该函数会被调用,并且会传给这个函数一个 dispatch 函数和 getState 函数;
-
dispatch 函数用于我们之后再次派发 action;
-
getState 函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;
-
使用 redux-thunk
yarn add redux-thunk
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducer from "./reducer.js"; const store = createStore(reducer, applyMiddleware(thunk)); export default store;
**案例代码:异步 action:**组件中通过 props 调用 action 发情网络请求
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { fetchHomeMultidataAction } from "../store/actionCreators";
export class Category extends PureComponent {
componentDidMount() {
this.props.fetchHomeMultidata();
}
render() {
return <h2>Category Page: {this.props.counter}</h2>;
}
}
const mapStateToProps = (state) => {
return {
counter: state.counter,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchHomeMultidata: () => {
dispatch(fetchHomeMultidataAction());
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Category);
actionCreators.js 中:
export const changeBannersAction = (banners) => ({
type: actionTypes.CHANGE_BANNERS,
banners,
});
export const changeRecommendsAction = (recommends) => ({
type: actionTypes.CHANGE_RECOMMENDS,
recommends,
});
export const fetchHomeMultidataAction = () => {
// 普通action
/*
如果是一个普通action,那么我们这里需要返回一个action对象
问题:对象是不能直接拿到服务器请求的异步数据的
return {}
*/
// 异步action处理返回函数
/*
如果返回一个函数那么redux是不支持的。需要借助插件redux-thunk
*/
return (dispatch, getState) => {
//异步操作
axios.get("http://xxxxxx/home/multidata").then((res) => {
const banners = res.data.data.banner.list;
const recommends = res.data.data.recommend.list;
// 派发action
dispatch(changeBannersAction(banners));
dispatch(changeRecommendsAction(recommends));
});
};
};
组件中 mapStateToProps 函数直接获取即可。
20.8 redux-devtools
- 第一步:在对应的浏览器中安装相关的插件(比如 Chrome 浏览器扩展商店中搜索 Redux DevTools 即可)
- 第二步:在 redux 中继承 devtools 的中间件;
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer.js";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
20.9 Redux 模块化
**combineReducers****函数实现模块化**
- combineReducers 是如何实现?
- 事实上,它也是将我们传入的 reducers 合并到一个对象中,最终返回一个 combination 的函数(相当于我们之前的 reducer 函 数了);
- 在执行 combination 函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的 state 还是新的 state;
- 新的 state 会触发订阅者发生对应的刷新,而旧的 state 可以有效的组织订阅者发生刷新;
store/index.js
import { createStore, applyMiddleware, compose, combineReducers } from "redux";
import thunk from "redux-thunk";
import counterReducer from "./counter";
import homeReducer from "./home";
// 将两个reducer合并在一起
const reducer = combineReducers({
counter: counterReducer,
home: homeReducer,
});
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
combineReducers实现原理
import counterReducer from "./counter";
import homeReducer from "./home";
// combineReducers实现原理(了解)
function reducer(state = {}, action) {
// 返回一个对象, store的state
return {
//state.counter 第一次undefined 就拿到默认值
counter: counterReducer(state.counter, action),
home: homeReducer(state.home, action),
};
}
21.ReduxToolkit
- Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。
- 在前面我们学习 Redux 的时候应该已经发现,redux 的编写逻辑过于的繁琐和麻烦。
- 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);
- Redux Toolkit 包旨在成为编写 Redux 逻辑的标准方式,从而解决上面提到的问题;
- 在很多地方为了称呼方便,也将之称为“RTK”;
安装 Redux Toolkit:
npm install @reduxjs/toolkit react-redux
- Redux Toolkit 的核心 API 主要是如下几个:
- configureStore:包装 createStore 以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供 的任何 Redux 中间件,redux-thunk 默认包含,并启用 Redux DevTools Extension。
- createSlice:接受 reducer 函数的对象、切片名称和初始状态值,并自动生成切片 reducer,并带有相应的 actions。
- createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个 pending/fulfilled/rejected 基于该承诺分 派动作类型的 thunk
21.Redux Toolkit 使用流程
-
store 的创建
- configureStore 用于创建 store 对象,常见参数如下:
- reducer,将 slice 中的 reducer 可以组成一个对象传入此处;
- middleware:可以使用参数,传入其他的中间件(自行了解);
- devTools:是否配置 devTools 工具,默认为 true;
import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "./features/counter"; import homeReducer from "./features/home"; const store = configureStore({ reducer: { counter: counterReducer, home: homeReducer, }, }); export default store;
-
通过 createSlice 创建一个 slice。
- createSlice 主要包含如下几个参数:
- name:用户标记 slice 的名词, 在之后的 redux-devtool 中会显示对应的名词;
- initialState:初始化值 ,第一次初始化时的值;
- reducers:相当于之前的 reducer 函数
- 对象类型,并且可以添加很多的函数;
- 函数类似于 redux 原来 reducer 中的一个 case 语句;
- 函数的参数:
- 参数一:state
- 参数二:调用这个 action 时,传递的 action 参数;
- createSlice 返回值是一个对象,包含所有的 actions;
store/features/counter.js(counter 的 reducer)
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: {
counter: 999,
},
reducers: {
addNumber(state, { payload }) {
console.log(payload);
state.counter = state.counter + payload;
},
subNumber(state, { payload }) {
state.counter = state.counter - payload;
},
},
});
export const { addNumber, subNumber } = counterSlice.actions;
export default counterSlice.reducer;
组件中展示数据:(这部分热 redux 一样)
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addNumber } from "../store/features/counter";
export class About extends PureComponent {
render() {
const { counter } = this.props;
return (
<div>
<h2>About Page: {counter}</h2>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
counter: state.counter.counter,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addNumber: (num) => {
dispatch(addNumber(num));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(About);
21.2 Toolkit 的异步操作
- 在之前 redux-thunk 中间件让 dispatch 中可以进行异步操作
- Redux Toolkit 默认已经给我们继承了 Thunk 相关的功能:createAsyncThunk
当 createAsyncThunk 创建出来的 action 被 dispatch 时,会存在三种状态:
- pending:action 被发出,但是还没有最终的结果;
- fulfilled:获取到最终的结果(有返回值的结果);
- rejected:执行过程中有错误或者抛出了异常;
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
// 1.
export const fetchHomeMultidataAction = createAsyncThunk("fetch/homemultidata", async () => {
const res = await axios.get("http://123.207.32.32:8000/home/multidata");
return res.data;
});
const homeSlice = createSlice({
name: "home",
initialState: {
banners: [],
recommends: [],
},
reducers: {
changeBanners(state, { payload }) {
state.banners = payload;
},
changeRecommends(state, { payload }) {
state.recommends = payload;
},
},
//2.
extraReducers: {
[fetchHomeMultidataAction.pending](state, action) {
console.log("fetchHomeMultidataAction pending");
},
[fetchHomeMultidataAction.fulfilled](state, { payload }) {
state.banners = payload.data.banner.list;
state.recommends = payload.data.recommend.list;
},
[fetchHomeMultidataAction.rejected](state, action) {
console.log("fetchHomeMultidataAction rejected");
},
},
});
export const { changeBanners, changeRecommends } = homeSlice.actions;
export default homeSlice.reducer;
extraReducers第二种写法(函数式写法)
可以向 builder 中添加 case 来监听异步操作的结果
extraReducers: (builder) => {
builder
.addCase(fetchHomeMultidataAction.pending, (state, action) => {
console.log("fetchHomeMultidataAction-pending");
})
.addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {
state.banners = payload.data.banner.list;
state.recommends = payload.data.recommend.list;
});
};
函数式写法还有一个用法
22.React Router
-
安装时,我们选择 react-router-dom;
-
react-router 会包含一些 react-native 的内容,web 开发并不需要;
npm install react-router-dom
22.1 BrowserRouter 或 HashRouter
- BrowserRouter 使用 history 模式;
- HashRouter 使用 hash 模式;
root.render(
<HashRouter>
<App />
</HashRouter>
);
22.2 路由映射配置
- Routes:包裹所有的 Route,在其中匹配一个路由
- Router5.x 使用的是 Switch 组件
- Route:Route 用于路径的匹配;
- path 属性:用于设置匹配到的路径;
- element 属性:设置匹配到路径后,渲染的组件;
- Router5.x 使用的是 component 属性
- exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
- Router6.x 不再支持该属性
<Routes>
<Route path="/home" element={<Home />}>
<Route path="/home/recommend" element={<HomeRecommend />} />
</Route>
</Routes>
22.3 路由跳转
- Link
to 属性:Link 中最重要的属性,用于设置跳转到的路径
<Link to="/home">首页</Link>
<Link to="/about">关于</Link>
- NavLink
需求:路径选中时,对应的 a 元素变为红色
- 我们要使用 NavLink 组件来替代 Link 组件:
- style:传入函数,函数接受一个对象,包含 isActive 属性
- className:传入函数,函数接受一个对象,包含 isActive 属性
<NavLink to="/home" style={({isActive}) => ({color: isActive ? "red": ""})}>首页</NavLink>
<NavLink to="/about" style={({isActive}) => ({color: isActive ? "red": ""})}>关于</NavLink>
默认的 activeClassName: 事实上在默认匹配成功时,NavLink 就会添加上一个动态的 active class; 所以我们也可以直接编写样式
如果你担心这个 class 在其他地方被使用了,出现样式的层叠,也可以自定义 class
<NavLink to="/home" className={({isActive}) => isActive?"link-active":""}>首页</NavLink>
<NavLink to="/about" className={({isActive}) => isActive?"link-active":""}>关于</NavLink>
-
Navigate
Navigate 用于路由的重定向,当这个组件出现时,就会执行跳转到对应的 to 路径中:
<Route path="/" element={<Navigate to="/home" />} />
23.4Not Found 页面配置
- 开发一个 Not Found 页面;
- 配置对应的 Route,并且设置 path 为*即可;
<Route path="*" element={<NotFound />} />
23.5 路由的嵌套
<Routes>
<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>
</Routes>
23.6 手动路由的跳转
- 跳转主要是通过 Link 或者 NavLink 进行跳转的,实际上我们也可以通过 JavaScript 代码进行跳转。
- Navigate 组件是可以进行路由的跳转的,但是依然是组件的方式。
- 如果我们希望通过 JavaScript 代码逻辑进行跳转(比如点击了一个 button),那么就需要获取到 navigate 对象。
-
函数式使用 hooks 跳转
export function App(props) { const navigate = useNavigate(); function navigateTo(path) { navigate(path); } return ( <div className="app"> <div className="nav"> <button onClick={(e) => navigateTo("/category")}>分类</button> <span onClick={(e) => navigateTo("/order")}>订单</span> </div> <div className="content"> <Routes> <Route path="/category" element={<Category />} /> <Route path="/order" element={<Order />} /> </Routes> </div> </div> ); }
-
类路由跳转(必须使用高阶组件封装才行,第一点实现)
import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"; // 高阶组件: 函数 function withRouter(WrapperComponent) { return function (props) { // 1.导航 const navigate = useNavigate(); // 2.动态路由的参数: /detail/:id const params = useParams(); // 3.查询字符串的参数: /user?name=why&age=18 const location = useLocation(); const [searchParams] = useSearchParams(); const query = Object.fromEntries(searchParams); const router = { navigate, params, location, query }; return <WrapperComponent {...props} router={router} />; }; } export default withRouter;
使用时候:
import { withRouter } from "../hoc"; export class Home extends PureComponent { navigateTo(path) { const { navigate } = this.props.router; navigate(path); } render() { return ( <div> <button onClick={(e) => this.navigateTo("/home/songmenu")}>歌单</button> {/* 占位的组件 */} <Outlet /> </div> ); } } export default withRouter(Home);
23.7 路由参数传递
传递参数有二种方式:
- 动态路由的方式;
- search 传递参数;
- 比如/detail 的 path 对应一个组件 Detail;
- 如果我们将 path 在 Route 匹配时写成/detail/:id,那么 /detail/abc、/detail/123 都可以匹配到该 Route,并且进行显示;
- 这个匹配规则,我们就称之为动态路由;
- 使用动态路由可以为路由传递参数
- search 传递参数
高阶组件:
import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
// 高阶组件: 函数
function withRouter(WrapperComponent) {
return function (props) {
// 1.导航
const navigate = useNavigate();
// 2.动态路由的参数: /detail/:id
const params = useParams();
// 3.查询字符串的参数: /user?name=why&age=18
const location = useLocation();
const [searchParams] = useSearchParams();
const query = Object.fromEntries(searchParams);
const router = { navigate, params, location, query };
return <WrapperComponent {...props} router={router} />;
};
}
export default withRouter;
- search 传递参数
23.8 路由的配置文件(包含路由懒加载)
- 目前路由定义都是直接使用 Route 组件,并且添加属性来完成的。
- 但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
- 在早期的时候,Router 并且没有提供相关的 API,我们需要借助于 react-router-config 完成;
- 在 Router6.x 中,为我们提供了 useRoutes API 可以完成相关的配置;
<div className="content">{useRoutes(routes)}</div>
类似 vue
路由表:
import Home from "../pages/Home";
import HomeRecommend from "../pages/HomeRecommend";
import HomeRanking from "../pages/HomeRanking";
import HomeSongMenu from "../pages/HomeSongMenu";
// import About from "../pages/About"
// import Login from "../pages/Login"
import Category from "../pages/Category";
import Order from "../pages/Order";
import NotFound from "../pages/NotFound";
import Detail from "../pages/Detail";
import User from "../pages/User";
import { Navigate } from "react-router-dom";
import React from "react";
const About = React.lazy(() => import("../pages/About"));
const Login = React.lazy(() => import("../pages/Login"));
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: "/home/songmenu",
element: <HomeSongMenu />,
},
],
},
{
path: "/about",
element: <About />,
},
{
path: "/login",
element: <Login />,
},
{
path: "/category",
element: <Category />,
},
{
path: "/order",
element: <Order />,
},
{
path: "/detail/:id",
element: <Detail />,
},
{
path: "/user",
element: <User />,
},
{
path: "*",
element: <NotFound />,
},
];
export default routes;
路由懒加载:
23.Hooks
- 总结一下 hooks:
- 它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性;
- 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决
- Hook 的使用场景:
- Hook 的出现基本可以代替我们之前所有使用 class 组件的地方;
- 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为 Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
- Hook 只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用
- 请记住 Hook 是:
- 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
- 100% 向后兼容的:Hook 不包含任何破坏性改动。
- 现在可用:Hook 已发布于 v16.8.0。
23.1 useState Hook
-
参数:初始化值,如果不设置为 undefined;
-
返回值:数组,包含两个元素;
- 元素一:当前状态的值(第一调用为初始化值);
- 元素二:设置状态值的函数;
-
useState 的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便
-
也可以在一个组件中定义多个变量和复杂变量(数组、对象)
-
使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
import React, { memo, useState } from "react";
const App = memo(() => {
const [message, setMessage] = useState("zimo");
function changeMessage() {
setMessage("紫陌");
}
return (
<div>
<h2>App:{message}</h2>
<button onClick={changeMessage}>修改文本</button>
</div>
);
});
export default App;
23.1 useEffect Hook
- useEffect Hook 可以让你来完成一些类似于 class 中生命周期的功能;
- 类似于网络请求、手动更新 DOM、一些事件的监听,都是 React 更新 DOM 的一些副作用(Side Effects);
- 所以对于完成这些功能的 Hook 被称之为 Effect Hook;
- useEffect 的解析
- 通过 useEffect 的 Hook,可以告诉 React 需要在渲染后执行某些操作;
- useEffect 要求我们传入一个回调函数,在React 执行完更新 DOM 操作之后,就会回调这个函数;
- 默认情况下,**无论是第一次渲染之后,**还是每次更新之后,都会执行这个 回调函数;
- 需要清除 Effect
- 在 class 组件的编写过程中,某些副作用的代码,我们需要在 componentWillUnmount 中进行清除:
- 比如我们之前的事件总线或 Redux 中手动调用 subscribe;
- 都需要在 componentWillUnmount 有对应的取消订阅;
为什么要在 Effect 中返回一个函数?
- 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
- 如此可以将添加和移除订阅的逻辑放在一起;
- 它们都属于 effect 的一部分;
React 何时清除 effect?
- React 会在组件更新和卸载的时候执行清除操作;
- Effect 在每次渲染的时候都会执行;
import React, { memo, useEffect, useState } from "react";
const App = memo(() => {
const [count, setCount] = useState(0);
// 负责告知React,在执行完当前组件渲染之后要执行的副作用代码
useEffect(() => {
console.log("监听redux中数据变化, 监听eventBus中的zimo事件");
// 返回值:回调函数 => 组件被重新渲染或者组件卸载的时候执行
return () => {
console.log("取消监听redux中数据变化, 取消监听eventBus中的zimo事件");
};
});
return (
<div>
<button onClick={(e) => setCount(count + 1)}>+1({count})</button>
</div>
);
});
export default App;
- 多个 Effect
- 使用 Effect Hook,我们可以将它们分离到不同的 useEffect 中:
- React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
import React, { memo, useEffect } from "react";
import { useState } from "react";
const App = memo(() => {
const [count, setCount] = useState(0);
// 负责告知react, 在执行完当前组件渲染之后要执行的副作用代码
useEffect(() => {
// 1.修改document的title(1行)
console.log("修改title");
});
// 一个函数式组件中, 可以存在多个useEffect
useEffect(() => {
// 2.对redux中数据变化监听(10行)
console.log("监听redux中的数据");
return () => {
// 取消redux中数据的监听
};
});
useEffect(() => {
// 3.监听eventBus中的why事件(15行)
console.log("监听eventBus的zimo事件");
return () => {
// 取消eventBus中的zimo事件监听
};
});
return (
<div>
<button onClick={(e) => setCount(count + 1)}>+1({count})</button>
</div>
);
});
export default App;
- Effect 性能优化
- 默认情况下,useEffect 的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:
- 某些代码我们只是希望执行一次即可,类似于 componentDidMount 和 componentWillUnmount 中完成的事情;(比如网 络请求、订阅和取消订阅);
- 多次执行也会导致一定的性能问题;
useEffect实际上有两个参数:
- 参数一:执行的回调函数
- 参数二:该 useEffect 在哪些 state 发生变化时,才重新执行;(受谁的影响)
如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:
- 这里的两个回调函数分别对应的就是 componentDidMount 和 componentWillUnmount 生命周期函数了;
import React, { memo, useEffect } from "react";
import { useState } from "react";
const App = memo(() => {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("Hello World");
useEffect(() => {
console.log("修改title:", count);
}, [count]);
useEffect(() => {
console.log("监听redux中的数据");
return () => {};
}, []);
useEffect(() => {
console.log("监听eventBus的why事件");
return () => {};
}, []);
useEffect(() => {
console.log("发送网络请求, 从服务器获取数据");
return () => {
console.log("会在组件被卸载时, 才会执行一次");
};
}, []);
return (
<div>
<button onClick={(e) => setCount(count + 1)}>+1({count})</button>
<button onClick={(e) => setMessage("你好啊")}>修改message({message})</button>
</div>
);
});
export default App;
23.3 useContext
在组件中使用共享的 Context 有两种方式
- 类组件可以通过 类名.contextType = MyContext 方式,在类中获取 context;
- 多个 Context 或者在函数式组件中通过 MyContext.Consumer 方式共享 context;
但是多个 Context 共享时的方式会存在大量的嵌套:
- Context Hook 允许我们通过 Hook 来直接获取某个 Context 的值;
index.js-------------------------------------------------------------------------------
root.render(
<UserContext.Provider value={{name: "zimo", level: 99}}>
<ThemeContext.Provider value={{color:"red",size:18}}>
<App />
</ThemeContext.Provider>
</UserContext.Provider>
)
函数组件中---------------------------------------------------------------------------
import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from "./context"
const App = memo(() => {
// 使用Context
const user = useContext(UserContext)
const theme = useContext(ThemeContext)
return (
<div>
<h2>User: {user.name}-{user.level}</h2>
<h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2>
</div>
)
})
export default App
当组件上层最近的 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。
23.4 useReducer
- 很多人看到 useReducer 的第一反应应该是 redux 的某个替代品,其实并不是。
- useReducer 仅仅是 useState 的一种替代方案:
- 在某些场景下,如果 state 的处理逻辑比较复杂,我们可以通过 useReducer 来对其进行拆分;
- 或者这次修改的 state 需要依赖之前的 state 时,也可以使用;
注释是 useState 写法
import React, { memo, useReducer } from "react";
// import { useState } from 'react'
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, counter: state.counter + 1 };
case "decrement":
return { ...state, counter: state.counter - 1 };
case "add_number":
return { ...state, counter: state.counter + action.num };
case "sub_number":
return { ...state, counter: state.counter - action.num };
default:
return state;
}
}
// useReducer+Context => redux
const App = memo(() => {
// const [count, setCount] = useState(0)
const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} });
// const [counter, setCounter] = useState()
// const [friends, setFriends] = useState()
// const [user, setUser] = useState()
return (
<div>
{/* <h2>当前计数: {count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<button onClick={e => setCount(count-1)}>-1</button>
<button onClick={e => setCount(count+5)}>+5</button>
<button onClick={e => setCount(count-5)}>-5</button>
<button onClick={e => setCount(count+100)}>+100</button> */}
<h2>当前计数: {state.counter}</h2>
<button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
<button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
<button onClick={(e) => dispatch({ type: "add_number", num: 5 })}>+5</button>
<button onClick={(e) => dispatch({ type: "sub_number", num: 5 })}>-5</button>
<button onClick={(e) => dispatch({ type: "add_number", num: 100 })}>+100</button>
</div>
);
});
export default App;
23.5 useCallback
useCallback 实际的目的是为了进行性能的优化
- useCallback 会返回一个函数的 memoized(记忆的) 值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的
- 通常使用 useCallback 的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存;
import React, { memo, useState, useCallback, useRef } from "react";
// useCallback性能优化的点:
// 1.当需要将一个函数传递给子组件时, 最好使用useCallback进行优化, 将优化之后的函数, 传递给子组件
// props中的属性发生改变时, 组件本身就会被重新渲染
const Home = memo(function (props) {
const { increment } = props;
console.log("Home被渲染");
return (
<div>
<button onClick={increment}>increment+1</button>
{/* 100个子组件 */}
</div>
);
});
const App = memo(function () {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("hello");
// 闭包陷阱: useCallback
// const increment = useCallback(function foo() {
// console.log("increment")
// setCount(count+1)
// }, [count])
// 进一步的优化: 当count发生改变时, 也使用同一个函数(了解)
// 做法一: 将count依赖移除掉, 缺点: 闭包陷阱
// 做法二: useRef, 在组件多次渲染时, 返回的是同一个值
const countRef = useRef();
countRef.current = count;
const increment = useCallback(function foo() {
console.log("increment");
setCount(countRef.current + 1);
}, []);
// 普通的函数
// const increment = () => {
// setCount(count+1)
// }
return (
<div>
<h2>计数: {count}</h2>
<button onClick={increment}>+1</button>
<Home increment={increment} />
<h2>message:{message}</h2>
<button onClick={(e) => setMessage(Math.random())}>修改message</button>
</div>
);
});
export default App;
当需要将一个函数传递给子组件时, 最好使用 useCallback 进行优化, 将优化之后的函数, 传递给子组件!
优化体现在传递子组件时。 父组件重新渲染都会重新定义函数体现不到优化。所以体现在子组件。函数传给子组件,子组件重新渲染不会重新定义父组件的函数。
23.6 useMemo
- useMemo 实际的目的也是为了进行性能的优化。
- useMemo 返回的也是一个 memoized(记忆的) 值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
import React, { memo } from "react";
import { useMemo, useState } from "react";
const HelloWorld = memo(function (props) {
console.log("HelloWorld被渲染~");
return <h2>Hello World</h2>;
});
function calcNumTotal(num) {
// console.log("calcNumTotal的计算过程被调用~")
let total = 0;
for (let i = 1; i <= num; i++) {
total += i;
}
return total;
}
const App = memo(() => {
const [count, setCount] = useState(0);
// const result = calcNumTotal(50)
// 1.不依赖任何的值, 进行计算
const result = useMemo(() => {
return calcNumTotal(50);
}, []);
// 2.依赖count
// const result = useMemo(() => {
// return calcNumTotal(count*2)
// }, [count])
// 3.使用useMemo对子组件渲染进行优化
const info = useMemo(() => ({ name: "why", age: 18 }), []);
return (
<div>
<h2>计算结果: {result}</h2>
<h2>计数器: {count}</h2>
<button onClick={(e) => setCount(count + 1)}>+1</button>
<HelloWorld result={result} info={info} />
</div>
);
});
export default App;
useMemo 是给子组件传一个值的。传引用型才有优化 {} []。普通的值没有优化。因为对象和数组会重新声明。
23.7 useRef
- 用法一:引入 DOM(或者组件,但是需要是 class 组件)元素
import React, { memo, useRef } from "react";
const App = memo(() => {
const titleRef = useRef();
const inputRef = useRef();
function showTitleDom() {
console.log(titleRef.current);
inputRef.current.focus();
}
return (
<div>
<h2 ref={titleRef}>Hello,zimo</h2>
<input type="text" ref={inputRef} />
<button onClick={showTitleDom}>查看title的DOM</button>
</div>
);
});
export default App;
- 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变
import React, { memo, useRef, useState, useCallback } from "react";
let obj = null;
const App = memo(() => {
const [count, setCount] = useState(0);
const nameRef = useRef();
console.log(obj === nameRef); //true
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;
useRef 返回一个 ref 对象,返回的 ref 对象再组件的整个生命周期保持不变。
28.8 useImperativeHandle
- 回顾一下 ref 和 forwardRef 结合使用:
- 通过 forwardRef 可以将 ref 转发到子组件;
- 子组件拿到父组件中创建的 ref,绑定到自己的某一个元素中;
- forwardRef 的做法本身没有什么问题,但是我们是将子组件的 DOM 直接暴露给了父组件:
- 直接暴露给父组件带来的问题是某些情况的不可控;
- 父组件可以拿到 DOM 后进行任意的操作;
import React, { memo, useRef, forwardRef, useImperativeHandle } from "react";
const HelloWorld = memo(
forwardRef((props, ref) => {
const inputRef = useRef();
// 子组件对父组件传入的ref进行处理
useImperativeHandle(ref, () => {
return {
focus() {
console.log("focus");
inputRef.current.focus();
},
setValue(value) {
inputRef.current.value = value;
},
};
});
return <input type="text" ref={inputRef} />;
})
);
const App = memo(() => {
const titleRef = useRef();
const inputRef = useRef();
function handleDOM() {
// console.log(inputRef.current)
inputRef.current.focus();
// inputRef.current.value = ""
inputRef.current.setValue("哈哈哈");
}
return (
<div>
<h2 ref={titleRef}>哈哈哈</h2>
<HelloWorld ref={inputRef} />
<button onClick={handleDOM}>DOM操作</button>
</div>
);
});
export default App;
- 上面的案例中,我们只是希望父组件可以操作的 focus,其他并不希望它随意操作;
- 通过 useImperativeHandle 可以值暴露固定的操作:
- 通过 useImperativeHandle 的 Hook,将传入的 ref 和 useImperativeHandle 第二个参数返回的对象绑定到了一起;
- 所以在父组件中,使用 inputRef.current 时,实际上使用的是返回的对象;
- 比如我调用了 focus 函数,甚至可以调用 printHello 函数;
23.9 useLayoutEffect
- useLayoutEffect 看起来和 useEffect 非常的相似,事实上他们也只有一点区别而已:
- useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 的更新;
- useLayoutEffect 会在渲染的内容更新到 DOM 上之前执行,会阻塞 DOM 的更新;
- 如果我们希望在某些操作发生之后再更新 DOM,那么应该将这个操作放到 useLayoutEffect。
import React, { memo, useEffect, useLayoutEffect, useState } from "react";
const App = memo(() => {
const [count, setCount] = useState(100);
useLayoutEffect(() => {
console.log("useLayoutEffect");
if (count === 0) {
setCount(Math.random() + 99);
}
});
console.log("App render");
return (
<div>
<h2>count: {count}</h2>
<button onClick={(e) => setCount(0)}>设置为0</button>
</div>
);
});
export default App;
官方更推荐使用 useEffect 而不是 useLayoutEffect。
23.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;
localStorage 数据存储
import { useEffect, useState } from "react";
function useLocalStorage(key) {
// 1.从localStorage中获取数据, 并且数据数据创建组件的state
const [data, setData] = useState(() => {
const item = localStorage.getItem(key);
if (!item) return "";
return JSON.parse(item);
});
// 2.监听data改变, 一旦发生改变就存储data最新值
useEffect(() => {
localStorage.setItem(key, JSON.stringify(data));
}, [data]);
// 3.将data/setData的操作返回给组件, 让组件可以使用和修改值
return [data, setData];
}
export default useLocalStorage;
23.11 redux hooks(useDispatch useSelector)
-
在之前的 redux 开发中,为了让组件和 redux 结合起来,我们使用了 react-redux 中的 connect:
- 但是这种方式必须使用高阶函数结合返回的高阶组件;
- 并且必须编写:mapStateToProps 和 mapDispatchToProps 映射的函数;
-
在 Redux7.1 开始,提供了 Hook 的方式,我们再也不需要编写 connect 以及对应的映射函数了
-
useSelector 的作用是将 state 映射到组件中:
-
参数一:将 state 映射到需要的数据中;
-
参数二:可以进行比较来决定是否组件重新渲染;
-
useDispatch 非常简单,就是直接获取 dispatch 函数,之后在组件中直接使用即可;
-
我们还可以通过 useStore 来获取当前的 store 对象;
import React, { memo } from "react";
import { useSelector, useDispatch, shallowEqual } from "react-redux";
import { addNumberAction, changeMessageAction, subNumberAction } from "./store/modules/counter";
// memo高阶组件包裹起来的组件有对应的特点: 只有props发生改变时, 才会重新渲染
const Home = memo((props) => {
// 1.使用useSelector将redux中store的数据映射到组件内
const { message } = useSelector(
(state) => ({
message: state.counter.message,
}),
shallowEqual
);
const dispatch = useDispatch();
function changeMessageHandle() {
// 2.使用dispatch直接派发action
dispatch(changeMessageAction("你好啊, zimo!"));
}
console.log("Home render");
return (
<div>
<h2>Home: {message}</h2>
<button onClick={(e) => changeMessageHandle()}>修改message</button>
</div>
);
});
const App = memo((props) => {
// 1.使用useSelector将redux中store的数据映射到组件内
const { count } = useSelector(
(state) => ({
count: state.counter.count,
}),
shallowEqual
);
// 2.使用dispatch直接派发action
const dispatch = useDispatch();
function addNumberHandle(num, isAdd = true) {
if (isAdd) {
dispatch(addNumberAction(num));
} else {
dispatch(subNumberAction(num));
}
}
console.log("App render");
return (
<div>
<h2>当前计数: {count}</h2>
<button onClick={(e) => addNumberHandle(1)}>+1</button>
<button onClick={(e) => addNumberHandle(6)}>+6</button>
<button onClick={(e) => addNumberHandle(6, false)}>-6</button>
<Home />
</div>
);
});
export default App;