一、组件通讯
组件是独立且封闭的单元,为了实现某些功能,需要打破组件的独立封闭性,让其与外界沟通
1.1.组件的props
props:接收传递给组件的数据
- 传递数据:给组件标签添加属性
- 接收数据【以对象的形式】:函数组件通过参数props接收数据,类组件通过this.props接收数据
- 特点
可以传递任何类型的数据:字符串、数字、数组、函数、jsx结构
props是只读的,只能读取,不能修改
在类组件中,如果写了构造函数,应该将props传递给super(),否则无法在构造函数中获取到props- children属性
只有引用子组件时内部有内容才会有children属性
可以是任意值:文本、React元素、数组、组件、函数- props校验
props校验可以保证组件使用者传入数据的格式,给出明确的错误提示,增加组件健壮性
允许在创建数组是,就使用propTypes来指定props的类型和格式
使用步骤
1.安装包(yarn add prop-types
)
2.导入包(import PropTypes from 'prop-types'
)
3.添加校验规则(组件名.propTypes={属性名:PropTypes.类型.isRequired}
)
约束类型:number、string、bool、array、object、func、symbol、element【React元素】
必填项:isRequired【设置后必须传入这个参数,否则报错】
特定结构对象:filter:PropTypes.shape({color:PropTypes.array,fontSize:PropTypes.number})
- props默认值
在引入组件时不传入参数,也可以拥有的默认值组件名.defaultProps={name:'小花'}
,场景pageSize页大小
不传入使用默认值,传入使用传入的值
1.2.通信方式
- 父组件 >> 子组件
父组件给子组件传参- 子组件 >> 父组件
父组件给子组件传回调函数,子组件调用并将要传递的数据return回去- 兄弟组件 >> 兄弟组件
将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态【状态提升】,要通信的子组件只需要通过props接受状态或操作状态的方法即可- 状态提升
用户对子组件操作,子组件不改变自己的状态,通过自己的props把这个操作改变的数据传递给父组件,改变父组件的状态,从而改变受父组件控制的所有子组件的状态,这也是React单项数据流的特性决定的。
import React from "react";
// 父组件【类组件】
class Parent extends React.Component {
state = {
lastName: "杨",
firstName: "",
};
constructor(props) {
super(props);
}
render() {
return (
<>
<div>这是父组件</div>
<Child1
lastName={this.state.lastName}
// 父组件提供回调函数,用于接受数据
getFirstName={(firstName) => {
// 在这里可以接收到子组件传递给父组件的值
this.setState({
firstName,
});
}}
/>
{/* 通过Child1获取firstName的值并传递给Child2 */}
<Child2 firstName={this.state.firstName}>
{/* “<p>这是第二个子节点</p>”会作为children属性被子节点的props.children接收 */}
{/* <p>这是第二个子节点</p> */}
{/* 还可以传递函数 */}
{()=>console.log('这是一个函数子节点')}
</Child2>
</>
);
}
}
// 子组件1【函数式组件】
const Child1 = (props) => {
const { lastName, getFirstName } = props;
return (
<>
<div>接收父组件的数据:{lastName}</div>
<button
onClick={() => {
getFirstName("超");
}}
></button>
</>
);
};
// 给Child1添加props校验
Child1.propTypes = {
lastName:propTypes.string.isRequired,
getFirstName:propTypes.func
}
// 子组件2【与子组件1是兄弟组件,通过状态提升与子组件1进行通讯】
const Child2 = (props) => {
const { firstName,children,color } = props;
// 接收函数子节点
children() // 会在控制台打印出:这是一个函数子节点
return (
<>
<div>接收父组件的数据:{firstName}喜欢{color}</div>
{/* 接收文本子节点 */}
{/* <div>{children}</div> */}
</>
);
};
// 给Child2设置一个默认props值
Child2.defaultProps={
color:'蓝色'
}
ReactDOM.render(<Parent />, document.getElementById("root"));
1.3.Context
全局状态管理:跨组件传递数据(多层嵌套)【Redux和hook的useContext】
- 使用步骤
1.调用React,createContext()创建Provider(提供数据)和Consumer(消费数据)两个组件
2.使用Provider组件作为父节点来包裹应用
3.设置value属性,表示要传递的数据
<Provider value="pink">
4.调用Consumer组件使用回调函数的参数接收数据
<Consumer>{(value)=><span>接收到的数据为{value}</span>}</Consumer>
import React from "react";
// Provider:数据提供者,Consumer数据消费者
const { Provider, Consumer } = React.createContext();
const App = () => {
// 使用Provider组件作为父节点
return (
// 设置value属性,表示要传递的数据
<Provider value="pink">
<div className="app">
<Node />
</div>
</Provider>
);
};
const Node = () => {
return (
<>
<div className="node">
<Child />
</div>
</>
);
};
const Child = () => {
return (
<>
<div className="child">
{/* 子组件中调用Consumer组件接收数据 */}
<Consumer>
{(data) => {
<span>从App中接收的值为:{data}</span>;
}}
</Consumer>
</div>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
二、组件的生命周期
只有类组件才有生命周期
- 生命周期
从组件创建到挂载搭到页面中运行,再到组件不用时卸载的过程- 钩子函数
生命周期的每个阶段伴随着一些方法的调用,这些方法就是生命周期的钩子函数,可以让开发人员在不同阶段操作组件- 注意:
在render中调用this.setState会出现递归调用:因为每次状态更新都会调用render
在componentDidMount时页面已经完成渲染,就可以使用document.getElementById(‘root’)来获取页面元素了
当接收新属性、setState更新状态、调用强制更新forceUpdate()方法都会导致组件更新
在componentDidUpdate不要直接调用setState,因为会导致递归更新,要将setState放到if条件
中,条件中判断更新前后的props是否相同,componentDidUpdate的参数可以获取上一次的状态值,this.props获取改变后的状态值,发送ajax请求也要写在if条件
中,因为请求完数据也要间接的进行setState操作
componentDidUpdate(prevProps){if(prevProps.count !== this.props.count){this.setState({})}}
在函数组件中
- 使用useEffect,如果useEffect不传参数,每一次重新渲染都会执行一次,如果useEffect传一个空数组,相当于componentDidMount挂载时会执行一次,如果useEffect传一个有值的数组,第一次执行渲染和每次状态发生变化的时候都会执行,useEffect的return是可选的清除机制相当于类组件的componentWillUnMount
三、组件复用
3.1.render props模式
- render props模式【一种写法】:prop是一个函数并且告诉组件要渲染什么内容的技术
如何拿到组件中的state:在使用组件时添加一个值为函数的prop,通过函数参数来获取
如何渲染任意UI:使用该函数的返回值作为渲染的UI内容
可以使用children代替render属性
import React from "react";
import img from "img";
const App = () => {
return (
<div className="app">
{/* 4.在使用组件时添加一个值为函数的prop,通过函数参数来获取 */}
{/* 使用该函数的返回值作为渲染的UI内容 */}
{/* <Mouse
updateCoordinate={(mouse) => {
return (
<p>
鼠标当前的位置{mouse.x} {mouse.y}
</p>
);
}}
/> */}
{/* 状态逻辑复用 */}
<Mouse>
{/* 使用children方式代替函数类型的prop */}
{(mouse) => {
return (
<img
src={img}
alt="猫"
style={{ position: "absolute", top: mouse.y, left: mouse.x }}
/>
);
}}
</Mouse>
</div>
);
};
// 封装复用状态的逻辑代码
class Mouse extends React.component {
// 【1.状态】
state = {
x: 0, // 鼠标的坐标
y: 0,
};
// 【2.操作状态的方法】
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY,
});
};
// 监听鼠标移动的事件
componentDidMount() {
window.addEventListener("mousemove", this.handleMouseMove);
}
// 在组件卸载是移除鼠标移动事件
componentDidUnMount() {
window.addEventListener("mousemove", this.handleMouseMove);
}
render() {
// 3.返回父组件提供的updateCoordinate方法将当前鼠标位置传递到父组件中
// (1).使用函数型prop
// return this.props.updateCoordinate(this.state);
// (2).使用children
return this.props.children(this.state);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
3.2.高阶组件(HOC)
- 高阶组件(HOC【一个函数】):实现状态逻辑复用,采用包装(装饰)模式
接收要包装的组件,返回增强后的组件- 使用步骤
1.创建一个函数,名称约定为with
开头
2.指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
3.在函数内部创建一个类组件,提供复用的状态逻辑代码,并将创建好的内部组件返回
4.在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
5.调用高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
import React from "react";
import img from "img";
// 高阶组件创建的增强组件名称是相同的,不利于调试,使用displayName设置不同的名称,用来区分
function getDisplayName(WrappedComponent) {
return (
WrappedComponent.getDisplayName || WrappedComponent.name || "Component"
);
}
// 创建高阶组件,里面指定函数参数为要渲染的组件
function withMouse(WrappedComponent) {
// 该组件提供复用的状态逻辑
class Mouse extends React.component {
// 【1.状态】
state = {
x: 0, // 鼠标的坐标
y: 0,
};
// 【2.操作状态的方法】
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY,
});
};
// 监听鼠标移动的事件
componentDidMount() {
window.addEventListener("mousemove", this.handleMouseMove);
}
// 在组件卸载是移除鼠标移动事件
componentDidUnMount() {
window.addEventListener("mousemove", this.handleMouseMove);
}
render() {
// 将组件的状态和props借用参数传递出去,如果不传递props会造成props丢失【高阶组件并没有往下传递props】
return <WrappedComponent {...this.state} {...this.props} />;
}
}
// 修改增强后的函数名称,最后得到的名字分别为WithMousePosition与WithMouseCat
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`;
return Mouse;
}
// 鼠标位置组件
const Position = (props) => {
<p>
鼠标当前的位置:(x:{props.x},y:{props.y})
</p>;
};
// 猫捉老鼠组件
const Cat = (props) => {
<img
src={img}
alt="猫"
style={{ position: "absolute", top: props.y - 64, left: props.x - 64 }}
/>;
};
// 获取增强后的组件:Position和Cat就是WrappedComponent
const MousePosition = withMouse(Position);
const MouseCat = withMouse(Cat);
// 将组件渲染到页面中
const App = () => {
return (
<div className="app">
{/* 渲染增强后的组件 */}
<MousePosition />
<MouseCat />
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
四、原理
4.1.setState
setState是异步更新的,【在自定义方法中是异步的,但在异步方法中是同步的】
- 注意后面的setState()不要依赖于前面的setState()
- 多次调用setState(),只会触发一次render重新渲染:为了性能的考虑
- 推荐语法
this.setState((state,props)=>{})
,也是异步更新,但state是最新值,调用多次setState会多次触发render- 第二个参数:callback,在状态更新并且重新渲染后立即执行的某个操作
class Index extends Component {
state: {
count: 1,
};
handleClick = () => {
// this.setState((state, props)=>{修改state},callback回调函数显示当前state)
this.setState(
(state, props) => {
return {
count: state.count + 1,
};
},
() => {
console.log("状态更新完成", this.state.count);
}
);
};
render() {
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.handleClick}></button>
</div>
);
}
}
4.2.JSX
JSX语法转化过程
- JSX语法被@babel/preset-react 插件编译为createElement()方法,再转化为React元素显示在屏幕上
4.3.组件更新机制
组件之间更新渲染
- 父组件重新渲染,其子组件和孙子组件都会重新渲染【渲染当前组件子数(当前组件及所有子组件)】
避免组件间不必要的重新渲染:父组件变化时若子组件没有任何变化也会重新渲染
- 使用
钩子函数shouldComponentUpdate(nextProps,nextState)
,返回true表示重新渲染,返回false表示不重新渲染
触发时机:在更新阶段的钩子函数,组件重新渲染前执行(shouldComponentUpdate - - > render)组件内部更新渲染
- state变化就会重新渲染视图
避免组件内部不必要的重新渲染:组件中只有一个DOM元素发生更新时要把整个组件内容重新渲染
使用
虚拟DOM + Diff算法
实现部分更新
虚拟DOM:本质上是一个JS对象,用来描述HTML结构+数据
部分更新执行过程
1.组件render方法调用后,初次渲染,React会根据初始state(Model)和JSX结构创建一个虚拟DOM对象(树)
2.根据虚拟DOM生成真正的DOM,渲染到页面中
3.当使用setState使状态发生改变时,重新根据新的数据,创建新的虚拟DOM对象(树)
4.与上一次得到的虚拟DOM对象,使用Diff算法对比(找不同),得到需要更新的内容
5.React将变化的内容更新到DOM中,重新渲染到页面render方法的调用并不意味着浏览器的重新渲染,仅仅说明要进行diff
五、路由
SPA:单页应用程序,就是一个html页面的应用程序,因为用户体验更好,为有效使单个页面来管理多页面的功能,就出现了路由
路由功能:一套映射规则,让URL路径
与组件
相对应,让用户从一个视图(页面)导航到另一个视图(页面)
使用步骤
- 1.安装:
yarn add react-router-dom
- 2.导入路由三个核心组件:
import { BrowserReoter as Router, Route, Link } from "react-router-dom";
- 3.使用Router组件包裹整个应用
- 4.使用Link组件作为导航菜单(路由入口),最后会转换为href :
<Link to="/first">跳转到页面一</Link>
- 5.使用Route组件配置路由规则和要展示的组件(路由出口):
<Route path="/first" component={First}></Route>
** 路由执行过程**
- 点击Link组件(a标签),修改浏览器地址栏中的url:
- React路由监听到地址栏url的变化
- React路由内部遍历所有Route组件,使用路由规则(path)与pathname进行匹配
- 当路由规则(path)能够匹配地址栏中的pathname时,就展示Route组件所在的内容
常用组件说明
- Router组件:包裹整个应用,一个React应用只需要使用一次
- 两种常用Router:HashRouter和BrowerRouter
HashRouter:使用URL的哈希值实现,http://loaclhost:3000/#/first
BrowerRouter:使用URL的哈希值实现,http://loaclhost:3000/first编程式导航:通过JS代码来实现页面跳转
this.props.history.push('/home')
:去/home路由匹配的组件页面this.props.history.go(-1)
:去上一个页面默认路由:刚进入页面就会显示的页面组件
<Route path="/" component={First}></Route>
模糊匹配:默认情况下React路由是模糊匹配的(新版的不会)
- 只要pathname【Link组件的to属性】以path【Route组件的path属性】开头就会匹配成功
精确匹配:给Route组件添加exact属性- pathname【Link组件的to属性】和path【Route组件的path属性】必须一致才会匹配成功
<Route path="/" component={First} exact></Route>
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
// import { HashRouter as Router, Route, Link } from "react-router-dom";
const First = () => <p>页面一的内容</p>;
const Login = (props) => {
const handleLogin = () => {
// 编程式导航,去路由为/home的页面
props.history.push("/home");
};
return (
<>
<p>登录页面</p>
{/* 点击登录按钮跳转到后台主页面 */}
<button onClick={handleLogin}>登录</button>
</>
);
};
const Home = (props) => {
const handleBack = () => {
// 编程式导航:返回上一个页面
props.history.go(-1);
};
return (
<>
<p>后台首页</p>
{/* 点击登录按钮返回到上一个登录页面 */}
<button onClick={handleBack}>返回</button>
</>
);
};
const App = () => {
<Router>
<div>
{/* 指定路由入口 */}
<Link to="/first">跳转到页面一</Link>
{/* 指定路由出口 */}
{/* 点击Link后会跳转到/first路径,并将First这个组件显示到此页面 */}
<Route path="/first" component={First}></Route>
<h1>编程式导航:</h1>
<Link to="/login">去登录页面</Link>
<Route path="/login" component={Login} />
<Route path="/home" component={Home} />
</div>
</Router>;
};
export default App;
六、Hook
系统自带Hook与自定义Hook都只能在1.其他Hook中运行 2.组件中运行
6.1.系统自带Hook
6.1.1.useState
6.1.2.useEffect
6.2.自定义Hook
Customer Hook是最优秀的组件代码复用方案,自定义组件必须以use开头
6.2.1.useMount
封装仅执行一次的useEffect,避免出现多个依赖为空数组的useEffect
如果不加use会被eslint认为是一个普通函数,而不是一个hook,在里面使用其他的hook就会报错
正常开发中直接使用带空数组的useEffect即可
export const useMount = (callback) => {
useEffect(() => {
callback();
}, []);
};
useMount(() => {
fetch(`${apiUrl}/users`).then(async (response) => {
if (response.ok) {
setUsers(await response.json());
}
});
});
6.2.2.useDebounce
防抖处理:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时间,才执行代码一次,防止用户频繁操作,比如:按钮点击、文本编辑保存等
// useDebounce:连续执行多次事件,设置一个定时器,在此次任务执行完后,清除上一次的定时器id,无论执行多少次,最后都只会剩下一个定时任务,只会发一次请求
// value是useEffect的依赖,delay是防抖时间
export const useDebounce = (value, delay) => {
const [debounceValue, setDebounceValue] = useState(value);
useEffect(() => {
// 每次在value变化之后设置一个定时器
const timeout = setTimeout(() => setDebounceValue(value), delay);
// 每次在上一个useEffect处理完以后再运行:清理上一个定时器id
return () => clearTimeout(timeout);
}, [value, delay]); // 监听value变化时执行useEffect,delay一般不会变化
// 只有最后一个debounceValue能够保存下来并返回出去
return debounceValue;
};
// 原来被监听的param
const [param, setParam] = useState({
name: "",
personId: "",
});
// 被useDebounce处理后的param
const debouncedParam = useDebounce(param, 1000);
useEffect(() => {
fetch(
`${apiUrl}/projects?${qs.stringify(cleanObject(debouncedParam))}`
).then(async (response) => {
if (response.ok) {
setProjects(await response.json());
}
});
}, [debouncedParam]); // 只监听最后一个定时器返回的param,只发送一次请求