React全家桶

1
2
3
4
5
6
7
8
9
10
React基础、React-Router路由、PubSub消息管理、Redux状态管理、Antd组件库

js缺点:Dom操作繁琐效率低、重绘重排、组件化
react优点:组件化声明式编码、移动端开发、虚拟dom的diffing算法

高效原因:1使用虚拟dom不总是直接操作真实dom2使用diffing算法,最小化更新dom

项目创建命令
npx create-react-app my-app
npm install antd --save
基本使用
1
2
3
4
5
6
7
8
    <script src="https://cdn.staticfile.org/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/18.2.0/umd/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>

<script type="text/babel">
let VDOM = <h1 id="test">Hello</h1>
ReacrDOM.render(VDOM,document.getElementById("root"))
</script>
创建虚拟dom
1
2
3
4
5
6
7
8
9
10
11
12
2种方式,React.createElementJSX
//jsx和js方式
<script type="text/javascript">
let VDOM = React.createElement(
"h1",
{ id: "test"},
"Hello"
);
ReacrDOM.render(VDOM,document.getElementById("root"))
console.log(VDOM,typeof VDOM,VDOM instanceof Object)
debugger;
</script>
jsx语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
jSX语法规则
1 虚拟dom不加引号
let dom =<h1>hello</h1>

2 标签中混入js表达式,用{}
const content="hello"
let dom = <h1>{content}</h1>

3 样式名用className
let dom =<h1 className="test">hello</h1>

4 内联样式,用style={{key:value}},字体用fontSize
let dom =<h1 style={{color:'red'}}>hello</h1>

5 只有1个根标签

6 标签:首字母小写,则转为html同名元素,如html无good则报错
首字母大写,则转为组件,若组件未定义则报错
let dom=<div> <good/> <Test/> </div>

7 {data} data是数组它会自动遍历,是对象会报错
const data=['1','2','3']
let dom= <ul>{data}</ul>

8 {}中可以写js表达式,不能写js语句,如forif
表达式会产生一个值,如data.map()
const data=['1','2','3']
let dom= <ul>{
data.map((item,index)=>{
return <li key={index}>{item}</li>
})
}</ul>
表达式:a、a+b、demo(1)、arr.map()、function test()
语句:if(){}、for()、switch(){case}

例-渲染数组data=[a,b,c]
const VDOM =(
<ul>
{data.map((item,index)=>{
return <li key={index}>{item}</li>
})}
</ul>
)
遍历需要指定key,用index不是很好
函数组件
1
2
3
4
5
6
function Test(){
return <h1>Hello</h1>
}
ReactDOM.render(<Test/>,document.getElementById("root"))
Test作为组件,首字母必须大写
es5有个严格模式,禁止自定义函数里的this指向window,babel编译后会开启严格模式,因此Test()里不能用this
复习类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person{
constructor(name){
this.name =name
}
speak(){ //speak()放在类的原型对象上,供实例使用
console.log(`我是${this.name}`) //类里this指向Person的实例
}
}
const p1 = new Person('张三')
p1.speak()

class Student extends Person{
constructor(name,age){
super(name) //子类的构造器里必须使用super,获取父类的属性
this.age =age
}
speak(){} //可以重写父类的方法
}
总结
1 类中构造器不是必须写的,要对实例初始化时才写
2 子类构造器里必须使用super
3 类中方法都是放在Prototype上
类组件
1
2
3
4
5
6
7
8
9
10
11
class Test extends React.Component{
render(){
console.log(this)
return <h1>Hello </h1>
}
}
ReactDOM.render(<Test/>,document.getElementById("root")
render()中的this是组件实例,而组件中的自定义方法中的this是undefined,通过函数对象.bind(this)解决

注意对于单行return <h1>Hello</h1>,对于多行必须加括号
return ( <h1>Hello</h1> )
state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
name:'张三',
};
this.changeName=this.changeName.bind(this) //用下边这个函数重新创建了一个带this的函数
}
changeName(){ this.setState({name:'李四'}) } //谁调用的changeName函数,这里的this就是谁,为了操作数据,我们需要这里是Test实例,因此下边button那里要用实例Test调用
changeName1(){ this.setState({name:'李四'}) }
changeName2=()=>{ this.setState({name:'李四'}) }//只有这种不存在this指向问题
render() {
return (
<div>
<h1>{this.state.name}</h1>
<button onClick={this.changeName}>点击</button> //写法1:在construcotr里bind(this)否则报错

<button onClick={this.changeName1.bind(this)}>点击</button> //写法2:在jsx里bind(this)
<button onClick={()=>this.changeName1()}>点击</button> //写法3:在jsx里用箭头函数,注意这里带括号

<button onClick={ this.changeName2 }>点击</button>//写法4:在类里用箭头函数定义

<button onClick={ changeName3 }>点击</button> //this是window,拿不到实例数据
</div>
);
}
}
changeName3(){ }
ReactDOM.render(<Test />, document.getElementById("root"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
1 当changName3写在组件外时,onClick={changeName3}可以调用,但是这个函数拿不到state数据,除非
let that
class Test{ that=this }
changeName3( console.log(that.state.name) ) 但这种写法很恶心

2 changeName函数必须不能加()加了会直接执行,不加表示给onClick赋值一个回调函数,当点击时去执行这个函数

3 当用Test的实例对象调用changeName时,changeName内的this就是Test实例对象
constructor中this是实例,如 this.state = {name:'张三'};
render中this也是实例,如 <h1>{this.state.name}</h1>
但为什么button里的this,如果写<button onClick={this.changeName}></button>就不是实例

Person类中定义一个speak()方法,(类中所有方法都局部开启了严格模式,使它不敢指向window而只能指向实例对象)
const p =new Person
p.speak()这是正常的调用,没有问题
const x =p.speak, x(),这是把speak函数赋值给了x,然x自执行,没有调用者,此时会指向全局对象window,但由于类中方法有严格模式,它不敢指向window,于是它变成了undefined

同理,上边react里是把this.changeName函数赋值给了onClick,而onClik执行时也会指向window,而由于类中方法有严格模式,它不允许指向window,于是变成了undefined

解决一bind(this):
写法1
constructor里写this.changeName=this.changeName.bind(this) + <button onClick={this.changeName}></button>
在类中写上这句话this.changeName=this.changeName.bind(this),会拿原型上的方法创建一个新的方法,挂在实例上,此时Test实例的原型和实例上都有changeName函数,不过实例上的这个被指定了this是实例对象,实例上有它就用实例上的,不用原型的那个了

写法2
不往实例上挂了,直接传过去带实例对象的函数<button onClick={this.changeName.bind(this)}></button>

解决二箭头函数:
写法3
<button onClick={()=>this.changeName()}></button>
注意这里的this.changeName()带括号,这里实际相当于定义了一个新函数赋值给了onClick
写法4
类内方法 changeName=()=>{this.setState({name:'李四'})} + <button onClick={this.changeName}></button>
当点击事件不需要传参时,第4种最好,但是需要传参,这种不带括号的写法就不能用了,此时用第3种最好

4 如果不用state里的值,直接在类构造器里this.name='李四',那么改变这个值,页面不会变化
修改state的值,必须用setState,不能直接改this.state.name='李四'错误

setState是合并不是整个替换,只会更新setState里有的参数,state里的其他参数不会变

5 Test实例里的construtor页面使用一次就只调用一次,而里边的render()会调用1+n次,第一次渲染调用一次,而每次修改页面都会调用一次
注意点:render里的this是实例,自定义函数里的this是调用者,箭头函数里this是函数外层上下文this指向,state值不能直接修改
state简写
1
2
3
4
5
6
7
8
9
1 类中可以直接写赋值语句,不必写在constructor里
class Test{
state={ name:"张三"}
}

2 类内写函数changeName(){ } 会挂载在原型上
类内写赋值形式的函数changName=function(){} 会挂在实例上,这样写也不必写在constructor里

3 changeName=()=>{console.log(this.state.name)} 箭头函数没有自己的this,它会用外层函数的this,就是实例,这样写button不会出现this指向问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//简写后的代码
class Test extends React.Component {
state = { name: '张三' }
changeName=()=>{ this.setState( {name:'李四'} ) }
render() {
return (
<div>
<h1>{this.state.name}</h1>
<button onClick={this.changeName}>点击</button>
</div>
);
}
}
ReactDOM.render(<Test />, document.getElementById("root"));
state函数写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
基本用法
import { useState } from 'react'
function Test(){
const [num,setNum]=useState(5) //5是初始值
return <button onClick={()=>setNum(num+1)}>{num}</button>
}
自定义修改数据的方法
function Test(){
const [num,setNum]=useState(5)
const changeNum=()=>{setNum(10)}
return <button onClick={changeNum}>{num}</button>
}
点击事件需要传参时
function Test(){
const [num,setNum]=useState(5)
const changeNum=(val)=>{setNum(val)}
return <button onClick={()=>changeNum(100)}>{num}</button>
}
props
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Person extends React.Component{
render(){
return <h1>你好,{this.props.name}</h1>
}
}
ReactDOM.render(<Person name="星辰"/>,root)

1 批量传参 ReactDOM.render(<Test {...data}/>,root) //这里react语法设计使对象可以用展开符
2 传数字参数 ReactDOM.render(<Test age={18} />,root) //age='18'是字符串,数字要加{}
3 对props进行限制(传参类型,是否必传,未传时的默认值)
Person.propTypes={
name:React.PropTypes.string //旧写法:react15以前React有这个属性,16以后放在了单独的依赖包prop-types,注意大小写
}
yarn add prop-types
<script src="node_modules\prop-types\prop-types.min.js"></script>
Person.propTypes={
name:PropTypes.string.isRequired 限制了string类型,并且必须传参
}
Person.defaultProps ={ name:'星辰'} 指定了默认值
如果限制函数,speak:PropTypes.func
4 props是只读的,不要去修改
5 简写方式(把限制都写在组件内)
class Person extends React.Component{
static propTypes={ name:React.PropTypes.string }
static defaultProps ={ name:'星辰' }
render(){ return <h1>你好,{this.props.name}</h1> }
}
6 讨论constructor,如果不省略constructor,此时不接收props的话,构造器里无法使用this.props取值
constructor(props){
super(props)
this.state={name:this.props.name}
}
1
2
3
4
5
6
7
8
补充:js中...和reduce的用法
js中展开运算符...能用于数组,不能用于对象,console.log(...obj)报错 console.log(...arr)正确
如果要拷贝,可以用深拷贝 或者 ...加花括号 let obj1 = {...obj}正确,但这个和react里的花括号不一样
对象合并 let obj3 = {...obj1,...obj2} ,let obj3={...obj,name:'lxc'}

数组方法arr.reduce((pre,curr)=>{ //pre是之前的值-遍历上个数据时返回的值,curr是当前的值-遍历的当前数据的值
return pre + curr //注意:第一次遍历时,pre是第一项,curr是第二项,因此curr是从第二项开始的
})
props函数式写法
1
2
3
4
5
6
function Person(props){
return <h1>你好,{props.name}</h1>
}
限制类型、是否必传、设置默认值,和类写在组件外一样
Person.propTypes={}
Person.defaultProps ={}
ref
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1 字符串ref,存在问题不推荐(见下述123)
<h1 id="abc">Hello </h1>
换成<h1 ref="abc">Hello </h1>
那么在自定义方法内就可以通过refs拿到节点,(拿到的是真实节点,不是虚拟dom,它身上有所有属性,虚拟dom的属性很少)
showData=()=>{ console.log(this.refs.abc) }

问题:1this指向,react要持续追踪组件,性能差
2父组件里用到子组件Table,在一行row上写字符串ref,它可能会被绑到Table组件上,而不是父组件
3比如一个组件库在组件上用了ref,用户就不能再用ref了

2 回调函数ref
<h1 ref={ c=>this.abc=c } >Hello </h1>
showData=()=>{ console.log(this.abc.innerText) }
.1 c=>this.abc=c是一个回调函数()=>{ },这里不会直接执行,react后边会去调用
.2 这里的参数c就是当前节点,用c=>console.log(c)可以证明,这里把节点赋值给了this.abc组件实例上的abc属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.3 探讨c=>this.abc=c会执行几次,第一次render()会调一次,如果ref回调函数是内联函数形式-即html标签中写函数,那么每次更新render()时,react会重新创建ref实例,他会先清空,再重新创建,打印发现每次页面更新有2次输出,第1次c是null,第2次c是节点

.4 jsx中注释方法{/* */}
class Test extends React.Component {
state = { name: "张三" };
changeName = () => { this.setState({ name: "李四" })}
render() {
return (
<div>
<h1 ref={(c) => {this.abc = c;console.log("@", c);}}>
{this.state.name}
</h1>
<button onClick={this.changeName}>点击</button>
</div>
);
}
}
ReactDOM.render(<Test />, document.getElementById("root"));
改成类组件的函数形式可以避免调2次,打印发现页面更新,1次都不会重新调用(官方说改不改无关紧要,但是如果我们需要子组件值改变时,父组件得到通知,比如想赋值到state里,那么最好用上边内联函数形式)
class Test extends React.Component {
state = { name: "张三" };
changeName = () => {this.setState({ name: "李四" })}
saveRef = (c) => { this.abc = c; console.log("@", c)} //类组件函数形式的回调
render() {
return (
<div>
<h1 ref={this.saveRef}>{this.state.name}</h1>
<button onClick={this.changeName}>点击</button>
</div>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
3 createRef
调用后返回一个容器,这个容器可以存储被ref所标识的节点,拿取数据时要加.current
该容器是专人专用,只能存一个节点,2个标签都用同一个容器,第一个会被覆盖掉,因此2个ref就要创建2个容器
class Test extends React.Component {
state = { name: "张三" };
myRef=React.createRef() //创建容器
showData=()=>{console.log(this.myRef.current)} //使用容器的数据,必须.current
render() {
return (
<div>
<h1 ref={this.myRef}>Hello</h1> {/* 用容器存储节点 */}
<button onClick={this.showData}>点击</button>
</div>
);
}
}
ref函数式写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
1 useRef
useRef返回一个可变的ref对象,返回的ref对象在整个生命周期内保持不变
import { useRef } from "react";
function Test(){
const myRef =useRef(null) //创建容器
const showData=()=>{console.log(myRef.current)} //使用容器中的数据,需要.current
return (
<div>
<h1 ref={myRef}>Hello</h1> //用容器存储节点
<button onClick={showData}>点击</button>
</div>
)
}

2 forwardRef
如果ref获取的节点不是普通标签,而是一个组件时,可以用这个-转发ref
上边代码全都不变,只把<h1 ref={myRef}>Hello</h1>改成
<Son ref={myRef} /> 此时报错是否是要用forwardRef
定义子组件
const Son = forwardRef((props,ref) => {
return (<h1 ref={ref}>Hello</h1>)
})
此时上边的父组件Test()打印的myRef.current,就是子组件里ref容器存储的节点,<h1>Hello</h1>
如果要拿整个子组件,我们就把ref={ref}放到最外层的div上

React.forwardRef接受一个渲染函数,该渲染函数接收props和ref参数并返回一个React节点(说白了就是函数组件),
它可以把父组件中的ref容器转发到子组件里,然后存储子组件里想要的节点

3 useImperativeHandle
如果子组件不想全部暴露给父组件,只暴露指定的方法和值
只把2中子组件修改为
const Son = forwardRef((props,ref) => {
useImperativeHandle(ref, () => ({ //用ref容器存方法和值,父组件通过.current.name可以取值
showData:()=>{console.log('@')},
name:'张三'
}))
return (<div><h1 >Hello</h1></div> )
})

4 回调ref
上边存在问题,ref对象内容发生变化时,useRef并不会通知你。变更.current属性不会引发组件重新渲染
const Son = forwardRef((props,ref) => {
const [sonVal, setSonVal] = useState("张三");
const changeSonVal =()=>{setSonVal("李四")}
useImperativeHandle(ref, () => ({
val:sonVal //子组件点击按钮会改变这个值
}))
return (<button onClick={changeSonVal}>{sonVal}</button>)
})
function Test(){
const [val, setVal] = useState("666");
const myRef =useRef(null)
useEffect(() => {
console.log(myRef.current.value) //这里初次渲染和改变后,打印都是undefined
setVal(myRef.current.value);
}, [myRef]);
const changeVal=()=>{
console.log(myRef.current.val)
setVal(myRef.current.val)
}
return (
<div>
<Son ref={myRef} />
<button onClick={changeVal}>{val}</button> //父组件不能直接用myRef.current.val会报错
</div>
)
}
由于子组件的值myRef.current.val不能直接用,每次改变时,必须调方法赋值到父组件里setVal(myRef.current.val)
父组件添加useEffect(让myRef改变时就触发setVal(myRef.current.value)也不管用,值是undefined)

解决方法
将const myRef =useRef(null)
改为
const myRef = useCallback(node => { //这里node就是想要的节点或数据
if (node !== null) {
console.log(node.val)
setVal(node.val);
}
}, []);
打印发现,无论初次渲染还是更新时,回调里的代码都会自动执行,初次渲染时会执行3次,更新时会执行2次
事件处理
1
2
3
4
1 通过onXxx属性指定事件处理函数(注意大小写)
react使用的是自定义事件,而不是原生DOM事件-如onclick重新封装成onClick,(为了更好的兼容性)
react的事件是通过事件委托方式处理的(委托给组件最外层元素)-如点击li委托给了ul处理,(为了高效)
2 通过event.target得到发生事件的DOM元素对象-不要过度使用ref
收集表单
1
2
3
4
5
6
7
8
9
10
11
handleSubmit(event){event.preventDefault()}可以阻止页面跳转等默认事件
非受控组件-现用现取,用什么值就要去取
受控组件-页面中只要输入就被维护到state里-等同双向数据绑定


onChange="this.saveData('username')"这样写会把this.saveData('username')的返回值交给onChange会报错
saveData=(type)=>{
return (event)=>{
setState({type:event.target.value})
}
}
高阶函数和柯里化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
符合2个规范中任何一个
1 接收的参数是函数
2 返回值是函数
常见的高阶函数:promise、setTimeout、arr.map()

1 函数的柯里化:通过函数的返回值仍是函数,可以实现多次接收参数进行统一处理
function sum(a){
return (b)=>{
return a+b
}
}

2 不用柯里化传参的写法:onChange={event=>this.saveData('type',event)}

.md文件插入文字后边被覆盖,按键盘insert可以恢复
生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1 触发组件卸载
ReactDOM.unmountComponentAtNode(document.getElementById('test'))

2 组件挂载
timer= setInterval(()=>{},100)写在组件里必须去用调用才生效
const timer= setInterval(()=>{setState()},100)写在render()里会自动执行,但这里会陷入死循环,setState()改变页面,render()就会执行创建一个新的定时器
解决方法
render()有个同级的方法,组件挂载完毕时会调用,render()其实也是生命周期钩子
componentDidMount(){
this.timer= setInterval(()=>{},100)
}

3 组件卸载前
componentWillUnmount(){
clearInterval(this.timer)
}
旧版生命周期
1
2
3
4
5
6
7
8
9
执行顺序
初始化 constructor--componentWillMount--componentWillMount--componentDidMount
更新 setState--shouldComponentUpdate--componentWillUpdate--render--componentDidUpdate
卸载 componentWillUnmount


.1 调用this.setState()更新时,shouldComponentUpdate必须返回布尔值,return false就会阻止更新
.2 调用this.forceUpdate()更新时,不会执行shouldComponentUpdate钩子
.3 props改变更新时,先componentWillReceiveProps再shouldComponentUpdate,这个钩子有个坑,初始化时不触发,其实应该叫 Props更新钩子,在shouldComponentUpdate前执行
新版生命周期
1
2
3
4
5
.1 componentWillMount、componentWillUpdate、componentWillReceiveProps这3个钩子即将被弃用,需要加UNSAFE_前缀

.2 新增了2个钩子getDerivedStateFromProps(初始化和3种更新的最先执行)和getSnapshotBeforeUpdate(3种更新的DidUpdate前执行)

废了3个,新增2个又很少用,因此DidMount、shouldComponentUpdate、componentDidUpdate、componentWillUnmount就这4个常用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
getDerivedStateFromProps:从props派生state,让state里的值任何时候都取决于props

static getDerivedStateFromProps(props,state){ //必须用static
return props //必须返回null或者state对象,这里会用props改变state里的值
}
下边打印发现,Son子组件中state中的name会跟着接收的props变化,而在Son里修改state里的name不管用

class Son extends React.Component {
state = { name: "888" };
static getDerivedStateFromProps(props, state) {
return props;
}
componentDidMount(){
console.log('mount',this.state.name) //打印输出为:张三
}
componentDidUpdate(){
console.log('update',this.state.name)//点击后打印输出为:李四
}
render() {
return <h1>{this.state.name}</h1>;
}
}
class Test extends React.Component {
state = { name: "张三" };
changeName = () => {
this.setState({ name: "李四" });
};
render() {
return (
<div>
<Son name={this.state.name} />
<button onClick={this.changeName}>点击</button>
</div>
);
}
}
1
2
3
4
5
6
7
8
getSnapshotBeforeUpdate:在更新前获取快照,可以给DidUpdate传参,类似WillUpdate

getSnapshotBeforeUpdate(prevProps,prevState){ //它接收2个参数,上一次更新前的props和state
return 'abc'//必须返回null或者快照(比如页面的滚动位置),这个参数会传给DidUpdate
}
componentDidUpdate(prevProps,prevState,snap){ //这个钩子接收3个参数,上一次更新前的props和state,快照值
console.log(snap) //打印出来就是abc
}
Diffing算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
面试题 react/vue中key的作用?内部原理是什么?为什么key不要用index?

简单说,key是虚拟dom的标识,在更新时key起重要作用
详细说,数据变化时,react根据新数据生成【新虚拟dom】,再把【新虚拟dom】和【旧虚拟dom】进行比较,规则如下
1 先找相同的key,若旧虚拟和新虚拟一样,则不变,若不一样,则生成新的真实dom替换页面
2 旧虚拟找不到的key,则直接生成真实dom渲染页面
3 用户input输入的内容是和key绑定的

用index做key存在的问题
1 效率低:如果数组是逆序添加删除,则真实dom每次都会全部重新生成
2 界面有问题:如果逆序添加删除,且结构里有输入类dom,则会产生错误的dom更新,用户输入的input跟随key显示在错误的条目上
3 不存在逆序添加删除或者后端返回数据是中间插入一条,则使用index作为key是没有问题的

因此,有输入框或按钮时,永远不要用index做key
脚手架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
1 安装
npm i create-react-app -g
yarn eject (webpack.config.js被隐藏了,这个可以显示出来)

2 了解public目录
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> 设置图标
<meta name="theme-color" content="#000000" /> 地址栏颜色,仅支持安卓手机
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> 把网页添加到主屏幕的图标,仅支持苹果手机
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> App应用加壳配置,类似安卓开发的配置文件
<noscript>You Cannot js</noscript> 提示不能运行js的页面
robots.txt 爬虫规则配置

3 了解src目录
index.js中 <React.StrictMode>用来检查代码错误
reportWebVitals 用来测试网页性能
核心文件:index.js webpack入口,index.html根节点root, App.js根组件

4 手写根组件
import React from 'react'
class App extends React.Component{}
或者
import React,{Component} from 'react'
class App extends Component{}

import React,{Component} from 'react' 这里是分别引入,不是解构,说明react文件里进行了分别暴露,如下所示:
const React={}
export class Component{} 分别暴露
React.Component = Component 把这个往React多挂了一份,这样就不用引入{Component},只引入React,再用React.Component
export defalt React 统一暴露

5 样式模块化
css文件名 index.css改成index.module.css
引入 import './index.css'改成import hello from './index.module.css'
使用 title样式用hello.title

或者用scss在外层包一层 .hello{.title{}}

6 安装插件
快捷写法 rcc类组件 rfc函数组件

7 组件化编程
拆分组件——实现静态组件——实现动态组件(动态数据、实现交互)
案例TodoList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
header = input输入后按回车,会添加一条
list-item = checkbox+任务名称+删除按钮,checbox勾选就是已完成,鼠标悬浮才显示删除
footer = chebox全选+已完成数/总任务数+按钮(清除所有已完成)

1 案例中的header-input没有用state,只拿值e.taget.value,清空时e.taget.value=''
2 监听回车 onKeyUp={} if(event.keyCode==13){}
3 uuid可以生成唯一id,yarn add uuid或者nanoid这个轻量一点
import{nanoid}form 'nanoid' const id =nanoid()
或import {v4 as uuid} from 'uuid' const id =uuid()
4 checkbox用defaultChecked有bug,只在第一次有效,后边修改不会变
5 鼠标移入高亮和显示删除 在list的每个item里都分别存state:{mouse} onMouseEnter改为true,onMouseLeave改为false
6 import PropTypes from 'prop-types' 限制item的数据和处理checkbox的方法必传
7 删除数组中指定id的一项 const newArr = arr.filter(item=>{return item.id !== id }) 相等时返回false就被过滤掉了
8 确定删除吗 if(window.confirm('确定删除吗')){}
9 数组方法reduce 可以做数据统计和条件求和
统计有几个arr.reduce((pre,current)=>{if(current.xx==xx) return pre+1 },0) 第一次pre值为后边的初始值,以后每次都是上一次返回值
条件求和arr.reduce((pre,current)=>{if(current.xx==xx) return pre+current.num },0)

10 连续解构const {a:{b}} =data ,连续解构+重命名const {a:{b:value}} =data
配置代理
1
2
3
4
5
6
7
8
9
10
11
//写一个接口app.js  npm i express -s 
const express = require("express");
const app = express();
app.get("/students", (req, res) => {
const students = [{id: "001",name: "张三",},{id: "002",name: "李四",}];
res.send(students)
});
app.listen(5000,err=>{
if(!err)console.log('服务器启动成功')
})
//启动服务 node app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
第一种
package.json中添加"proxy":"http://localhost:5000"
接口请求3000是react项目端口
import axios from 'axios'
axios.get('http://localhost:3000/students').then(res=>{console.log(res.data)},err=>{})

第二种,多个代理
在src目录创建setupProxy.js,里边用commonjs语法,react会读取这个文件添加到webpack

低版本const proxy = require('http-proxy-middleware') //react自带的库
高版本const { createProxyMiddleware: proxy } = require('http-proxy-middleware');
module.exports = function(app){
app.use(
proxy('/api1',{ //遇见/api1前缀的请求,就会触发该代理配置
target:'http://localhost:5000', //请求转发给谁
changeOrigin:true,//控制服务器收到的请求头中Host的值,服务器可以get('Host)限制请求,
//当为true时,服务器得到的host是5000,false时服务器得到的host是3000
pathRewrite:{'^/api1':''} //重写请求路径(必须)
}),
proxy('/api2',{
target:'http://localhost:5001',
changeOrigin:true,
pathRewrite:{'^/api2':''}
}),
)
}
Pubsub
1
2
3
4
5
6
yarn add pubsub-js 消息订阅与发布
import PubSub from 'pubsub-js'
const token = PubSub.subscribe('hello',(msg,data)=>{console.log(msg,data)}) //接收方订阅,接收2个参数
PubSub.publish('hello','helloworld') //发送方发布,这里的hello和helloworld对应接收方的msg和data

PubSub.unsubscribe(token) //取消订阅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//一些库的使用
import React from 'react'
import {v4 as uuid} from 'uuid'
import PropTypes from 'prop-types'
import axios from 'axios'
import PubSub from 'pubsub-js'

function Hello() {
const id =uuid()
axios.get('http://localhost:3000/api1/students').then(res=>{console.log(res.data)},err=>{})
const token = PubSub.subscribe('hello',(msg,data)=>{console.log(msg,data)})
PubSub.publish('hello','helloworld')
PubSub.unsubscribe(token)
return (
<div>111</div>
)
}
Hello.propTypes={name:PropTypes.string}
export default Hello
Fetch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
axios.get(url).then( res=>{ },err=>{ } ) 走res就表示拿到数据了

fetch(url).then(res=>{},err=>{} )把请求接口分成了【请求】+【响应】两件事,走res只表示请求到了服务器,至于有没有返回数据要进一步判断
这里res.json是一个promise,里边包含了返回数据

1 完整写法
fetch(url)
.then( res1=>{return res1.json()},err1=>{return new Promise(()=>{})} ) //请求成功和请求失败
.then( res2=>{这里拿数据},err2=>{} ) //返回成功和返回失败

当走err1,如果什么都不写,相当于返回了undefined,是非promise值,那么会当成返回值走res2,返回值为undefined
为了在请求失败时,不再判断返回状态,给他return一个初始的promise

2 优化版:省略err,最后统一捕获
fetch(url,config) //config是一个配置对象
.then(res1=>{return res1.json})
.then(res2=>{拿数据})
.catch(err=>{})

3 继续优化版
Promise2种用法 Promise.then(res=>{}) 和const res=await Promise 这里2个res是一样的
try{
const res= await fetch(url) 请求的结果
const data = await res.json() 响应的结果
}catch(err){ }
注意外边包async const getData = async()=>{ }

路由P75-93

1
2
3
4
5
6
7
8
9
10
11
原理:点击导航——改变路径——监测地址栏——匹配对应的组件
路由就是映射关系key-value,key是路径,value是组件或function
分类:前端路由是组件,后端路由是接口

实现原理
history.push(path) 向历史栈推进路径,但阻止页面跳转
history.listen(location) 监听路径变化,匹配组件

路由模式
history 直接使用H5推出的history的api
hash 使用锚点,可以改变路径并留下历史栈,但页面不刷新
Router5路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
react-router包含3种实现,用于web、native和core,我们学web版
npm i react-router-dom@5

1 基本使用
import { Link, BrowserRouter, Route } from "react-router-dom";
// 写路由链接
<BrowserRouter>
<Link to="/about">关于</Link>
<Link to="/home">首页</Link>
</BrowserRouter>
//注册路由,写在显示组件的区域
<BrowserRouter>
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
</BrowserRouter>

BrowserRouter分开包,是不同的路由器,为了用同一个路由器管理,把它包在根组件App上

2 路由组件
一般组件,通过props自定义传参
路由组件,接收3个固定属性history、location、match

3 NavLink
<NavLink activeClassName="active" to="/about">关于</NavLink> 比Link多一个属性activeClassName,可以设置高亮效果,如果类名是active,可以省略掉

4 标签体内容是一个特殊的标签属性children

封装NavLink <MyNavLink to="/about">关于</MyNavLink> 标签体里的'关于',会以props.children属性传给组件
定义MyNavLink组件 <NavLink {...this.props} /> children值会显示到标签体里

5 Switch只匹配第一个
<Switch>
<Route path="/home" component={About} />
<Route path="/home" component={Home} />
</Switch>
加上switch后,匹配到第一个,后边的就不去匹配了

6 样式丢失问题
index.html中引用了样式'./common.css' 相对路径
<Route path="/abc/about" component={About} /> 使用了层级的路径

进入首页,能正常拿到common.css,样式没问题,
点击About进入/abc/about页面后,继续沿用刚才的common.css,样式也没问题
再点刷新,会重新获取样式文件,这时请求的是localhost:3000/abc/common.css,路径错误,拿不到css (拿不到资源时会把index.html返回给你,因此虽然看到请求css结果为200成功,但拿到的不是css)

解决方法
index.html中路径写成'/common.css'根路径 或 '%PUBLIC_URL%/common.css'绝对路径
或用hash路由,请求localhost:3000/#/abc/common.css它会忽略#后边的内容

7 模糊匹配和精准匹配
默认是模糊匹配,跳转/home/abc 也能显示 Home组件
<Link to="/home/abc">首页</Link>
<Route path="/home" component={Home} />

严格匹配,添加exact={true} (最好不要开启,有2级路由时会出问题)
<Route exact={true} path="/home" component={Home} />


8 Redirect写在最底下,当都没匹配时跳转该页面
<Route path="/home" component={Home} />
<Redirect to='/'/>

9 嵌套路由
<Link to="/home/abc">首页</Link>
<Route path="/home/abc" component={ABC} /> 它是Home的子组件

如果开启严格匹配<Route exact={true} path="/home" component={Home} />
'/home/abc'和父组件Home匹配不上,Home都不会显示,子组件/home/abc更不会显示了
(路由是按顺序依次来匹配的,先匹配一级组件,再匹配二级组件)

10 路由传参
传递params参数 <Link to={`/home/${id}/${name}`} children="首页" />
声明接收 <Route path="/home/:id/:name" component={Home} />
取值 Home组件的props里会接收到params参数, props.match.params = {id:xxx,name:xxx}

传递search参数 <Link to={`/home/?id=01&name=张三`} children="首页" />
无需声明接收 <Route path="/home" component={Home} />
取值 props.location.search = '?id=01&name=张三',需要自己处理

react自带qs库(2024已弃用),可解码(?id=01&name=张三 是urlencoded编码)
import qs from 'querystring'
qs.parse( props.location.search.substring(1)) )


传递state参数,无需声明接收 (这个不是状态state),注意用的pathname不是path
<Link to={ {pathname:'/home',state:{id:10,name:'张三'} }} children="首页" />
<Route path="/home" component={Home} />
取值 props.location.search = {id:10,name:'张三'}
地址栏不带参数,刷新不会丢失参数,因为location存在props.hitstory里,history是浏览器历史栈

11 跳转模式
默认是push,开启replace
<Link replace={true} to={`/home`} children="首页" />
简写<Link replace to={`/home`} children="首页" />

12 编程式路由
<Route path="/home/:id" component={Home} />这个相当于Router-View,永远不能省的

this.props.history.replace(url)
this.props.history.push(url)
这种形式可以传params和search参数

this.props.history.push(url,state)
这样可以传state参数,state直接写对象即可

13 前进后退
history.goBack()
history.goForward()
history.go(n) n=1前进,n=-1后退

14 非路由组件跳转页面
class Hello extends Component{ }
export default WithRouter(Hello)
它加工一般组件,让它获得路由组件才有的3个属性:props.history,location,match

15 路由模式
BrowserRouter使用H5的History APi,不带#
HashRouter使用URL的哈希值,类似锚点,路径带#

哈希路由刷新页面会丢失state参数,但哈希路由可以解决一些路径错误问题,比如上边的样式丢失

state是存到history里的,哈希路由没有用history API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
关于组件不能用编程式路由的问题
const Home = (props) => {
console.log(props)
return <div>我是Home</div>;
};

export default class Hello extends Component {
toHome=()=>{
console.log(111,this) //打印发现这里的this是类组件没问题,但this身上的props里没有history,因此没办法使用编程式路由
this.props.history.push(url,state)//无法跳转
}
render() {
return (
<div>
<Link to={ {pathname:'/home',state:{id:10,name:'张三'} }} children="首页" />
<Route path="/home" component={Home} />
<button onClick={this.toHome}>点击</button>
</div>
);
}
}
// 原因是:当前组件没有被Route包裹,没有被Route包裹的组件不是路由组件,props里没有history、location、match3大属性
antd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
注意antd已经做了更好的封装,引入Button自带样式了,不需要下边这些操作了

1 修改配置
yarn add react-app-rewired customize-cra 这个库可以修改配置
根目录创建配置文件 config-overrides.js
module.exports = function override(config,env){//接收的参数是原有配置
return config
}
加载配置,通过修改启动命令
'start':'react-app-rewired start'

2 按需引入样式
yarn add babel-plugun-import 这个库是按需引入样式的库
修改配置文件config-overrides.js
const { override, fixBabelImports,addLessLoader} = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
lessOptions:{
javascriptEnabled: true,
modifyVars: { '@primary-color': 'green' },
}
}),
);

3 自定义主题
yarn add less less-loader
修改配置,见上边addLessLoader
redux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
redux用于状态管理,不是react官方的库,在vue\ng里也能用

原理
组件——>action创建者——dispatch(action)分发action对象——>store调度者——>reducers处理数据(接收preState和action,返回newState)——>store

组件——getState取值——>store

action对象包含2个属性type必传和data可选
reducer用于初始化和加工数据
store将state、action、reducer联系到一起

字符串*1可以转数字
()=>({name:'aaa'}) 返回对象要加()

yarn add redux 核心库,vue,react,ng都能用
精简版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
1 创建store
import { createStore } from "redux";
export default createStore(countReducer);

2 创建reducer
const countReducer = (preState = 0, action) => {//形参的默认值
let { type, data } = action;
switch (type) {
case "increment":
return preState + data;
case "decrement":
return preState - data;
default:
return preState;
}
};

3 取值、改值和更新页面
export default class Hello extends React.Component {
componentDidMount(){
store.subscribe(()=>{
this.setState({}) //只要store改变,就调render()更新,redux只管理状态,页面更新要自己实现
})
}
render() {
return (
<div>
{store.getState()} //取值
<button onClick={()=>store.dispatch({type:'increment',data:1})} children='增加'/> //分发action对象
</div>
);
}
}

在index.js中,可以省去在每个组件里都订阅store
store.subscribe(()=>{
ReactDOM.render(<App/>,root)
})

4 创建cerateAction.js和异步action
action是对象-同步,action是函数-异步(函数可以开启异步任务)

export const increAction = data=>({type:'incre',data:data}) //同步的

export const increAsyncAction = data = >{
return dispatch=>{ //返回一个异步func给store调用,因此可以把dispatch传过来
setTimeout(()=>dispatch(createIncreAction(data)),1000 ) //异步里一般会调用同步的
}
}

redux只认对象,如果要处理函数action,store.js还需要引入中间件 yarn add redux-thunk
import thunk from 'redux-thunk'
export default createStore(countReducer,applyMiddleware(thunk));

异步action不是必须的,也可以在组件里请求完再调同步action
react-redux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
react官方出的方便使用redux yarn add react-redux

1 原理
容器组件和UI组件是父子关系
容器组件可以随意使用redux的api,UI组件不能用
容器可以通过props传 redux的状态和操作方法

容器组件不是自己创建的,通过connect方法生成,里边包含很多api

2 使用方法
创建容器组件
import CountUI from "./CountUI"; //这是自己创建的组件
import { connect } from "react-redux";
const CountContainer = connect()(CountUI)
export default CountContainer

使用容器组件
<CountContainer store={store}/>

3 给UI组件传状态和方法
由于没有直接用<CountUI />,没有办法写标签接收props参数
只能通过容器组件传递给UI组件

connect()接收2个参数,必须是function,分别用来传状态和方法,当容器组件调用2个方法时,返回状态对象和方法对象,
当connect(a,b)(CountUI)第2个()调用时就能把 状态对象和方法对象 传给CountUI

const a= state=> { return {count:state} } 由于容器组件有store,它调用a时是可以给a方法传state的
const b = dispatch =>{return {add:()=>dispatch() } }

a,b名写标准点 a叫mapStateToProps ,b叫mapDispatchToProps
状态对象和方法对象的key-value就是UI组件接收的props里的key-value

总结:创建store.js就可以直接用redux了,为把同步异步全都整理到一起,创建了cerateAction.js,为了用react-redux规范写,创建了CountUI.jsx和CountContainer.jsx,最后使用组件<CountContainer store={store}/>
优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1 mapDispatchToProps可以写成对象{add:返回action对象的函数},react-redux拿到action对象会自动分发

创建容器简写后
import { increAction, increAsyncAction } from "./createAction";
export default connect( state=>({count:state}), {add:increAction,asyncAdd:increAsyncAction})(CountUI)

如果省略createAction.js,可以写成
export default connect( state=>({count:state}), {add:data=>({type:'incre',data:data})(CountUI)

2 react-redux使用connect生成的容器组件自带检测store变化更新页面的能力,不再需要自己订阅store更新页面

3 使用Provider给根组件传store,它会分析所有需要store的容器组件并传过去,不再需要自己给容器组件传store
import {Provider} from 'react-redux'
ReactDOM.render(<Provider store={store}>
<App/>
</Provider>,root)

4 整合UI组件和容器组件,写到一个jsx里

项目文件规范 redux文件夹里store.js + actions文件夹 + reducers文件夹
多个reducer
1
2
3
4
5
6
7
8
9
10
11
12
多个reducer的数据共享
// store.js,combinReducers的参数是合并后的总状态对象
const allReducer = combinReducers({
counter:countReducer,
person:personReducer
})
// 容器组件
export default connect( state=>({count:state.counter}), {add:increAction})(CountUI)

一个reducer存多个值,,只需要preState ={count:0,arr:[] }
容器组件可以分开拿,也可以整体拿state.counter
export default connect( state=>({count:state.counter.count,arr::state.counter.arr}), {add:increAction})(CountUI)
reducer纯函数
1
2
3
4
5
6
7
8
9
10
11
12
13
给数组添加,redux只比较地址值是否改变
用 return preState.push(person) 即使数据变了,但还是同一个数组,redux不会改变状态
要创建一个新的数组 return [...preState,person]

reducer必须是纯函数
我有一个程序,输入1,它返回10,第二次输入1,它还是返回10
同样的输入,必定得到同样的输出
约束:
不得改写参数数据
不会产生副作用如发网络请求,
不会调用不纯的方法如Date.now()这个每次都不一样

上边第一种改了参数,就不会有相同输出了,而第二种传同一个地址值的preState结果是一样的,(虽然也改了state,但实际2次调用传的是不同地址值的对象)
Redux DevTools
1
2
3
4
5
6
7
浏览器装拓展 Redux DevTools
安装库 yarn add redux-devtools-extension
store.js引入
import {composeWithDevTools} from 'redux-devtools-extension'
createStore(countReducer,composeWithDevTools())

createStore(countReducer,composeWithDevTools(applyMiddleware(thunk))) //有异步时用这个
最终版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1 action对象的type单独在一个文件里定义常量
2 action方法单独写在action/xxx.js文件里
3 整合多个reducer单独写到一个reducers/index.js里,store.js只引入一个allReducer(用reducers这个名更好)
4 尽量触发对象简写形式 a:a可以写成a
combinReducers({
counter:countReducer,
person:personReducer
})
用同名可以写成combinReducers({counter,person})

export default connect( state=>({count:state}), {add:increAction,addAsync:increAsyncAction})(CountUI)
也可以改成同名简写
export default connect( state=>({count:state}), {add,addAsync})(CountUI)

5 打包运行
npm i serve -g
在静态文件目录下cmd执行 serve,可以快速启动一个服务器
或 npm i serve -s build + serve 目录(build)
Router6路由
1
2
3
4
5
6
7
8
9
以3个包发布的 react-router路由核心库,
react-router-dom包含前者全部,并添加操作dom的组件
react-router-native包含第一个全部,并添加native的api

变化:
Switch -> Routes,
compoent={} -> element={}
新增hokks:useParams\userNavagate\useMatch
推荐函数式组件

组件变化

1
2
3
4
5
6
7
8
BrowserRouter\HashRouter\Link\NavLink不变
1 Routes组件 <Switch/>换成<Routes/>,不能省略了

2 Route组件 component={About} 换成 element={<About/>},注意</>,caseSensitive设置区分大小写
Route可以嵌套使用,且可配置useRoutes()配置路由表,但需通过<Outlet/>组件渲染子路由

3 Navigate组件 <Redirect to='/about'/>换成使用<Navigate/>,它还有repalce或push方式,只要这个组件渲染就会跳转

1
2
3
4
5
6
7
<NavLink to='/about' children='关于'/>
<NavLink to='/home' children='首页' end/> 子级高亮时让父级不再高亮
<Routes> Switch换成Routes*
<Route path='/about' element={<About/>} />换成element
<Route path='/HOME' caseSensitive element={<Home/>} />区分大小写
<Route path='/' element={<Navigate to='/about' replace/>} />取代Redirect + replace
</Routes>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
4 路由表
上边<Routes>里的这些可以全部省略,写成
const element = useRoutes([
{path:'/about',element:(<About/>)},
{path:'/',element:(<Navigate to='/about' replace/>)},
])
在需要路由组件的位置使用{element},在App.jsx注册组件用一次即可,相当于vue中的router-view,
子级路由可以用<Outlet>可以不用再 引入useRoutes+{element}

项目规范:把路由表写到单独的js,组件里引入const element =useRoutes(routes) 再使用{element}

5 嵌套路由
{path:'/about',element:(<About/>),children:[
{path:'son',element:(<Son/>)},
]}

子级path的写法:'/about/son' 或'son'不带斜杠 或'./son' (NavLink里to同样)

NavLink有个end属性,当子级高亮时让父级不再高亮
NavaLink里activeClassName属性废弃不能用了,需要自己写判断className={isTure ? 'aaa' :'bbb'}

6 路由传参
<NavLink to='/home/001/张三' children='首页'/>
{path:'/home/:id/:name',element:(<Home/>)}
params传参不变,路由表里path还是要声明接收,取值使用useParams或useMatch
const {id,name}= useParams()
或const { id,name } =useMatch('/home/:id/:name').params 要传路径,不推荐用这个

<NavLink to='/home/?id=001&name=张三' children='首页'/>
search传参不变,取值使用useSearchParams()和useLocation(),但结果是数组,数组里search,setSearch都是函数
const [search,setSearch]= useSearchParams()
const id= search.get('id') 拿值
onClick={()=>setSearch('id=002&name=李四')} 改值
或const search=useLocation().search 这里是'?id=002&name=李四' 需要用qs解码,比较麻烦

旧版state传参 <NavLink to='{{ pathname:'/home',state:{id:01} }}' children='首页'/>
新版state传参 <NavLink to='/home' state={{id:01}}/>
取值使用useLocation()
const {state} = useLocation() 拿到state是{id:01}

7 编程式路由
旧版使用路由组件的props.history实现
新版使用useNavigate(),废弃了withRouter
const navigate = useNavigate()
navigate('/home')
navigate('/home',{ state:{id:01},replace:false }) params和search参数直接写到url里
navigate(-1) 前进后退

8 Hook-不常用
useInRouterCnntext() 返回布尔值,是否处于路由器包裹中,不区分是否路由组件,(用于第三方库,想知道是否在路由器里)
useNavigationType() 返回3种类型之一:pop刷新页面、push、replace
const result = useOutlet() 用来在/home路径下把它的子路由/home/son组件显示出来,如果还没挂载子路由 result=null,如果挂载了,result=子路由对象
useResolvedPath() 给定一个任意URL值,解析其中的path、search、hash值

总结
组件:BrowserRouter\HashRouter、Routes\Route、Link\NavLink、Navigate、Outlet
Hook:useRoutes\useNavigate、useParams\useSearchParams\useLocation\useMatch、4个不常用的
setState
1
2
3
4
5
6
7
8
9
10
11
12
对象式
this.setState({count:9})
console.log(this.state)
通知react修改是同步的,react后续修改是异步的,打印结果state没变

this.setState({count:9},()=>{ console.log(this.state)} )
第二个参数可选-回调函数在render()之后执行,因此能拿到更新后的state

函数式
第一个参数不仅可以是对象,还可以是一个回调函数updater,updater可以接收state和props,需要返回一个同上的对象
this.setState((state,props)=>{ return {count:9} })
好处是 1 可以直接拿到state this.setState(state=>({count:state.count+1}) 2
lazyload
1
2
3
4
5
6
7
8
9
10
11
12
打开页面时,所有的路由组件都会被加载,只是不显示

路由懒加载
1 把import Home from './Home'改成
const Home = lazy(()=>import('./Home'))

2 要在注册路由的位置包裹Suspense,指定当网络很差时,组件位置显示什么内容
<Suspense fallback={<div>Loading</div>}>
<Route to="/home" element={</Home/>} />
</Suspense>
3 fallback也可以是组件,但不能懒加载这个组件
?备注:路由表里好像可以配置每个组件的懒加载样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//routes/js
import { lazy } from "react";
const Home = lazy(()=>import("./Home"));
const routes = [{path: "/home",element: <Home />,},];
export default routes

//App.js
import React, {Suspense}from "react";
import routes from "./routes";
import { useRoutes,Link} from "react-router-dom";
export default function App() {
const element = useRoutes(routes);
return (
<div>
<Link to="/home">首页</Link>
<Suspense fallback={<div>Loading</div>}>{element}</Suspense>
</div>
);
}
useState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在函数组件里用state
export default function About() {
const [count,setCount] =useState(9)
const add=()=>{
setCount(count+1)
setCount(preval=>preval+1)
}
return (
<div>
<div>{count}</div>
<button onClick={add}>点击</button>
</div>
)
}
.1 const [count,setCount] =useState(9)中,count是state里的参数,9是初始值,setCount是更新状态的方法,等同于setState
.2 每次render更新,About函数都会重新运行,const [count,setCount] =useState(9)会重复运行,但react底层做了缓存,使它不会被覆盖
.3 setCount有2种写法:传值或传函数
setCount(newVal)
setCount(count => newVal)该函数接收原来的count,返回一个newVal
useEffect
1
2
3
4
5
6
在函数组件里用生命周期
主要是用来监测数据变化的,接收2个参数,第1个是回调函数,第2个是要监测对象的数组(可选),当监测的值变了就会执行回调函数
useEffect( ()=>{更新} ) 第2个参数不传,表示监测所有,挂载和更新时都会触发回调
useEffect( ()=>{挂载},[] ) 第2个参数传空数组,表示谁都不监测,只在挂载时触发回调
useEffect( ()=>{watch},[count]) 第2个参数传某个参数,在这个参数变化时触发回调,可以实现vue中watch
useEffect( ()=>{ return ()=>{卸载} },[] ) 第1个参数可以返回一个回调函数,这个函数会在组件卸载前执行
useRef
1
2
3
在函数组件里用ref获取标签
const myRef= useRef() 可以创建ref容器,使用方法和类组件的createRef类似
(这里边有很多问题,父子传参、暴露指定参数、实时更新页面等,参见上边网上查阅的ref函数写法)
Fragment
1
2
3
碎片标签
jsx必须有一个唯一标签,为了层级太多不写div,可以写Fragment 或 空标签 <>,它们编译后会被丢弃
<Fragment key={1} > </Fragment>,Fragement还可以接收key参数,当用到遍历时使用
Context传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
和任意后代组件通信,一般用于封装react插件

创建Context容器,(A--B--C依次是父子关系)
A使用子组件B时,包裹一层并传value(必须叫value,不能改名)
const MyContext =React.createContext()
<MyContext.Provider value={data}>
<B/>
</MyContext>

取值方法1-仅适用类组件
后代都能取值,但必须声明才能用
static contextType= MyContext
this.context取值

取值方法2-都可以用
在显示内容的区域D包裹一层Consumer,这个标签里写函数,它接收参数value
<MyContext.Consumer>
{ value=>{return D} }
</MyContext.Consumer>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { Component } from "react";
const MyContext = React.createContext();

export default function AAA() {
return (
<div>
<MyContext.Provider value={{ id: "01" }}>
<BBB />
</MyContext.Provider>
</div>
);
}
class BBB extends Component {
static contextType = MyContext;
render() {
return (
<>
<CCC />
{this.context.id}
</>
);
}
}
const CCC = () => {
return (
<MyContext.Consumer>
{(value) => {
return <div>{value.id}</div>;
}}
</MyContext.Consumer>
);
};
PureComponent
1
2
3
4
5
6
7
8
9
10
11
12
Component存在2个问题
1 setState({}) 传空对象也会render()
2 父组件render(),子组件也会重新render(),即使子组件没用到父组件任何参数
高效做法:只有state和props变化才重新render()

shouldComponentUpdate(nextProps,nextState){
if(nextState.id==this.state.id) return false
}

使用PureComponent代替Component实现了这些优化,但只做浅比较,如果还是同一个对象,只是内部数据改变,返回false页面不更新,因此不要直接修改state的值
this.state.id=99 //直接修改,对象地址不变,应该创新新数据 data= {...this.state,{id:99}}
this.setState(state)
renderProps
1
2
3
4
5
6
7
8
9
10
11
12
children形式props
<A> <B/> </A>
B组件写到A组件标签体里,不会直接显示,会传到A的props.chilren里
在A中需要展示B的地方 {this.props.children}

类似组件插槽,A是B的父组件,但怎么实现A给B传参,如下:

render形式props
<A render={(data) => <B data={data} /> } />
在A中需要展示B的地方 {this.props.render(data)}

使用A时给它传一个render回调函数,(该函数接收data ,返回B组件),render名字可以自己起,但这样写大家都懂
ErrorBoundary
1
2
3
4
5
6
7
8
9
10
11
12
错误边界:一个子组件出问题,不要让整个页面都崩掉,在父组件中捕获错误

export default class Home extends Component {
state={hasError:''}
static getDerivedStateFromError(error){ //子组件出错就会触发钩子,只能捕获后代组件生命周期里的错误,不能捕获自己或者定时器等
return {hasError:error}
}
componentDidCatch(err,info){} //这个钩子用于统计页面错误,发送给后台
render() {
return <>{this.hasError ? 'Child出错' :<Child />}</> //出错时渲染备用组件
}
}
组件通信方式
1
2
3
4
5
1 props:children形式props常规 和 render形式props插槽
2 消息订阅与发布:pubsub
3 集中式状态管理:redux
4 生产者消费者模式:conText
props父子 pubsub兄弟 conText祖孙