React 组件分类
- 根据组件的定义方式,可以分为:
- 函数组件 (Functional Component );
- 类组件 (Class Component);
- 根据组件内部是否有状态需要维护,可以分成:
- 无状态组件 (Stateless Component)
- 有状态组件 (Stateful Component);
- 根据组件的不同职责,可以分成:
- 展示型组件 (Presentational Component);
- 容器型组件 (Container Component);
- 这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:
- 函数组件、无状态组件、展示型组件主要 关注 UI 的展示;
- 类组件、有状态组件、容器型组件主要 关注数据 逻辑;
创建 React 组件
组件的名称首字母必须大写;
React 中的哲学:数据属于谁,谁才有权力改动;
创建类组件
-
类组件的定义有如下要求:
- 类组件需要继承自 React.Component;
- 类组件必须实现 render 函数;
-
在 ES6 之前,可以通过 create-react-class 模块来定义类组件,但是目前官网建议使用 ES6 的 class 类定义;
-
使用 class 定义一个组件:
- constructor 是可选的,通常在 constructor 中初始化一些数据;
- this.state 中维护的是组件内部的数据;
- render() 方法是 class 组件中唯一必须实现的方法;当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
- React 元素:通常通过 JSX 创建;
- Portals:可以渲染子节点到不同的 DOM 子树中;
- 字符串 或 数值类型:它们在 DOM 中会被渲染为文本节点;
- 布尔类型 或 null:什么都不渲染;
import React, { Component } from 'react'; export default class App extends Component { constructor() { super(); this.state = { } } render() { return <h2>Hello App</h2> } }
创建函数组件
-
函数组件是使用 function 来进行定义的函数,只是这个函数会返回和类组件中 render 函数返回一样的内容;
-
函数组件有自己的特点:
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
- 没有 this (组件实例);
- 没有内部状态 (state);
export default function App() { return ( <div>Hello World</div> ) }
组件状态
类组件中的状态
-
组件状态:组件可以自行维护的数据,组件状态仅在
类组件
中有效;- 状态 (state),本质上是类组件的一个属性,是一个对象;
- 不能直接改变状态:因为 React 无法监控到状态发生了变化,必须使用 this.setState 改变状态,一旦调用了 this.setState,会导致当前组件重新渲染;
-
组件中的数据:
- props:该数据是由组件的使用者传递的数据,所有权不属于组件自身;
- state:该数组是由组件自身创建的,所有权属于组件自身,因此组件有权改变该数据;
-
setState() 语法:
对象形式
:当不需要基于当前状态进行更新,或者更新不需要依赖之前的 state 或 props 时,应使用对象形式,这种形式直接传递一个新状态对象给 setState,它会与当前状态进行浅合并;this.setState({ counter: 10 }, () => { // 状态完成改变之后触发,该回调运行在 render 之后 });
函数形式
:当需要基于当前的状态来进行更新时,应使用函数形式;/** * prevState 代表当前状态,可以获取到之前的状态值; * 返回值会覆盖掉之前的状态 * 该函数是异步执行的 */ this.setState((prevState) => ({ counter: prevState.counter + 1 }));
-
深入认识 setState
- setState 它对状态的改变,可能是 同步 的、异步 的;
- 最佳实践:
- 把所有的 setState 当作是异步的;
- 永远不要信任 setState 调用之后的状态;
- 如果要使用改变之后的状态,需要使用回调函数 (setState 对象形式的第二个参数);
- 如果新的状态要根据之前的状态进行运算,使用函数的方式改变状态 (setState 的函数形式);
- React 会对异步的 setState 进行优化,将多次 setState 进行合并 (将多次状态改变完成后,再统一对 state 进行改变,然后触发 render,react 源码中实现:在 html 中事件函数中的状态会异步更新,其他地方的状态会同步更新);
-
示例:
import React, { PureComponent } from "react"; export default class Counter extends PureComponent { constructor(props) { super(props); this.state = { counter: 0, }; } render() { return ( <div> <h2>当前计数: {this.state.counter}</h2> <button onClick={(e) => this.increment()}>+1</button> <button onClick={(e) => this.decrement()}>-1</button> </div> ); } increment() { // this.setState({ counter: this.state.counter + 1 }); this.setState((curr)=> ({ counter: curr.counter + 1 })); } decrement() { this.setState({ counter: this.state.counter - 1 }); } }
函数组件中的状态
-
使用 React Hook 中的 useState 可以在函数组件中使用状态;
-
示例:
// 函数式组件 + hook 实现 import React, { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <h2>当前计数: {count}</h2> <button onClick={(e) => setCount(count + 1)}>+1</button> <button onClick={(e) => setCount(count - 1)}>-1</button> </div> ); }
State Hook
语法
const [state, setState] = useState(initialState);
- 在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同;
- setState 函数用于更新 state,它接收一个新的 state 值并将组件的一次重新渲染加入队列;
数据更新(基本)
-
问题:点击按钮后连续调用 3 次 setCount(count + 1),最后界面上 count 的值并没有 +3,仍然是 +1;
import React, { useState } from "react"; export default function () { const [count, setCount] = useState(0); function handleOnClick() { setCount(count + 1); setCount(count + 1); setCount(count + 1); } return ( <div> <div>count: {count}</div> <button onClick={handleOnClick}>+1</button> </div> ); }
-
解决:函数式更新 (新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState);
setCount(count => count + 1); setCount(count => count + 1); setCount(count => count + 1);
数据更新(引用)
-
问题:当 useState 的值为对象时,可能会存在视图不更新的情况;
import React, { useState } from "react"; export default function () { const [list, setList] = useState([0, 1, 2]); const [useInfo, setUserInfo] = useState({ name: "张三", age: 18, }); function handleOnClick() { list.push(4); list.push(4); setList(list); useInfo.name = "李四"; useInfo.age = 20; setUserInfo(useInfo); } return ( <div> <p>姓名:{useInfo.name}</p> <p>年龄:{useInfo.age}</p> <p>ist.length: {list.length}</p> <button onClick={handleOnClick}>修改</button> </div> ); }
-
问题原因:React 中默认是浅监听,当 state 的值为对象时,栈中存的是对象的引用 (地址),setState 改变的是堆中的数据,栈中的地址还是原地址,React 浅监听到地址没变,故会认为 State 并未改变,所以没有重渲染页面;
-
解决方案:只要改变了原对象的地址即可,可通过 克隆、拓展运算符 实现;
function handleOnClick() { setList([...list, 4, 4]); useInfo.name = "李四"; useInfo.age = 20; setUserInfo({ ...useInfo }); }
获取最新的值
setSate 后拿到最新的值 (由于 setSate 后并不会立即更新,React 会在某个时候将多个 setSate 进行合并后再更新)
-
使用 useRef,但是数据的更新不会引起视图的更新;
import React, { useState, useRef, useEffect } from "react"; export default function () { const [count, setCount] = useState(0); const countRef = useRef(0); function handleOnClick() { countRef.current += 1; setCount(count + 1); console.log("正常打印", count); // 获取的还是之前的值 console.log("countRef", countRef.current); // 获取的是更新后的值 } return ( <div> <div>count: {count}</div> <button onClick={handleOnClick}>+1</button> </div> ); }
-
使用 useEffect,这种方式在很多场景下也不适用,每次更新都会执行 useEffect 中的内容,往往需求并不是如此;
import React, { useState, useRef, useEffect } from "react"; export default function () { const [count, setCount] = useState(0); useEffect(() => { console.log("useEffect", count); // 获取的是更新后的值 }, [count]); function handleOnClick() { setCount(count + 1); console.log("正常打印", count); // 获取的还是之前的值 } return ( <div> <div>count: {count}</div> <button onClick={handleOnClick}>+1</button> </div> ); }
-
使用函数式更新;
import React, { useState, useRef, useEffect } from "react"; export default function () { const [count, setCount] = useState(0); const countRef = useRef(0); function handleOnClick() { setCount(count + 1); console.log("正常打印", count); // 获取的还是之前的值 setCount((count) => { console.log("函数式更新获取最新值", count); // 获取的是更新后的值 return count; }); } return ( <div> <div>count: {count}</div> <button onClick={handleOnClick}>+1</button> </div> ); }
-
使用 ahooks 的 useGetState (原理:使用 useRef 将 useState 的值存起来)
import React, { useState, useRef, useCallback } from "react"; const useGetState = (initVal) => { const [state, setState] = useState(initVal); const ref = useRef(initVal); const setStateCopy = (newVal) => { ref.current = newVal; setState(newVal); }; const getState = () => ref.current; return [state, setStateCopy, getState]; }; export default function () { const [count, setCount, getCount] = useGetState(0); function handleOnClick() { setCount(count + 1); console.log("正常打印", count); // 获取的还是之前的值 console.log("useGetState", getCount()); // 获取的是更新后的值 } return ( <div> <div>count: {count}</div> <button onClick={handleOnClick}>+1</button> </div> ); }
组件状态案例:弹性小球
// index.js
import React from "react";
import ReactDOM from "react-dom";
import BallList from "./components/BallList";
ReactDOM.render(<BallList />, document.getElementById("root"));
// BallList.js
import React, { Component } from "react";
import Ball from "./Ball";
import { getRandom } from "../util";
/**
* 每隔一段时间,自动产生一个小球,各种数据随机
*/
export default class BallList extends Component {
constructor(props) {
super(props);
this.state = {
ballInfoes: [],
};
const timer = setInterval(() => {
var info = {
left: getRandom(0, document.documentElement.clientWidth - 100),
top: getRandom(0, document.documentElement.clientHeight - 100),
xSpeed: getRandom(50, 500),
ySpeed: getRandom(50, 500),
bg: `rgb(${getRandom(0, 255)}, ${getRandom(0, 255)}, ${getRandom( 0, 255 )})`,
};
this.setState({
ballInfoes: [...this.state.ballInfoes, info],
});
if (this.state.ballInfoes.length === 10) clearInterval(timer);
}, 1000);
}
render() {
const balls = this.state.ballInfoes.map((item, i) => (
<Ball key={i} {...item} />
));
return <>{balls}</>;
}
}
// Ball.js
import React, { Component } from "react";
import "./Ball.css";
/**
* 一个能够自动移动的小球
*/
export default class Ball extends Component {
constructor(props) {
super(props);
// 属性中需要分别传递横纵坐标上的速度,每秒移动的像素值 props.xSpeed, props.ySpeed
// 需要传递背景颜色,如果没有传递,则使用红色
this.state = {
left: props.left || 0, //横坐标
top: props.top || 0, //纵坐标
xSpeed: props.xSpeed,
ySpeed: props.ySpeed,
};
const duration = 16; // 间隔的毫秒数
setInterval(() => {
const xDis = (this.state.xSpeed * duration) / 1000;
const yDis = (this.state.ySpeed * duration) / 1000;
// 根据速度,改变 left、top 值
let newLeft = this.state.left + xDis;
let newTop = this.state.top + yDis;
if (newLeft <= 0) {
newLeft = 0;
// 横坐标反向
this.setState({ xSpeed: -this.state.xSpeed });
} else if (newLeft >= document.documentElement.clientWidth - 100) {
newLeft = document.documentElement.clientWidth - 100;
// 横坐标反向
this.setState({ xSpeed: -this.state.xSpeed });
}
if (newTop <= 0) {
newTop = 0;
// 纵坐标反向
this.setState({ ySpeed: -this.state.ySpeed });
} else if (newTop >= document.documentElement.clientHeight - 100) {
newTop = document.documentElement.clientHeight - 100;
// 纵坐标反向
this.setState({ ySpeed: -this.state.ySpeed });
}
this.setState({ left: newLeft, top: newTop });
}, duration);
}
render() {
return (
<div className="ball" style={{ left: this.state.left, top: this.state.top, background: this.props.bg || "#f40" }} ></div>
);
}
}
// util.js
export function getRandom(min, max) {
return Math.floor(Math.random() * (max + 1 - min) + min);
}
.ball{
width: 100px;
height: 100px;
border-radius: 50%;
position: fixed;
}
React✍️ 什么是 JSX
上一篇