文章目录
- 一、初识
- 二、jsx
- 1.jsx元素写法有两种:
- 2、jsx元素本质是一个对象,这个对象是不可变对象
- 3、使用{}
- 4、jsx也可以做了函数的入参,也就是在调用一个函数时,传递传递一个jsx元素,也可以做为函数的返回值,还可以用在if或for中。
- 5、jsx元素中有三个变化的属性:
- 6、jsx元素中新增了三个属性:
- 7、jsx只能有一个根标签
- 8、在jsx元素,不仅组件可以使用单标签,任何html标签都可以使用单标签
- 9、在JSX中写行内样式
- 10、JSX中,所有React组件(由class或function定义)的名称必须以大写字母打头
- 11、JSX语法中是支持点语法
- 12、对于组件来说,props就是自定义属性
- 13、props.children
- 14、在使用JSX时,如果是数组,可以直接渲染
- 三、状态
- 四、条件渲染
- 五、表单绑定,列表渲染
- 六、生命周期
- componentWillUnmount(卸载)
- 七、状态提升
- 八、封装一个组件
- 九、上下文
- 十、高阶组件
- 十一、Ref和Ref转发
- 12、Hooks
- 13、路由
- 14、antd组件库
- 15、React实现管理系统
一、初识
官网:https://react.docschina.org/
官方脚手架:https://create-react-app.bootcss.com/
创建react项目: npx create-react-app 项目名称
相关博客:http://47.94.210.129/malulesson/Framework/react/01.html
二、jsx
jsx = javascript + xml
1.jsx元素写法有两种:
jsx的形式
React.createElement的形式
2、jsx元素本质是一个对象,这个对象是不可变对象
- jsx元素最终需要被渲染成真实DOM元素,所以不能直接操作"原材料",只能使用它。
- 由class或function定义出来的叫组件,组件名必须大写
- 由jsx语法或React.createElement返回的结果叫jsx元素,jsx元素充当组件的视图模板
3、使用{}
在jsx元素中,你要嵌套表达式,必须使用{}包起来,也就是说,在jsx元素中,看到{}你要知道里面放的都是表达式。任何有值的内容都是表达式
4、jsx也可以做了函数的入参,也就是在调用一个函数时,传递传递一个jsx元素,也可以做为函数的返回值,还可以用在if或for中。
5、jsx元素中有三个变化的属性:
- class 变成了 className
- for 变成了 htmlFor
- tabindex 变成了tabIndex
6、jsx元素中新增了三个属性:
- key 用于列表渲染
- ref 方便DOM操作
- dangerouslySetInnerHTML 用来渲染一片html字符串
7、jsx只能有一个根标签
8、在jsx元素,不仅组件可以使用单标签,任何html标签都可以使用单标签
9、在JSX中写行内样式
<div style={ { key1:'value1', key2:'value2',... } }></div>
- 在JSX中,写class类名
10、JSX中,所有React组件(由class或function定义)的名称必须以大写字母打头
11、JSX语法中是支持点语法
React.Component
React.Fragment <></>
Ml.Button
Ml.Form
12、对于组件来说,props就是自定义属性
- 只能使用它,不要去修改它
13、props.children
- 在使用组件时,如果组件使用双标签,在双标签之间就可以写内容,在双标签之间写的内容是通过props.children来接收的,props.children可以是任意类型的数据,基本数据类型可以,引用数据类型也可以,也可以是函数
测试结果:
14、在使用JSX时,如果是数组,可以直接渲染
- 在上面的数组中可以写哪些数据类型
- 如果数组中有boolean值,有null,有und,会被直接忽略,也就是说不会生成对应的文本节点。
三、状态
1、this.setState
- this.setState({}, callback) 在修改状态时,当新值与旧值无关时,推荐使用这种写法,callback表示当状态修改后,自动执行,当状态修完后,有一些业务逻辑放到callback中。
- this.setState((state, props)=>({}), callback) 当新值与旧值有关时,新值由旧值计算而来,形参state永远表示旧值,建议使用这种写法。callback表示当状态修改后,自动执行,当状态修完后,有一些业务逻辑放到callback中。
2、修改状态的同步和异步问题
- 在React18之前,React16和React17,this.setState在合成事件中,是异步的。在宏任务,Promise.then中是同步的。所谓的React合成事件是指on*系列事件,生命周期函数。
- 为什么是V18版本中,把this.setState设计成异步的?
- 为了性能优化。在V18中,无论this.setState在哪里,都是异步的,这种特性,叫”并发模式“
3、事件绑定
上面的写法,不完美,因为在监听器中不能获取到this。this是und。解决办法有两种:
- 利用ES5中的bind中手动绑定this
- 利用ES6中的箭头函数自动绑定this
ES5绑定代码如下:
使用ES6中的箭头函数自动绑定this
获取事件对象
4、状态在函数组件中的使用
- 函数组件中是不能定义状态,但是配合hook可以定义状态,hook类似于vue3中的组合式API
setNum是同步还是异步,在不同的版本中是不一样的,总结如下:
- 在V18之前,在合成事件中是异步的,在宏任务和promise.then中是同步的。
- 在V18中,无论在哪里都是异步的。
setNum中,是没有callback。setNum还有一种写法
四、条件渲染
类组件中实现条件渲染
在React中,没有指令,使用原生JS来实现
单一元素的条件渲染
{
flag && < jsx / >
}
两个元素,实现条件渲染
多个元素的条件渲染,建议封装成一个自定义的渲染函数
在vue中有一个v-show,是通过display控制显示与隐藏的,使用react实现之
函数组件中实现条件渲染
五、表单绑定,列表渲染
类组件实现todolist
把表单变成受控表单,收集表单中的数据
在React中,收集数据,需要写一个value,还需要写一个onInput,根据人家建议,换成onChange,
当点击添加按钮,就需要把收集到的todo存起来,如下:
把每一个todo存储到数组中,如下:
…
完整代码
// import { useEffect } from "react";
// import { useState } from "react";
// function Todo() {
// let [todo, setTodo] = useState("")
// let [list, setList] = useState(JSON.parse(localStorage.getItem('list')) || [])
// let addtodo = () => {
// if (!todo.trim()) {
// alert("不能为空")
// return
// }
// setList([...list, { id: Date.now(), task: todo }])
// setTodo("")
// }
// let addByEnter = (e) => {
// if (e.key === "Enter") {
// addtodo()
// }
// }
// let delTodo = (id) => {
// setList(list.filter(item => item.id !== id))
// }
// useEffect(() => {
// localStorage.setItem('list', JSON.stringify(list))
// }, [list])
// return (
// <div>
// <div>
// <input type="text" value={todo}
// onChange={e => setTodo(e.target.value)}
// onKeyUp={e => addByEnter(e)}
// />
// <button onClick={addtodo}>添加</button>
// <hr />
// <div>
// {
// list.map(item => (
// <div key={item.id}>
// <span>{item.id}</span>
// <span>~~</span>
// <span>{item.task}</span>
// <button onClick={() => delTodo(item.id)}>删除</button>
// </div>
// ))
// }
// </div>
// </div>
// </div>
// )
// }
// export default Todo
/* 类式写法 */
import { Component } from "react";
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
Todo: "",
list: [],
}
}
getTodo(e) {
this.setState({ todo: e.target.value })
}
addtodo(e) {
if (!this.state.todo.trim()) {
alert("不能为空!")
return
}
this.setState(state => ({
list: [...state.list, { id: Date.now(), task: state.todo }]
}))
this.setState({
todo: ""
})
}
addByenter(e) {
if (e.key === "Enter") {
this.addtodo()
}
}
delTodo(id) {
this.setState(state => ({
list: state.list.filter(item => item.id !== id)
}))
}
render() {
let { todo, list } = this.state;
return (
<div>
<div>
<input type="text" value={todo}
onChange={e => this.getTodo(e)}
onKeyUp={e => this.addByenter(e)}
></input>
<button onClick={e => this.addtodo(e)}>添加</button>
</div>
<div>
{
list.map(item => (
<div key={item.id}><span>{item.id}</span>--<span>{item.task}</span>
<button onClick={() => this.delTodo(item.id)}>删除</button>
</div>
))
}
</div>
</div>
)
}
}
export default Todo
- 受控表单:一个表单的value或checked由state来决定,通过控制state就可以修改表单数据,这样的表单叫受控表单。
- 受控组件:一个组件的自定义属性由状态来控制,只有这个状态发生变化,组件才能更新。后面讲。
- 列表渲染:在React中,通过map方法实现。因为map方法,可以对数据进行加工,返回新的数据(jsx);
六、生命周期
- 装载阶段(3个):constructor, render, componentDidMount
- 更新阶段(2个):render,componentDidUpdate
- 卸载阶段(1个):componentWillUnmount
constructor(装载)
在constructor中,可以完全this的绑定
在官方文档中,还有一种解决办法,就是在constructor中进行this绑定
- 在constructor中有一个props,这个props是用来接收父传递的数据的,props的数据流和state的数据流必须独立。
- 在constructor中,不能调用setState方法
- 在constructor中,不能调用接口,不能进行DOM操作,不能开定时器… 一切和业务相关的代码都不能写在constructor钩子函数中。
- 在constructor中,可以做什么:
- 定义状态
- 绑定方法的this
render(装载)
- 作用:用于返回组件的视图结构,这个结构在vue叫模板,在react中叫jsx。它的背后是生成一棵Fiber树,在vue中叫虚拟DOM,在React叫Fiber树,早期的React也是使用的虚拟DOM。
- 在render函数中,不能调用setState
render是一个函数,这个函数返回一个jsx,jsx叫jsx元素,本质是一个对象,创建jsx元素有2种方法,一种是直接使用jsx,另外一种是React.creeateEleement。也就说,调用render函数,会生成棵Fiber树,类似于Vue中的虚拟DOM,这个Fiber树是双向链表结构。生成这课Fiber树的过程是异步的,生成的过程是可以中断,能不能中断是看浏览器忙不忙,如果浏览器比较忙,就可能中断,等待浏览器不忙时,会继续生成,直到Fiber树创建完成。然后会进行协调运算,这个协调运算类似于Vue中的Diff运算,也就是一棵老的Fiber树和一棵新的Fiber树进行对比运算。运算对比后,就会进入到commmit提交阶段,一次性提交Fiber更新DOM。
componentDidMount(装载)
类似于vue中的mounted,表示页面第一次渲染完成。在这个钩子函数中可以做什么?
- 调用接口
- 开定时器
- DOM操作
- 编写业务逻辑
render(更新)
componentDidUpdate(更新)
- 相当于Vue中的updated,表示页面再次渲染成功。
- 监听器是用来监听数据是否变化,updated表示数据变化了,会执行updated,也就是说数据变化了,在updated也可以监听到了。要监听数据变化,在vue中,使用监听器比使用updated更方便。在react中是没有监听器的概念的,在React中实现类似于Vue的监听器的功能,需要使用compoentDidUpdate钩子函数了。
-
如果不使用componentDidUpdate,还有没有办法实现类似于Vue中的监听器?
- 答:this.setState({}/fn, callback) 利用callback也可以感知到数据变化了。推荐使用componentDidUpdate。因为多个setState会合并,合并后,callback也会出问题
-
你要想在componentDidUpdate中调用setState,需要给出一个出口,不给出口,直接爆栈
-
React实现Vue中的监听器
总结:
- componentDidUpdate中可以调用setState,但是必须给出出口(终止条件),否则会产生死循环,循环到一定次数就会报错。
- componentDidUpdate可以模拟vue中的监听器,特别需要注意终止条件。
- 除了使用componentDidUpdate实现监听器之外,还可以使用this.setState中的callback来实现,不建议使用,因为setState会合并,callback容易出问题。
componentWillUnmount(卸载)
类似于Vue中的beforeDestroy,表示组件即将销毁。
- 清缓存
- 清除定时器
- 关闭长连接
- 销毁DOM元素
…
shouldComponentUpdate(了解)
它是控制更新阶段的开关,就是来控制是否更新,当返回true就正常更新,当返回false时就不更新。
默认情况下,没有写的话,就是返回true,看一下state参数
shouldComponentUpdate,返回true时,正常更新,返回false时,不执行更新阶段。注意
- 当执行forceUpdate时,会绕过shouldComponentUpdate方法,一定会进入到更新阶段。
- shouldComponentUpdate,可以使用PureCompoentf替代
组件中有很多状态,有些状态会参与到界面刷新,也就是说有些状态变了,需要更新页面。但是还有一些状态是不参与到界面更新,也就是状态变了,不需要更新页面的,此时就体现出开关的重要性了。参与页面更新的状态,状态变化了,在showCompoentUpdate中返回true,正常更新。如果没有参与页面刷新的状态变化了,在shouldCompoentUpdate中返回false,就需要再次调用render。这样,就会少生成一次Fiber树。这个钩子函数是用来性能调优的,可以阻塞掉那些不参与视图渲染的状态更新导致的Fiber生成。
-
React组件渲染(更新)流程,由两个阶段组成的,一个叫render阶段,一个叫commit阶段,如下:
-
render阶段:
- 目标是生成Fiber树,这个过程是异步的,是可中断,并且不会执行任何副作用。到底中断与否,看的是浏览器主线程的忙不忙。
-
commit阶段:
- 目的是把协调运算的结果,一次性提交渲染或更新真实DOM。这个过程在V18之前是不可中断的,在V18中是可以人为中断的。
七、状态提升
- 什么是状态提升
- 一个父组件,下面有两个子组件,这两个子组件,需要通信,通常会把这个状态定义到父组件中,通过父传子,再通过子传父,实现兄弟之间的通信,状态提供到父上面,就是所谓的状态提升。
- 在react中没有所谓的自定义属性或自定义事件,都是所谓props。也就是说研究状态提升就是研究所谓的父子通信。
官方小案例
把状态提升到父组件中,传递给子组件
实现子传父,在React中实现如下:
父组件中的代码是可以简写的
把温度状态传递给华氏温度组件,如下
华氏温度组件接收,转化,使用,如下:
现在改变华氏温度
import { Component, useState } from "react"
class Fahrenheit extends Component {
render() {
const { value, onChange } = this.props
let change = (e) => {
// 得到最新的华氏温度,需要回传给父组件
// console.log(e.target.value);
// 把华氏温度变成摄氏温度,回传给父组件
let temper = (Number(e.target.value) - 32) * 5 / 9;
onChange(Number(temper))
}
return (
<div>
<label>华氏温度:</label>
<input type="text" value={value * 9 / 5 + 32} onChange={change} />
</div>
)
}
}
function Celsius(props) {
let { value, onChange } = props;
let change = (e) => {
onChange(e.target.value);
}
return (
<div>
<label>摄氏温度:</label>
<input type="text" value={value} onChange={change} />
</div>
)
}
function DemoA() {
const [temper, setTemper] = useState(0);
return (
<div>
<Fahrenheit value={temper} onChange={val => setTemper(val)}></Fahrenheit>
<hr />
<Celsius value={temper} onChange={val => setTemper(val)}></Celsius>
</div>
)
}
export default DemoA
总结:
-
所谓的状态提升,就是把一个状态提升到父组件中,实现父传子,实现子传父。
-
在React中,一般不讨论自定义属性或自定义事件,写在组件上的都叫props。props的值可以是基本数据,也可以是对象,也可以是数组,当然也可以是函数,也可以是jsx。
-
如果是函数,函数分两类,一类叫事件函数,另一类叫渲染函数。如果是事件函数,建议以on打头,如果是渲染函数,建议不要使用on打头。
<Model a="hello" b={110} onChange={()=>{}} footer={()=>(<footer />)}></Model>
onChange={()=>{}} 叫事件函数
footer={()=>(<footer />)} 叫渲染函数
八、封装一个组件
使用组合的思想封装弹窗组件
样式.scss
.ml-layer {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.6);
.ml-modal {
width: 520px;
background: white;
border-radius: 3px;
position: absolute;
top: 120px;
left: 50%;
margin-left: -260px;
box-sizing: border-box;
header {
line-height: 50px;
padding: 0 20px;
border-bottom: 1px solid #eee;
font-size: 12px;
&>div {
width: 100%;
height: 100%;
overflow: hidden;
&>div:first-child {
float: left;
}
&>div:last-child {
float: right;
cursor: pointer;
}
}
}
footer {
line-height: 50px;
padding: 0 20px;
border-top: 1px solid #eee;
font-size: 12px;
height: 50px;
overflow: hidden;
&>.ml-button {
float: right;
}
}
main {
box-sizing: border-box;
padding: 20px;
font-size: 14px;
}
}
}
.ml-button {
display: inline-block;
cursor: pointer;
margin: 0 15px;
span {
display: inline-block;
height: 30px;
box-sizing: border-box;
line-height: 28px;
font-size: 12px;
border-radius: 2px;
padding: 0 15px;
border: 1px solid transparent;
}
span.default {
border-color: #ccc;
}
span.primary {
color: white;
background: blue;
border-color: blue;
}
span.danger {
color: white;
background: red;
border-color: red;
}
span.info {
color: white;
background: green;
border-color: green;
}
}
代码
import "../asets/modal.scss"
import PropTypes from "prop-types"
import { useState } from "react"
function Button(props) {
let { type, children } = props
return (
<div className="ml-button">
<span className={type}>{children}</span>
</div>
)
}
Button.propTypes = {
type: PropTypes.oneOf(["default", "primary", "danger", "info"]),
children: PropTypes.node
}
Button.defaultProps = {
type: "default",
children: "按钮"
}
function Modal(props) {
let { title, closeable, children, type, onChange } = props
let click = (e) => {//点击遮罩层关闭弹窗
// console.log(123);
// console.log(e.target.className == "ml-layer");
e.target.className === "ml-layer" && onChange()
}
let opt = (e) => {//判断点击按钮做出相关执行
// console.log(e.target.className);
if (e.target.className === "default") {
onChange()
} else if (e.target.className === type) {
console.log(type);
onChange()
}
}
let renderFooter = () => {
let btns = []
switch (type) {
case "primary":
btns = [
<Button type="primary" key="1">确定</Button>,
<Button key="2" >取消</Button>
]
break;
case "danger":
btns = [
<Button type="danger" key="1">取消</Button>,
<Button key="2" >取消</Button>
]
break;
case "info":
btns = [
<Button type="info" key="1">确定</Button>,
]
break;
}
return btns
}
return (
<div className="ml-layer" onClick={click}>
<div className="ml-modal">
<header>
<div>
<div> {title}</div>
<div onClick={() => onChange()}> {closeable && "x"}</div>
</div>
</header>
<main>{children}</main>
<footer onClick={opt}>
{/* <Button type="primary">确定</Button>
<Button type="danger">取消</Button> */}
{renderFooter()}
</footer>
</div>
</div>
)
}
//验证
Modal.propTypes = {
title: PropTypes.string,
closable: PropTypes.bool,
children: PropTypes.node,
type: PropTypes.oneOf(["primary", "danger", "info"])
}
// 非必传项,需要提供默认值
Modal.defaultProps = {
title: "默认小标题",
closable: true,
children: <div>主体默认内容</div>,
type: "info"
}
function PageA() {
let [flag, setFlag] = useState(false)
let change = () => {
// console.log(flag);
setFlag(!flag)
}
return (
<div>
<button onClick={() => setFlag(true)}> open modal</button>
{flag && <Modal title={"添加用户"} closeable type="primary" onChange={change}>
<div>
<input type="text" />
<div>你确认添加此用户么</div>
</div>
</Modal >}
</div >
)
}
export default PageA
效果
++++
import PropTypes from "prop-types"
import "@/assets/style.scss"
import { useState } from "react"
// <Button type="promary">确定</Button>
function Button(props) {
let { type, children, onClick } = props;
return (
<div className="ml-button">
{/* onClick 叫React的合成事件 */}
<span className={type} onClick={onClick}>{children}</span>
</div>
)
}
Button.propTypes = {
type: PropTypes.oneOf(["default", "primary", "danger", "info"]),
children: PropTypes.node,
onClick: PropTypes.func,
}
Button.defaultProps = {
type: 'default',
children: '按钮',
onClick: () => { },
}
function Modal(props) {
let { title, closable, children, type, visiable, onCancel, onOk, footer } = props;
// 根据不同的type,渲染出不同的按钮组
// 写渲染函数
let renderFooter = () => {
let btns = [];
switch (type) {
case "confirm":
btns = [
<Button type="primary" key="1" onClick={onOk}>确定</Button>,
// Button是组件, onClick是事件函数 是一个特殊的props
<Button key="2" onClick={onCancel}>取消</Button>
]
break;
case "danger":
btns = [
<Button type="danger" key="1" onClick={onOk}>删除</Button>,
<Button key="2" onClick={onCancel}>取消</Button>
]
break;
case "info":
btns = [
<Button type="info" key="1" onClick={onCancel}>我知道了</Button>,
]
break;
}
return btns;
}
let handerLayer = (e) => {
// target表示你实打实点击的那个元素
// console.log(e.target);
// currentTarget表示点击那个元素所在的最外层元素
// console.log(e.currentTarget);
// console.log(e.target.dataset.self);
if (e.target.dataset.self) {
onCancel()
}
}
return (
<div className="ml-layer" style={{ display: visiable ? 'block' : 'none' }} data-self="layer" onClick={handerLayer}>
<div className="ml-modal">
<header>
<div>
<div>{title}</div>
<div onClick={onCancel}>{closable && "X"}</div>
</div>
</header>
<main>
{
children
}
</main>
<footer>
{/* 使用渲染函数 */}
{/* {renderFooter()} */}
{footer ? footer() : renderFooter()}
</footer>
</div>
</div>
)
}
Modal.propTypes = {
title: PropTypes.elementType, // jsx, string, null,und, func
closable: PropTypes.bool,
children: PropTypes.node,
type: PropTypes.oneOf(["confirm", "danger", "info"]),
visiable: PropTypes.bool,
onCancel: PropTypes.func,
onOk: PropTypes.func,
footer: PropTypes.func
}
Modal.defaultProps = {
title: "默认小标题",
closable: true,
children: <div>主体内容默认值</div>,
type: "info",
visiable: false,
onCancel: () => { },
onOk: () => { },
footer: () => { }
}
function PageA() {
// 定义一个状态控制弹窗是否显示
let [visiable, setVisiable] = useState(false)
let submit = () => {
setTimeout(() => {
console.log("submit...");
console.log("发送ajax请求...");
setVisiable(false)
}, 200)
}
return (
<div>
<button onClick={() => setVisiable(true)}>open modal</button>
{/* 如果一个props是一个事件函数,建议使用on打头 */}
<Modal
title={"添加用户"}
closable
type="confirm"
visiable={visiable}
onCancel={() => setVisiable(false)}
onOk={submit}
footer={
() => {
return [
<Button type='danger' key="1">残忍离开</Button>,
<Button type='primary' key="2">确定</Button>
]
}
}
>
<div>
<input type="text" />
<div>你确定要添加此用户吗?</div>
</div>
</Modal>
</div>
)
}
export default PageA;
拆分后代码
import PropTypes from "prop-types"
import "@/assets/style.scss"
import { useState } from "react"
// <Button type="promary">确定</Button>
function Button(props) {
let { type, children, onClick } = props;
return (
<div className="ml-button">
{/* onClick 叫React的合成事件 */}
<span className={type} onClick={onClick}>{children}</span>
</div>
)
}
Button.propTypes = {
type: PropTypes.oneOf(["default", "primary", "danger", "info"]),
children: PropTypes.node,
onClick: PropTypes.func,
}
Button.defaultProps = {
type: 'default',
children: '按钮',
onClick: () => { },
}
function ModalHeader(props) {
let { title, closable, onCancel } = props;
return (
<div>
<div>{title}</div>
<div onClick={onCancel}>{closable && "X"}</div>
</div>
)
}
function ModalFooter(props) {
let { type, onCancel, onOk, footer } = props;
let renderFooter = () => {
let btns = [];
switch (type) {
case "confirm":
btns = [
<Button type="primary" key="1" onClick={onOk}>确定</Button>,
// Button是组件, onClick是事件函数 是一个特殊的props
<Button key="2" onClick={onCancel}>取消</Button>
]
break;
case "danger":
btns = [
<Button type="danger" key="1" onClick={onOk}>删除</Button>,
<Button key="2" onClick={onCancel}>取消</Button>
]
break;
case "info":
btns = [
<Button type="info" key="1" onClick={onCancel}>我知道了</Button>,
]
break;
}
return btns;
}
return (
footer ? footer() : renderFooter()
)
}
function Modal(props) {
let { children, visiable, onCancel, width } = props;
let handerLayer = (e) => {
if (e.target.dataset.self) {
onCancel()
}
}
return (
<div className="ml-layer" style={{ display: visiable ? 'block' : 'none' }} data-self="layer" onClick={handerLayer}>
<div className="ml-modal" style={{ width: `${width}px`, marginLeft: `-${width / 2}px` }}>
<header>
{/* 叫props穿透 */}
<ModalHeader {...props}></ModalHeader>
</header>
<main>
{
children
}
</main>
<footer>
<ModalFooter {...props}></ModalFooter>
</footer>
</div>
</div>
)
}
Modal.propTypes = {
title: PropTypes.elementType, // jsx, string, null,und, func
closable: PropTypes.bool,
children: PropTypes.node,
type: PropTypes.oneOf(["confirm", "danger", "info"]),
visiable: PropTypes.bool,
onCancel: PropTypes.func,
onOk: PropTypes.func,
footer: PropTypes.func,
width: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
])
}
Modal.defaultProps = {
title: "默认小标题",
closable: true,
children: <div>主体内容默认值</div>,
type: "info",
visiable: false,
onCancel: () => { },
onOk: () => { },
footer: () => { },
with: 520
}
function PageA() {
// 定义一个状态控制弹窗是否显示
let [visiable, setVisiable] = useState(false)
let submit = () => {
setTimeout(() => {
console.log("submit...");
console.log("发送ajax请求...");
setVisiable(false)
}, 200)
}
return (
<div>
<button onClick={() => setVisiable(true)}>open modal</button>
{/* 如果一个props是一个事件函数,建议使用on打头 */}
<Modal
title={"添加用户"}
closable
type="confirm"
visiable={visiable}
onCancel={() => setVisiable(false)}
onOk={submit}
footer={
() => {
return [
<Button type='danger' key="1" onClick={() => setVisiable(false)}>残忍离开</Button>,
<Button type='primary' key="2">确定</Button>
]
}
}
width={800}
>
<div>
<input type="text" />
<div>你确定要添加此用户吗?</div>
</div>
</Modal>
</div>
)
}
export default PageA;
什么是组合?
组合是React组件化的设计模式。也就是研究如何封装一个组件,步骤如下:
- 第一步:根据UI设计图拆解组件。
- 第二步:把这个独立的组件单独进行封装。
- 第三步:利用props把组件串联起来。
九、上下文
1、什么是上下文
在React中,利用上下文,进行组件间的通信。类似于Vue中的proveder/inject。
特点:
- 在组件树中,上下文中一种单向数据流通信,不能颠倒。
- 通信是可以跨级的,祖先提供数据,后代消费数据。
- 这个通过方法,不具有响应式。
创建对应的组件
上下文是为了实现通信,需要创建一个上下文
使用上下文,有两种方案,1、
先在祖先中提供数据
看Child能不能获取数据
上面是我们使用上下文的方式一,还有一种方式,是推荐的
总结使用上下文的步骤:
第一步:创建const ThemeContent = React.createContext()创建上下文
第二步:使用Provider提供数据,是给后代提供数据
第三步:消费上下文中的数据有两种方案。
小案例
import React, { PureComponent, useState } from "react";
const ThemeContext = React.createContext();
// Provider是用来提供数据
// Consumer是用来消费数据
const { Provider, Consumer } = ThemeContext
// 上下文只能类组件才有
class Child extends PureComponent {
render() {
console.log(this);
return (
<Consumer>
{
(ctx) => {
return (
<div style={ctx}>
<h3>我是孩子组件</h3>
</div>
)
}
}
</Consumer>
)
}
}
function Parent(props) {
return (
<div>
<h3>父组件</h3>
<Child />
</div>
)
}
function PageA() {
let obj = { a: 1 }
// 在JSX中是不能直接渲染一个对象,但是可以渲染一个数组
const [theme, setTheme] = useState({ color: "#000000", background: "#ffffff" })
let change = (key, e) => {
setTheme({ ...theme, [key]: e.target.value })
}
return (
<Provider value={theme}>
<div>
<div>
<h2>页面</h2>
<hr />
<Parent />
</div>
<div>
前景色: <input type="color" value={theme.color} onChange={e => change('color', e)} />
背景色: <input type="color" value={theme.background} onChange={e => change('background', e)} />
</div>
</div>
</Provider>
)
}
export default PageA
效果如下 :
把颜色切换,可以封装成一个组件,如下:
import React, { PureComponent, useState } from "react";
const ThemeContext = React.createContext();
// Provider是用来提供数据
// Consumer是用来消费数据
const { Provider, Consumer } = ThemeContext
// 上下文只能类组件才有
class Child extends PureComponent {
render() {
console.log(this);
return (
<Consumer>
{
(ctx) => {
return (
<div style={ctx}>
<h3>我是孩子组件</h3>
</div>
)
}
}
</Consumer>
)
}
}
function Parent(props) {
return (
<div>
<h3>父组件</h3>
<Child />
</div>
)
}
function ThemeToggle({ theme, onChange }) {
let change = (e) => {
let key = e.target.name;
let val = e.target.value;
onChange({ ...theme, [key]: val })
}
return (
<>
<div>
前景色: <input type="color" name="color" value={theme.color} onChange={change} />
背景色: <input type="color" name="background" value={theme.background} onChange={change} />
</div>
</>
)
}
function PageA() {
let obj = { a: 1 }
// 在JSX中是不能直接渲染一个对象,但是可以渲染一个数组
const [theme, setTheme] = useState({ color: "#000000", background: "#ffffff" })
let change = (key, e) => {
setTheme({ ...theme, [key]: e.target.value })
}
return (
<Provider value={theme}>
<div>
<div>
<h2>页面</h2>
<hr />
<Parent />
</div>
{/* onChange={e => setTheme(e)} 叫事件函数 */}
<ThemeToggle theme={theme} onChange={e => setTheme(e)}></ThemeToggle>
</div>
</Provider>
)
}
export default PageA
效果如下:
-
上下文的使用场景:
- 路由中会使用到上下文
- 状态管理中也会使用到上下文
- 在一些组件库,如切换主题,切换组件大小… 也会使用到上下文
- 在际化中,也会使用到上文
-
总结React中的组件通信:
- 状态提升(父传子,子传父),核心靠props
- 上下文,是祖先与后代之间的通信,父子关系不需要明确
- props穿透,需要搞清楚,父子关系,缺点:会让后代的props变得臃肿。
十、高阶组件
HOC = Higher Order Compoent。高阶组件是类组件编程中一种非常重要的代码逻辑复用技巧。
可以使用ES6中的装饰器语法
高阶组件可以简写,如下:
foo就可以叫高阶组件,也叫高阶函数,接收一个组件作为入参,经过一翻装饰(仅仅是装饰),最后返回一个新的组件。对入参组件是不能修改的。
前面返回的新组件是一个类组件,现在这让它返回函数组件,如下:
简写
高阶组件的定义方式通常有两种:
第一种:const hoc = (WC) => props => (<jsx/>) 使用: hoc(DemoA)
第二步:const hoc = (...arg) => WC => props => (<jsx/>) 使用:hoc(1,2)(DemoA)
可以装饰多次,如下:
把上面装饰的写法,换成函数调用如下:
DemoA组件接收的props,
想获取所有的props,如下:
上面的foo和bar都可以叫高阶组件,使用:
- 在类组件上可以通过 hoc(DemoA) , 还可以通过ES6中的装饰器
- 在函数组件上,只能通过 hoc(DemoA)
高阶组件的应用
-
高阶组件应用的也非常多,如下:
- 路由中的withRouter()
- Redux中的connect()
- Mobx中的inject和observer()
- 利用高阶组件,可以给每个项目添加公司的视图结构
- 实现后面管理系统中的水印
- 实现权限校验
- 把重复的业务功能封装到高阶组件,如:埋点,DOM操作,BOM操作…
…
封装一个高阶组件,可以给不同的页面添加不同的title,如下:
十一、Ref和Ref转发
1,Ref写在DOM元素上
2,Ref写在子组件上
注:不能写在函数组件上
Ref转发
如果把一个ref写在函数组件上,会后错,此时就需要使用ref转发,ref转发目的是为了获取函数式组件中的DOM元素。如下:
12、Hooks
1.什么是hook
hook是React官方提供的API
- 作用:用于在函数式组件中模拟出类组件的功能。如:state,生命周期,ref,上下文…
- 价值:有了hook,我们就可以不再使用类组件。官方说了,并不是要淘汰类组件。hook不能出现在类组件。
- 哪些:useState, useEffect, useLayoutEffect,useContext, userReducer, useRef, useMemo, useCallback…
- 开源hook: react-use, ahooks…
2.useState
useState是用于在函数式组件中定义状态的。
3.useEffect
effect是副作用的意思。作用:是用来模拟类组件中的生命周期的,不是模拟所有的生命周期,是模拟这三个componentDidMount/componentDidUpdate/componentWillUnmount生命周期。
类似于vue中的watchEffect,vue中的watchEffect会自动依赖依赖,React中的useEffect,需要手动指定依赖。
语法:
useEffect(() => {
fn1();
return fn2()
}, [依赖数组])
在fn1中写一些副作用,在fn2中清除副作用。
useEffect的工作流程:
- 当没有“依赖数组”这个参数时,初始化只执行fn1,当re-render时,先执行fn2,再执行fn1。当路由切换时,只执行fn2。
- 当有“依赖数组”这个参数时,但是是一个空数组,初始化只执行fn1,当re-render时,什么也不执行。当路由切换时,只执行fn2。
- 当有“依赖数组”这个参数时,但是不是一个空数组,初始化只执行fn1。有且仅有当“依赖数组"中的变量发生变化导致rerender时,先执行fn2,再执行fn1()。当路由切换时,只执行fn2。
语法总结:
- 格式:useEffect(()=>{fn1, return fn2}, [依赖数组])
- 情况一:没有依赖数组,初始化时,执行fn1,rerender时,先fn2,再fn1。路由切换只执行fn2。
- 情况二:有依赖数组,是一个空数组。初始化时,执行fn1,rerender时,fn2和fn1都不执行。路由切换只执行fn2。
- 情况三:有依赖数组,依赖数组中有状态。初始化时,执行fn1,只有依赖数组中的状态变化了,rerender时,先fn2,再fn1。路由切换只执行fn2。
- fn1相当于类组件中的componentDidMount
- fn2相当于类组件中的componentWillUnMount
- 依赖数组,相当于类组件中的componentDidUpdate
注意
- 在一个函数式组件中,可以书写多个useEffect,多个useEffect,它们是彼此不影响
- useEffect是用来执行副作用,建议一个useEffect只执行一个副作用。不要在同一个useEffect中同时执行多个副作用。
- 在函数式组件中,不要把副作用直接暴露在函数体内,一定要使用useEffect进行控制
4.useLayoutEffect
- 运行机制和useEffect是一样的,区别在于useLayoutEffect执行时候更早
- 在这个hook中,不能进行ref或dom操作。
5.useContext
函数式组件中是没有上下文的,使用useContext,就可以在函数式组件中使用上下文
语法:
const ctx = useContext(上下文对象)
6.useRef
如果ref写在函数组件标签上,会报错。需要使用ref转发,转发到了函数式组件中的JSX中的DOM标签上。进而获取函数式组件中的JSX中的DOM元素。
7.useMemo
作用:用于性能优化,用于把一些比较消耗性能的计算进行缓存,类似于vue中的计算属性。
语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
-
特点:有且仅有当”依赖数组"中的状态发生变化了,useMemo才会重新执行那个昂贵的计算。
-
注意:useMemo的第二个参数是依赖数且,也有三种写法,和useEffect一样。
-
computeExpensiveValue表示昂贵计算。
8.useCallback
作用:用于性能优化,用来缓存一个函数声明。
可以使用useMemo去代替useCallback
语法:
const fn = useCallback(() => {}, [依赖数组])
9.useReducer
useState是用来定义状态的,如果有很多的状态,通过需要写多个useState。使用useReducer有一个好处,一个useReducer可以代替多个useState。是在React函数式组件中模块Redux的数据流。
语法:
const [state, dispath / foreUpdate / setState] = useReducer(reducer, {
初始值
})
import { useState, useEffect, useReducer } from "react"
// state表示状态,action表示信号,根据不同的信号,就可以针对性地修改状态
// reducer 是管理员的意思,要修改状态必须通过reducer
// 管理员根据信号进行状态的修改
// 修改状态的流程:1)对于state进行深copy 2)修改更新state 3)返回修改后的state
const reducer = (state, action) => {
let newState = JSON.parse(JSON.stringify(state)); // 1)对于state进行深copy
// 根据信号更新state
switch (action.type) { // action是一个对象,对象中有一个type,不同的type表示不同的信号
case "NUM_ADD":
newState.num += 1
break;
case "NUM_SUB":
newState.num -= 1
break;
}
return newState;
}
// 定义初始值
const initState = {
num: 1,
list: ["a", "b"],
falg: true
}
const A = props => {
// dispatch 是用来派发一个action,管理员就可以收到action
const [state, dispatch] = useReducer(reducer, initState);
return (
<div>
<h2 >函数组件</h2>
<h3>{state.num}</h3>
<h3>{state.list}</h3>
<button onClick={() => dispatch({ type: "NUM_ADD" })}>+1</button>
<button onClick={() => dispatch({ type: "NUM_SUB" })}>-1</button>
</div>
)
}
export default A
10.useId
作用:返回一个唯一的标识,在函数式组件的整个运行过程中都是唯一的。
11.useDeferredValue
作用:和防抖类似。和真正的防抖区别在于,这个hook所延迟的时间是不确定,由浏览器自己决定。
13、路由
安装路由:
cnpm i react-router-dom -S
路由相关的有三个包:
- react-router 路由核心包
- react-router-dom 基于浏览器的路由包,专用于做web开发
- react-router-native 基于RN平台的路由包,专用于原生APP开发
14、antd组件库
官网:https://ant.design/docs/react/introduce-cn
安装antd中的字体图标
cnpm install --save @ant-design/icons