初识 React

是什么

用于构建用户界面的js库,是一个将数据渲染为html视图的开源js库,由facebook开发的

为什么要学习react

  • 原生js操作dom繁琐,效率低,

  • 使用js直接操作dom,浏览器会进行大量的重绘重排

  • 原生js没有组件化编码方案,代码复用率低

特点

  • 采用组件化模式、声明式编码,提高开发效率及组件复用率。
  • React Native中可以使用React语法进行移动端开发
  • 使用虚拟DOM+优秀的Diffing 算法,实现dom的复用,尽量减少与真实DOM的交互。

hello_react

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello_react</title>
</head>
<body>
<!-- 准备好一个“容器” -->
<div id="test"></div>

<!-- 引入react核心库 -->
<script type="text/javascript" src="./js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="./js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="./js/babel.min.js"></script>

<script type="text/babel">
/* 此处一定要写babel */
// 1. 创建虚拟DOM
const VDOM = <h1>Hello, React</h1>; /* 此处一定不要写引号,因为不是字符串 ,我们写的是jsx语法 */

// 2. 渲染虚拟DOM 到页面
ReactDOM.render(VDOM, document.getElementById('test'));
</script>
</body>
</html>
  • 上述三个js文件一定要按顺序引入
  • 我们引入react.development.js后,在全局就会出现React对象,引入react-dom.development.js后,在全局就会出现ReactDOM对象
  • script标签的类型一定要是是text/babel,因为我们写的是jsx代码,然后借助**浏览器的babel**进行代码转换

jsx

是什么

jsx是javascriptxml的缩写,xml早期用于存储和传输数据。现在已经被json替代

1
2
3
4
<student>
<name>Tom</name>
<age>19</age>
</student>

为什么在react中使用jsx不使用js?

创建虚拟dom

两种语言创建虚拟dom的语法不同

  • jsx

    1
    const VDOM = <h1 id="title"><span>Hello,React</span></h1>
  • js

    1
    const VDOM = React.createElement('h1',{id:'title'},React.createElement('span',{},'Hello,React'))

显然,使用jsx创建虚拟dom更简单。

虚拟dom是什么

我们知道使用jsx创建虚拟dom更简单,那虚拟dom是什么呢?

虚拟dom本质就是一个js对象,是对真实dom的高度抽象,我们可以通过输出虚拟dom和真实dom来比较分析它们的区别

1
2
3
4
5
6
7
<script type="text/babel">
/* 此处一定要写babel */
const VDOM = <h1>Hello, React</h1>; /* 此处一定不要写引号,因为不是字符串 ,我们写的是jsx语法 */
console.log(VDOM)
const DOM = document.getElementById('test')
console.dir(DOM)//不使用log,因为输出的信息太少
</script>

我们可以观察到,虚拟dom和真实dom都是对象,但是真实dom身上的属性,比虚拟dom上的属性多得多。

jsx语法规则

  • 定义虚拟dom的时候,不要写引号
  • 在标签中混入js表达式的时候要用{},一定注意区分:js语句js表达式
    • 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,下面这些都是表达式:
      • a
      • a+b
      • demo(1),函数调用,值是函数的返回值
      • arr.map(),函数调用,值是函数的返回值
      • function test(){},函数定义
    • 语句(代码):下面这些都是语句(代码):
      • if(){}
      • for(){}
      • switch(){case:xxxx}
  • 样式的类名不要用class,而要用className,为了避开es6的class关键字
  • 内联样式要用style={{key:value}}的形式书写
  • 标签必须闭合
  • 只有一个根标签,类似vue2
  • 标签首字母如果是小写的,则将该标签转换为html中的同名标签,如果html没有对应的标签则报错
  • 标签首字母如果是大写的,react就去渲染对应的组件,若该组件没有定义则报错。
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
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.title{
background-color:'red'
}
</style>
</head>
<body></body>
<script type="text/babel>
const myId ='aTgUiGu'
const myData= 'HeLlo,rEact'
//1.创建虚拟DOM
const VDOM =(
<div>
<h2 className="title" id={myId.toLowercase()}>
//最外面的大括号表示{color:'white',fontsize:'29px'}是一个js表达式
<span style={{color:'white',fontsize:'29px'}}>{myData.toLowerCase()}</span>
</h2>
<h2 className="title" id={myId.toUppercase()}>
<span style={{color:'white',fontsize:'29px'}}>{myData.toLowerCase()}</span>
</h2>
//标签必须闭合,包括单标签
<input type="text"/>
<Good>123</Good>
</div>
//2.渲染虚拟DOM到页面
ReactDoM.render(VDoM,document.getElementById('test'))
</script>
</html>

组件与模块化

模块化

将复杂的js文件拆分成一个一个js文件,每个js文件就是一个模块

组件化

组件是能实现局部功能的代码和资源的集合(html,css,js,images…)

组件化

函数式组件

1
2
3
4
5
6
7
8
9
10
11
12
<script type="text/babel">
//1.创建函数式组件
function MyComponent(props){
console.log(this);//此处的this是undefined,因为babel编译后开启了严格模式
return <h2>我是用函数定义的组件(适用于简单组件的定义)</h2>
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent/>,document.getElementById('test))
//执行了ReactDom.render(<Mycomponent/>.......之后,发生了什么?
//1.React解析组件标签,找到了MyComponent组件。
//2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中
</script>

要注意的点包括:

  • 函数名必须大写,函数名就是组件名,要符合组件名的规范,首字符必须大写
  • 函数必须有返回值,返回一个虚拟dom
  • ReactDOM.render的第一个参数必须是组件标签,而不是组件名。
  • 函数式组件可以接收传入的props,因为函数可以传参

类式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/babel">
//1. 创建类式组件
class MyComponent extends React.Component {
render() {
// render是放在哪里的? — MyComponent的原型对象上,供实例使用。
// render中的this是谁? — MyComponent的实例对象=MyComponent组件实例对象。
console.log('render中的this:', this);
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>;
}
}

//2. 渲染组件到页面
ReactDOM.render(<MyComponent/>, document.getElementById('test'));
/*执行了 ReactDOM.render(<MyComponent/>.....之后发生了什么?
1.react解析组件标签,找到了MyComponent组件
2.发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法
3.将render返回的虚拟dom转换成真实dom,随后呈现在页面中
*/
</script>

要注意的点包括:

  • 如果一个类想要成为类式组件,必须继承React.Component
  • 这个类必须实现render方法,且这个方法必须有返回值

组件实例的3大属性

组件实例上的所有属性,都是React.Component类的构造函数初始化的,因为我们定义的类并没有构造器。然而即便我们不书写构造器,也会默认添加一个构造器:

1
2
3
4
5
6
7
class Child extends React.Component {} 
// 等价于
class Child extends React.Component {
constructor(...args) {
super(...args); //自动调用父类构造函数,父类构造函数中的this指向Child组件实例,所以能起到初始化组件实例的作用
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Parent {
constructor(name, age) {
this.name = name
this.age = age
}
}
//情况1
//这种情况完美的验证了即便我们不书写构造函数,父类构造器也会帮我们初始化实例
class Child extends Parent {
// 未定义构造函数
}
//情况2
class Child extends Parent {
constructor(...args) {
super(...args)
}
}

const child = new Child('tom', 21);
console.log(child);//无论是何种情况,都输出 Child {name: 'tom', age: 21}

要注意的是,使用super关键字调用父类构造函数,和直接调用父类构造函数的很重要的区别在于,使用super关键字调用父类构造函数,构造函数的this指向子类实例。

state

可以看到上面实例的state属性是null,如果我们想要修改这个属性,就必须在自定义的组件(类)中添加构造函数。

初始案例

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
<script type="text/babel">
//1. 创建组件
class Weather extends React.Component {
//手动添加构造函数
constructor(props) {
//任何有关this的操作不能写在super前面
super(props)//调用React.Component(父类)的构造函数初始化子类实例
this.state = { isHot: false,wind:'微风'};
//根据原型链上的changeWeather,创造一个同名方法挂载到实例本身上,确保在回调函数中直接调用这个函数时this指向组件实例
this.changeWeather = this.changeWeather.bind(this)
}
//调用1+n次,第一次调用是因为初次渲染,后续n次调用是因为数据变化。
render() {
const { isHot } = this.state;
return (
//传入一个函数作为回调,但是这个回调函数不是通过this调用的,而是直接调用的
//必须传入的一个函数吗,不能是函数调用吗
<h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{this.state.wind}</h1>
);
}
changeWeather(){
//如果这个方法是被组件实例调用的,那么就能访问到state
//this.state.isHot = !this.state.isHot//不能直接修改state中的数据,视图不会更新
this.setState({isHot:!this.state.isHot})//这是一个合并操作,不是覆盖操作,也就是说,wind属性会保留
}
}

//2. 渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'));
</script>

要注意的包括以下几点:

  • 类中的方法都在局部开启了严格模式
  • render方法是通过组件实例调用的,不过这个组件实例不是我们手动创建的,这个方法也不是我们手动调用的。
  • 在react中onClick不能写成onclick,虽然在js原生语法中就是写做onclick

state的精简

在上述代码中我们为了初始化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
<script type="text/babel">
//1. 创建组件
class Weather extends React.Component {
state = { isHot: false,wind:'微风'};//这样写效果也是一样的,都会被添加到组件实例自身上
//调用1+n次,第一次调用是因为初次渲染,后续n次调用是因为数据变化。
render() {
const { isHot } = this.state;
return (
//传入一个函数作为回调,但是这个回调函数不是通过this调用的,而是直接调用的
//必须传入的一个函数吗,不能是函数调用吗
<h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{this.state.wind}</h1>
);
}
//我们把changeWeather函数写成 a=1 的形式,这样的话,这个方法就不会被挂载到原型对象上了,而是组件实例本身上
//如果我们不写作箭头函数,由于这个函数在点击事件触发后,还是会直接调用,this指向undefined
//但是如果我们写作箭头函数,this的指向就是组件实例
changeWeather = () => {
//this.state.isHot = !this.state.isHot//不能直接修改state中的数据,视图不会更新
this.setState({isHot:!this.state.isHot})//这是一个合并操作,不是覆盖操作,也就是说,wind属性会保留
}
}

//2. 渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'));
</script>

setState

在react中,我们不能直接修改state,否则虽然数据会改变,但是react无法监听到,视图也不会更新,我们必须使用this.setState方法,修改数据并通知视图更新。

props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script type="text/babel">
// 创建组件
class Person extends React.Component {
render() {
const { name, age, sex } = this.props;
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
);
}
}

// 渲染组件到页面
ReactDOM.render(<Person name="jerry" age="19" sex="男"/>, document.getElementById('test1'));
ReactDOM.render(<Person name="tom" age="18" sex="女"/>, document.getElementById('test2'));
ReactDOM.render(<Person name="老刘" age="30" sex="女"/>, document.getElementById('test3'));
</script>

可以看出在react中,也是通过给组件标签添加属性来实现给组件传值的,然后这些属性会被收集到组件实例的props属性中,值为一个对象。而下面的代码则展示了如何快速的给组件传值

1
2
3
let obj = {name:"jerry",age:19,sex:"男"}
//标签内的大括号表示内部是js表达式
ReactDOM.render(<Person {...obj}/>, document.getElementById('test1'));

限制传入组件值的类型

1
ReactDOM.render(<Person name="jerry" age="19" sex="男"/>, document.getElementById('test1'));

通过上述方式传入的name,age等属性值,它们的类型都是字符串,后续如果需要在模板中实现:

1
<li>年龄:{age+1}</li>

的效果,得到的就是191,即字符串拼接。

想要对传入组件的值的类型进行限制,并添加默认值,需要添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//对标签属性进行类型、必要性的限制
//给Person类,添加propTypes属性
Person.propTypes={
name: PropTypes.string.isRequired//限制name必传,且为字符串
sex: PropTypes.string,//限制sex为字符串
age: PropTypes.number,//限制age为数值
speak: PropTypes.func,//限制speak为所数
}
//给Person类添加defaultProps属性
//指定默认标签属性值
Person.defaultProps ={
sex:男',//sex默认值为男
age:18 //age默认值为18
}

其中的PropTypes对象是通过,引入react/prop-types.min.js文件后,出现的全局的对象。

可以看出在react中限制给组件传入的值的类型是非常麻烦的。

其实我们可以把 Person.propTypes = {...},和Person.defaultProps = {...}的操作写在类的内部,从而简化代码。本质都是在类的构造函数上加属性(typeof Class A = 'function'

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
<script type="text/babel">
// 创建组件
class Person extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,//限制name必传,且为字符串
sex: PropTypes.string,//限制sex为字符串
age: PropTypes.number,//限制age为数值
speak: PropTypes.func,//限制speak为所数
}
static defaultProps = {
sex:男',//sex默认值为男
age:18 //age默认值为18
}
render() {
console.log(this);
const { name, age, sex } = this.props;
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
);
}
}
// 渲染组件到页面
ReactDOM.render(<Person name="jerry" age="19" sex="男"/>, document.getElementById('test1'));
ReactDOM.render(<Person name="tom" age="18" sex="女"/>, document.getElementById('test2'));
ReactDOM.render(<Person name="老刘" age="30" sex="女"/>, document.getElementById('test3'));
</script>

我们之前在学习react组件的构造器的时候也发现了props的身影:

1
2
3
4
 constructor(props) {
//任何有关this的操作不能写在super前面
super(props);
}

其中的props,就是传递给组件的所有参数组成的对象,如果不写super(props),就无法在构造函数中通过this.props访问到传递给组件的参数对象,但是最终this.props还是会被正确初始化。

我们可能认为,因为函数式组件内部没有this,所以不存在组件实例的三大属性。但是因为函数可以传参,我们可以在函数式组件中拿到props:

1
2
3
4
5
6
7
function Person(props){
const {name,age,sex}= props
return (<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>)

同时我们也观察到到,类式组件的构造函数中(如果书写的话),传入的参数也包含props。

refs

this.refs

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
class Demo extends React.Component {
// 展示左侧输入框的数据
showData = () => {
const { input1 } = this.refs;
alert(input1.value);
}

// 展示右侧输入框的数据
showData2 = () => {
const { input2 } = this.refs;
alert(input2.value);
}

render() {
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据" />
&nbsp;
<button onClick={this.showData}>点我提示左侧的数据</button>
&nbsp;
<input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />
</div>
);
}
}

在react中,我们没有必要直接通过document.querySelector来获得dom对象,我们直接给组件标签添加ref属性,并传入一个唯一的值key,然后这个标签对应的dom元素,就可以通过this.refs.key访问到,这一点和vue2中的语法是很像的(在vue2中是通过this.$refs.key拿到)。这种写法因为需要借助this,所以不能在函数式组件中使用。

回调函数

然而这种给ref属性赋值字符串的语法,是不被推荐的,因为被认为是效率低的,推荐的写法是传入回调函数,在传入的回调函数的参数中能拿到对应的dom。

1
2
3
4
5
6
7
8
9
10
render() {
return (
<div>
//获取到dom,并挂载到组件实例上去(这里是挂载到了input属性)
<input ref={(c)=>{this.input = c}} type="text" placeholder="点击按钮提示数据" />
&nbsp;
<button onClick={this.showData}>点我提示左侧的数据</button>
</div>
);
}

不过要注意的是,如果 ref 回调函数是以内联函数的方式定义的(比如上面的例子),在更新过程中,它会被执行两次,第一次传入参数 null,然后第二次会传入 DOM 元素。这是因为在每次渲染时,会创建一个新的函数实例,所以 React 清空旧的 ref(传入null), 并且设置新的,不过这是无关紧要的,开发过程中仍然可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script src="./react/react.development.js"></script>
<script src="./react/react-dom.development.js"></script>
<script src="./react/prop-types.min.js"></script>
<script src="./react/babel.min.js"></script>
<script type="text/babel">
class Component extends React.Component {
state = { cold: false }
func = () => { alert(this.input.value) }
changeWeather = () => { this.setState({ cold: !this.state.cold }) }
render() {
return (
<div>
<h1>当前天气{this.state.cold ? '炎热' : '寒冷'}</h1>
//内联函数
<input ref={(c) => { this.input = c; console.log("@", c) }} />
<button onClick={this.func}>点击获取输入框的值</button>
<button onClick={this.changeWeather}>点击改变天气</button>
</div>
)
}
}
ReactDOM.render(<Component />, document.querySelector('#box'))
</script>

为了避免这种问题,我们可以传入一个在类中已经定义好的,挂载到组件实例上的函数

React.createRef()

然而,react最推荐的方式是使用React.createRef()先定义一个容器,然后再把dom放入容器中:

1
2
myRef = React.createRef()
<input ref={this.myRef} type="text" placeholder="点击按饥提示数据"/>

最后通过this.myRef.current就能访问到dom,要注意的是每个容器只能放一个dom,感觉不如document….

事件处理

  • 通过onXxx属性指定事件处理函数(注意大小写)
  • React 使用的是自定义(合成)事件,而不是使用的原生 DOM 事件
  • React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)
  • 通过 event.target 得到发生事件的 DOM 元素对象

受控组件和非受控组件

  • 受控组件就是表单组件值改变的时候就更新值到state
  • 而非受控组件就是通过ref获取表单组件dom,然后需要的时候通过dom.value来获得值

生命周期(旧)

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
<script type="text/babel">
class Component extends React.Component {
state = { opacity: 1 }
componentDidMount(){
this.timer = setInterval(()=>{
const opacity = this.state.opacity-0.1
if(opacity<=0){
this.setState({opacity:1})
}else{
this.setState({opacity})
}
},200)
}
componentWillUnmount(){
clearInterval(this.timer)
}
render() {
return (
<div>
<span style={{opacity:this.state.opacity}}>学不好react怎么办</span>
<button onClick={()=>{ReactDOM.unmountComponentAtNode(document.querySelector('#box'))}}>不活了</button>
</div>
)
}
}
ReactDOM.render(<Component />, document.querySelector('#box'))
</script>

不能在render函数中开启定时器,通过this.setState来修改状态,因为这个操作会触发render,从而导致无限调用render,开启多个定时器,所以我们把开启定时器的代码写在生命周期函数中。简单的来说,不能在render函数中书写可能触发render的操作。

  • shouldComponentUpdate:这个钩子的作用就类似一个阀门,如果我们在组件中不写这个钩子,这个钩子默认存在且返回值为true。如果这个钩子的返回值为false,那么数据更新了也不会调用render方法更新视图

  • forceUpdate方法的效果是强制组件更新,即便数据没有改变,也不会经过shouldComponentUpdate钩子的判断

  • 具有父子组件的页面初次加载的时候,先按顺序执行父组件的constructorcomponentWillMountrender钩子,再按顺序执行子组件的constructorcomponentWillMountrendercomponentDidMount钩子,再执行父组件的componentDidMount钩子。也就是说,页面初次加载的时候,和update有关的钩子都不会被执行

  • 父组件执行到render函数的时候,开始解析子组件,直到子组件的dom创建好了(componentDidMount),父组件再执行后续代码,挂载dom(componentDidMount)。

  • 当父组件间中的数据更新(调用setState),父组件会依次调用shouldComponentUpdatecomponentWillUpdaterender钩子,调用到render钩子的时候,因为render中包含了子组件,所以开始触发子组件更新,然后子组件依次调用:componentWillReceivePropsshouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate钩子,再执行父组件的componentDidUpdate的钩子。

  • 也就是说,父组件如果不是某个组件的子组件,就不会触发componentWillReceiveProps钩子。

  • 其实如果父组件并没有给子组件传值,父组件重新render,也会导致子组件重新render。但是子组件重新render,只会调用子组件shouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate钩子,父组件不会重新渲染。

  • 我们可以看到,react的所有生命周期钩子都包含component,且几乎都以它开头,这是否有点繁琐呢?相比于vue;去除掉component,我们可以观察到,willMount就是vue中的beforeMountDidMount就是vue中的mounted

  • 对于更新部分,在vue中不存在shouldComponentUpdate这样控制是否更新的钩子,而willUpdate就是vue中的beforeUpdate,而DidUpdate就是vue中的updated

  • 对于render,在vue中,虽然render函数出现频率没有react中的那么高,但是它们的作用都是一致的,就是创建虚拟dom,而且它们被调用的时机都是相似的,在WillMountDidMount之间,或者在WillUpdateDidUpdate之间。

案例代码:

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
<!DOCTYPE html>
<html lang="en">
<body>
<div id="box"></div>
</body>
<script src="../react/react.development.js"></script>
<script src="../react/react-dom.development.js"></script>
<script src="../react/prop-types.min.js"></script>
<script src="../react/babel.min.js"></script>
<script type="text/babel">
class A extends React.Component {
state = { car: '奥迪' }
componentDidMount(){
console.log('A-componentDidMount')
}
componentWillMount(){
console.log('A-componentWillMount')
}
shouldComponentUpdate(){
console.log('A-shouldComponentUpdate')
return true
}
componentWillUpdate(){
console.log('A-componentWillUpdate')
}
componentDidUpdate(){
console.log('A-componentDidUpdate')
}
componentWillReceiveProps(props){
console.log('A-componentWillReceiveProps')
}
render() {
console.log('A-render')
return (
<div>
<B/>
<button onClick = {()=>{this.setState({car:'宝马'})}}>换车</button>
</div>
)
}
}
class B extends React.Component{
componentWillReceiveProps(props){
console.log('B-componentWillReceiveProps')
}
componentDidMount(){
console.log('B-componentDidMount')
}
componentWillMount(){
console.log('B-componentWillMount')
}
shouldComponentUpdate(){
console.log('B-shouldComponentUpdate')
return true
}
componentWillUpdate(){
console.log('B-componentWillUpdate')
}
componentDidUpdate(){
console.log('B-componentDidUpdate')
}
render(){
console.log('B-render')
return (
<div>{this.props.car}</div>
)
}
}
ReactDOM.render(<A />, document.querySelector('#box'))
</script>
</html>

生命周期(新)

在react的17.x版本后,componentWillMountcomponentWillUpdatecomponentWillReceiveProps这三个钩子(3个will)已经不推荐使用,因为它们是不重要的,而且经常被错误的使用,并且还可能在未来的异步渲染中引发更多问题,因此17.x版本后这个三个钩子必须加上UNSAFE_前缀,这并不意味这这三个钩子是不安全的,只是为了加长单词长度让人们尽可能的少用它们,并且这三个钩子在未来很可能被删除。

除此以外,还添加了2个新的生命周期钩子:getDerivedStateFromPropsgetSnapshotBeforeUpdate,虽然这2个钩子在开发过程这几乎没有用武之地,但是还是需要了解的。

getDerivedStateFromProps

1
2
3
4
static getDerivedStateFromProps(props,state){
console.log('getDerivedStateFromProps',props,state);
return props
}

这个钩子的中文意思就是,从props获取派生的状态,当你的state在任何的时候都取决于props,那么这个钩子才有作用。

而且这个钩子前必须使用static修饰,说明它其实会挂载到类上面,也就是构造函数上去。

这个静态方法会在组件实例化,以及每次组件更新之前被调用,它接收两个参数:props(新的属性)和state(当前的状态)。该方法的目的是根据传入的新属性,来决定是否需要更新组件的状态。

getSnapshotBeforeUpdate

这个钩子的中文意思是,在更新之前获取快照

1
2
3
4
5
6
7
8
9
//在更新之前获取快照
getSnapshotBeforeUpdate()(
console.log('getSnapshptBeforeUpdate');
return 'atguigu'
}
//组件更新完毕的钩子
componentDidUpdate(preProps,preState, snapshotValue){
console.log('Count---componentDidUpdate',preProps,preState,snapshotValue);
}

这钩子的返回值,会被传递给componentDidUpdate钩子,也就是snapshotValue

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
<script type="text/babel">
class Component extends React.Component {
state = { newsArr:[] }
componentDidMount(){
//每一秒钟往数组首部添加一个元素,后续代码还使用index作为key,真的很不利于dom复用
setInterval(()=>{
const news = '新闻' + (this.state.newsArr.length+1)
this.setState({newsArr:[news,...this.state.newsArr]})
},1000)
}
getSnapshotBeforeUpdate(){
return this.refs.ul.scrollHeight
}
componentDidUpdate(preProps,preState,val){
//数据和视图都更新后,再修改列表的scrollTop属性
this.refs.ul.scrollTop += this.refs.ul.scrollHeight-val
//其实如果为了简单,直接使用 this.refs.ul.scrollTop +=30就好了,这里主要是使用一下getSnapshotBeforeUpdate
//在元素没有滚动条之前,scrollTop无论如何修改都是0
}
render() {
return (
//可以直接在结构中写数组,react会帮忙渲染
<div>
<ul ref='ul'>
{this.state.newsArr.map((i,index)=> <li style={{height:30}} key={index}> {i} </li> )}
</ul>
</div>
)
}
}
ReactDOM.render(<Component />, document.querySelector('#box'))
</script>

diff算法

diff算法的最小比较单位是结点

react/vue中的key有什么作用?(key的内部原理是什么?)

简单的说:key是虚拟DOM对象的唯一标识,在更新显示时key起着极其重要的作用。
详细的说:当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,随后进行新旧虚拟dom的比较,比较规则如下:

  • 存在与新虚拟dom的key值相等的旧虚拟dom

    • 若虚拟DOM中内容没变,直接使用之前的真实DOM
    • 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中旧的真实DOM
  • 不存在与新虚拟dom的key值相等的旧虚拟dom,根据新的虚拟dom,创建新的真实DOM,随后渲染到到页面

使用每条数据的唯一标识作为key,有利于diff算法,有利于提高dom的复用率。

为什么遍历列表时,key最好不要用index?

如果逆序添加数据,会造成没有必要的真实dom更新。

脚手架

介绍

  • xxx脚手架:用来帮助程序员快速创建一个基于xxx库的模板项目
    • 包含了所有需要的配置(语法检查、jsx编译、devServer)
    • 下载好了所有相关的依赖
    • 可以直接运行一个简单效果
  • react提供了一个用于创建react项目的脚手架库:create-react-app
  • 项目的整体技术架构为:react + webpack + es6 + eslint
  • 使用脚手架开发的项目的特点:模块化,组件化,工程化;工程化的意思是构建工具(webpack)自动帮我们进行语法检查,代码压缩,兼容性处理等等一系列的功能

创建项目并启动

  • 全局安装

    1
    npm install -g create-react-app
  • 切换到想创项目的目录,使用:

    1
    create-react-app hello-react
  • 进入项目文件夹:

    1
    cd hello-react
  • 启动项目:

    1
    npm start

项目结构分析

public

public目录下存放的都是静态文件

html文件

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
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 设置文档使用UTF-8字符编码。 -->
<meta charset="utf-8" />
<!-- 定义网站的favicon图标。 -->
<!-- 在React项目中,%PUBLIC_URL%指向public目录,构建时会自动替换为正确路径,避免手动维护绝对路径。 -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- 用于移动端适配,让布局视口宽度等于设备宽度,开启理想视口 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 设置地址栏和页签的颜色,兼容性不好,只能在安卓手机上生效 -->
<meta name="theme-color" content="#000000" />
<!-- 网页的描述文本,有利于seo -->
<meta
name="description"
content="Web site created using create-react-app"
/>
<!-- 只适用于苹果手机,当苹果手机用户把网页链接添加到桌面的时候显示的图片 -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- 项目打包成混合开发的app的时候需要的配置文件,指定app的名字,图标和权限 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

robots.txt

爬虫规则文件

src

项目的源代码

App.js

App组件,也叫根组件,类似vue中的App.vue

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
import logo from './logo.svg';
import './App.css';

function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}

export default App;

看以看出,App.js使用的是函数式组件,而且对于使用react脚手架开发的项目,在js文件中也可以直接使用jsx语法,也不会报错。

App.css

App组件的样式文件,在App.js文件中被导入

App.test.js

App组件的测试文件,不过使用的频率并不高

index.css

项目的全局样式文件

index.js

项目的入口文件

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();

<App />标签外包裹<React.StrictMode>的作用是,帮助我们自动检查代码书写不合理的地方。

reportWebVitals是一个函数,是用来记录页面性能的,用到了web-vitals

setup.Tests.js

是用来测试整个项目的

样式的模块化

React本身并没有提供像Vue那样的scoped属性,来直接实现样式的局部作用域,那再react中如何实现组件样式的隔离呢?

如果我们在不同的组件中定义了相同的样式比如:

Hello/Hello.css文件中

1
2
3
.title{
background-color:'red'
}

Welcome/Welcome.css文件中

1
2
3
.title{
background-color:'blue'
}

这样当我们在App.js中同时引入Hello组件和Welcome组件,就会产生样式冲突 ,那如何避免样式冲突呢?

一种解决办法就是修改Hello.css文件名为Hello.module.css,同时还要修改引入css文件的方式

1
2
3
4
5
6
import hello from './Hello.module.css
export default class Hello extends Component{
render(){
return <h2 className={hello.title}>Hello,React!</h2>
}
}

vscode的react插件安装

  • 安装这个插件后,书写react代码就有对应的代码提示了

  • rcc:快速创建一个类式组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import React, { Component } from 'react'

    export default class index extends Component {
    render() {
    return (
    <div>index</div>
    )
    }
    }
  • rfc:快速创建一个函数式组件

功能界面的组件化编码

  • 拆分组件:拆分界面,抽取组件
  • 实现静态组件:使用组件实现静态页面效果
  • 实现动态组件
    • 动态显示初始化数据
      • 数据类型
      • 数据名称
      • 保存在哪个组件?,即数据存放的位置
    • 交互(从绑定事件监听开始)

案例todolist

  • body的宽度默认等于视口宽度,等于html的宽度,没有必要使用body{width:100%}

  • 在react组件名必须大写,否则会被认为是html标签

  • 搭建静态结构

  • 确定状态由谁来维护比较合适?如何把header(子组件)的数据传递给app(父组件),父组件给子组件传入一个方法,

    子组件调用这个方法的时候传入值,就能修改父组件中的数据。

  • 如何监听表单输入?添加onKeyUp属性

    1
    <input type="text" placeholder='请输入你的任务名称,回车键确认' onKeyUp={this.keyUp}/>
  • 如何确定按下了enter键?(event.keyCode每个按键都又对应的keyCode)添加的内容不能为空,enter后要清空输入框

  • 如何拿到<input type="checkbox"/>的值?通过e.target.checked

  • item组件如何修改父组件的父组件(app组件)中的数据?先将在app组件中定义修改todos的方法,在传递给List组件,List组件直接传递给item组件。

  • 鼠标悬浮到指定事项上,应该改变样式,可以使用:hover伪类实现

    1
    2
    3
    4
    5
    6
    .item button{
    visibility: hidden;
    }
    .item:hover button{
    visibility: visible;
    }
  • 实现删除,如何实现一个提示框提示是否删除?window.confirm,为什么不能直接写confirm

  • 在react中如何实现vue中的计算属性?

  • defaultChecked属性只能生效一次,之后被删除

  • 总数为0的时候,不能显示全选

所学知识点:

  • 拆分组件、实现静态组件,注意:className、style的写法
  • 动态初始化列表,如何确定将数据放在哪个组件的state中?
    • 某个组件使用:放在其自身的state中
    • 某些组件使用:放在他们共同的父组件state中(官方称此操作为:状态提升)
    • 关于父子之间通信:
      • 【父组件】给【子组件】传递数据:通过props传递
      • 【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
      • 注意defaultChecked和checked的区别,类似的还有:defaultValue和value
    • 状态在哪里,操作状态的方法就在哪里

在react中配置代理

方法一 :在package.json中添加proxy属性

这种配置方法的缺点很明显,就是不能配置多个代理(多个目标服务器)

1
"proxy":"http://localhost:5000"

方法二:在src目录下新建setupProxy.js文件,该文件中只能使用CJS语法,因为会被合并到webpack配置文件中。

这种方法的好处是能配置多个代理,并且能控制哪个请求要代理到哪个服务器;缺点是配置较为复杂,而且需要修改请求的url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const proxy = require('http-proxy-middleware')
module.exports = function(app)(
app.use(
proxy('/api1', {//遇见/api1前缀的请求,就会触发该代理配置
target:'http://1ocalhost:5000',//请求转发给谁
changeOrigin:true,//控制服务器收到的请求头中Host的值,如果为true,修改为1ocalhost:5000
pathRewrite:{'^/api1':''}//重写请求路径(必须)
),
proxy('/api2', {
target:'http://localhost:5001'
changeorigin:true,
pathRewrite:{'^/api2':""}
}
)

其实前端中代理的配置,本质都差不多,都是基于http-proxy-middleware这个库。关于代理的更多介绍,参考前端面试vue一文。

在兄弟组件间传递数据

使用消息订阅发布机制,下载pubsub库。

react路由

路由的理解

什么是路由

  • 一个路由就是一个映射关系(key:value)
  • key为路径, value可能是function或component

路由分类

  • 后端路由:

    • 理解: value是function, 用来处理客户端提交的请求。
    • 注册路由: router.get(path, function(req, res))
    • 工作过程: 当node接收到一个请求时, 根据请求路径,找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据。
  • 前端路由:

    • 浏览器端路由, value是component, 用于展示页面内容。
    • 注册路由: <Route path="/test" component={Test}>
    • 工作过程: 当浏览器的path变为/test时, 当前路由组件就会变为Test组件。

react-router

  • react的一个插件库
  • 专门用来实现一个SPA应用
  • 基于react的项目基本都会用到此库。
  • 这个库其实有三个版本,分别是web,native和anywhere,我们学的其实是react-router-dom,即web端的路由库。

推荐一个网站:印记中文 - 深入挖掘国外前端新领域,为国内 Web 前端开发人员提供优质文档!

单页面程序的原理就是,点击修改路由,然后路由器监听到路由改变,替换组件。

原生html中通过a标签来跳转页面,在react中通过Link标签来实现,而且这个链接要使用特定的Router标签(BrowserRouter或者HashRouter)包裹。

其实link标签最后还是会被转化成a标签,不过在此基础上添加了监听,阻止了页面跳转

再react中通过Route标签,在组件中注册路由(注册组件的子路由),当这个组件被渲染的时候,对应的路由被注册,且启动一次路由匹配,可能触发重定向

而在vue中,在router/index.js文件中注册路由

1
2
3
4
5
6
7
//BrowserRouter使用的是history路由,HashRouter代表使用的是哈希路由
import About from './components/about'
import {Link,Route,BrowserRouter} from 'react-router-dom'
<BrowserRouter>
<Link to='/about'>跳转到about页面</Link>
<Route path='/about' component={About} ></Route>
<BrowserRouter>

要注意的是Link标签,和对应的Route标签必须在同一个BrowserRouter标签下,为了方便起见,我们通常使用BrowserRouter包裹整个App组件,毕竟这个应用应该只有一个路由器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App/>
</BrowserRouter>
</React.StrictMode>
);

如果想要点击标签有对应的高亮样式,那么就不能使用Link标签而是NavLink标签

1
<NavLink activeClassName="atguigu"> </NavLink>

activeClassName的值默认是active,但是如果你想要自定义高亮样式,那么就自定义一个类,然后传入。

封装NavLink:如果每个NavLink标签都加上activeClassName,那么这个标签的长度就变得非常长了,不方便阅读,也不够简洁,其实我们可以自定义一个MyNavLink组件,实现对NavLink的封装。本质就是返回一个添加了activeClassNameNavLink

1
2
3
4
5
6
7
8
9
import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'
export default class MyNavLink extends Component {
render() {
return (
<NavLink {...this.props} activeClassName="active"></NavLink>
)
}
}

然后复用,这样在MyNavLink标签上就不需要写activeClassName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import './App.css';
import {Route} from 'react-router-dom'
import About from './pages/about';
import Content from './pages/content';
import MyNavLink from './components/MyNavLink';
function App() {
return (
<div className='container'>
<div className='left'>
<MyNavLink to='/about'> About </MyNavLink>
<MyNavLink to='/content'> Content </MyNavLink>
</div>
<div className='main'>
<Route path='/about' component={About}></Route>
<Route path='/content' component={Content}></Route>
</div>
</div>
);
}

export default App;

值得注意的是,组件标签(比如NavLink,MyNavLink)的标签体,也算是一个标签属性(children属性),同样的,在组件标签中给children属性赋值,其实就是在书写标签体。

1
2
3
<MyNavLink to='/about'> About </MyNavLink>
<!--等效于-->
<MyNavLink to='/about' children="About ">

路由组件和一般组件

根据组件的用途不同,可分为一般组件路由组件

写法不同

路由组件是指切换路由展示的组件,不通过直接书的方式写来渲染;而一般组件则是直接拿来渲染的组件;比如我们有个组件About,通过上述方式使用的就叫做路由组件,如果通过直接书写即<About/>方式展示的,就叫做一般组件。

接收到的参数不同

一般组件如果不在标签上传值,那么这个组件内部就接收不到任何值(this.props是空对象);但是路由组件即便没有显式给它传参,也会接收到参数。

  • history

    • go: f go(n)
    • goBack: f goBack()
    • goForward: f goForward()
    • push: f push(path, state)
    • replace: f replace(path, state)
  • location:

    • pathname:”/about”
    • search: “”
    • state:undefined
  • match:

    • params: {}
    • path: “/about”
    • url: “/about”

存放位置不同

一般组件通常放到components目录下,而路由组件通常放在pages或者views目录下。

Switch组件

1
2
3
4
5
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Test}/>
<Route path="/home" component={Home}/>
</Switch>

如果不使用Switch组件,如果路由匹配到Test组件,还会继续匹配,最终同时会展示Home组件和Test组件;但是如果使用了Switch组件,匹配成功后,就不会继匹配了。简单的说,给Route标签包裹Switch组件的作用就是,确保只展示第一个匹配到的组件

模糊匹配

react路由默认使用的是模糊匹配,也就是说,如果当前路由是/home/a/b,某个组件的path是/home,那么这个组件将会被展示,但是如果给route标签添加exact属性,就开启了严格匹配

我们一般情况是不开启严格匹配的,举个例子,如果我们想要展示二级路由组件,就必须先展示一级路由组件,但是开启了严格匹配,就匹配不到一级路由组件了。

redirect

1
2
3
4
//App.js
<Route path="/about" component={About}/>
<Route path="/home" component={Test}/>
<Redirect to='/about'>//当前2个路由都未被匹配,则重定向到/about

当页面url等于localhost:3000/,渲染的其实是App组件,也就是根组件(因为访问localhost:3000/会返回index.html文件,然后解析这个html文件构建dom树,异步加载引入的js文件,当dom树构建好便执行js文件,将App组件挂载到这个html文件上),然后上述注册路由的代码就会被触发,并开始与当前页面url匹配,然后发现前2个都匹配不了,于是重定向到/about

嵌套路由

开启二级路由,只需要在一级路由组件中(比如About组件),继续书写NavLinkRoute标签即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default class About extends Component {
render() {
return (
<div>
<div className='header'>
<MyNavLink to='/about/message'>message</MyNavLink>
<MyNavLink to='/about/news'>news</MyNavLink>
</div>
<div className='content'>
<Route path='/about/message' component ={Message}></Route>
<Route path='/about/news' component={News}></Route>
</div>
</div>
)
}
}

必须注意的是,二级组件的匹配路径,必须以父组件(一级组件)的匹配路径开头,的比如/about,为什么要这样写呢?因为我们想要展示出二级组件,必须先展示出一级组件,而只有按照这种方式书写二级组件匹配路径,才能做到同时匹配一级组件和二级组件。当我们点击跳转到/about只会展示一级组件about,同时注册二级路由(因为相关注册代码就再about组件里)。如果我们再添加Redirect,就会重定向到message组件。

1
2
3
4
5
<div className='content'>
<Route path='/about/message' component ={Message}></Route>
<Route path='/about/news' component={News}></Route>
<Redirect to='/about/message'></Redirect>
</div>

携带路由参数

动态路由传参

完成动态路由传参需要三步,分别是传参,声明,和取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default class News extends Component {
state = {news:[{id:1,title:'消息1'},{id:2,title:'消息2'},{id:3,title:'消息3'}]}
render() {
const {news} = this.state
return (
<div>
<ul>
{<!--传参-->}
{news.map(i => <li key={i.id}><Link to = {`/about/news/detail/${i.id}`}>{i.title}</Link></li>)}
</ul>
<div>
{<!--声明-->}
<Route to="/about/news/detail/:id" component={Detail}></Route>
</div>
</div>
)
}
}

取值:可以在Detail组件中,通过this.props.match.params获取到我们传入的参数。

查询参数传参

完成查询参数传参需要2步,分别是传参和取值

路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
按收参数:this.props.location.search
备注:获取到的search是urlencoded编码字符串,需要借助querystring解析

通过state传参

通过state传参只需要2步,第一步是传参,第二部步是接收。

这种方式区别于前2种方式,路径里没有任何提示,或者说路径中不包含任何传递给路由组件的数据,

路由链接(携带参数):<Link to = {{pathname:'/demo/test', state:{name:'tom',age:18} }}>详情</Link>

感觉这个state好像history API中的history.pushState(title, state, url)中的state。

注册路由(无需声明,正常注册即可):<Routepath="/demo/test" component={Test}/>
接收参数:this.props.location.state
备注:刷新也可以保留住参数

路由跳转模式

默认情况下,我们的路由跳转模式是push,也就是说每次路由跳转都会往历史记录栈里push一条历史记录,其实我们可以修改路由模式为replace只需要在Link标签上添加replace属性

其实在vue中也是一样的Router.push()对应的就是push模式,Router.replace对应的就是replace模式

编程式路由导航

this.props.hsitory属性下有2个方法:push和replace,分别对应2中路由跳转模式。

1
2
3
4
5
6
history:
go: f go(n)//类似history api中的go方法
goBack: f goBack()//后退
goForward: f goForward()//前进
push: f push(path, state)
replace: f replace(path, state)//第二个参数用于通过state传参
1
2
3
4
5
6
<ul>
{news.map(i => <li key={i.id}>
<Link to = {`/about/news/detail/${i.id}`}>{i.title}</Link>
<button onClick={()=>{this.replace(`/about/news/detail/${i.id}`)}}>replace跳转</button>
</li>)}
</ul>

withRouter

一般组件并不会被传递路由组件的那些api(比如go,goBack,goForward),不显式地给一般组件传参,一般组件就接收不到任何参数,为了能让一般组件也能使用路由组件的那些api,我们就需要借助withRouter这个函数

1
2
3
4
5
6
7
8
9
10
11
import { withRouter } from 'react-router-dom'
class Header extends Component {
render() {
return (
<div>
header
</div>
)
}
}
export default withRouter(Header)//只需要再导出普通组件的时候,使用withRouter包装一下

在新版本的react-router中,这个api已经被移除了。

BrowserRouter和HashRouter的区别

  • BrowserRouter使用的是History API,借助这些api来形成历史记录,而HashRouter单纯是通过修改URL的哈希部分,来形成历史记录的

  • state传参中的state参数,是history api特有的,使用BrowserRouter并刷新页面,state不会丢失,而使用HashRouter并刷新页面,state会丢失

    1
    history.pushState(state, title, url)

UI组件库Ant Design

  • 使用ui组件库不需要去背,用熟练了就好
  • 难点在于分析那些代码是属于你想要的组件的
  • 按需引入样式,减少最终文件的体积,查看文档(具体位置是《在create-react-app中使用》)按照文档的指示一步一步操作即可。
  • 修改主题颜色

redux

学习文档

英文文档:https://redux.js.org/
中文文档:http://www.redux.org.cn/
Github:https://github.com/reactjs/redux

redux是什么

  • redux是一个专门用于做状态管理的JS库(不是react插件库)。

  • 它可以用在react,angular, vue等项目中,但基本与 react 配合使用。

  • 作用:集中式管理react应用中多个组件共享的状态

什么情况下需要使用redux

  • 某个组件的状态,需要让其他组件可以随时拿到(共享)。
  • 一个组件需要改变另一个组件的状态(通信)。
  • 总体原则:能不用就不用,如果不用比较吃力才考虑使用。
  • reducer的作用不仅仅包括更新数据,还有初始化数据的作用,因为刚开始是没有previewState的,所以它的值是 undefined

redux的三个核心概念

react-redux

react扩展

setState

setState(stateChange, [callback]) —— 对象式的 setState

  1. stateChange 为状态改变对象(该对象可以体现出状态的更改)
  2. callback 是可选的回调函数,它在数据和视图都更新完毕后 (render 调用后) 才被调用

setState(updater, [callback]) —— 函数式的 setState

  1. updater 为返回 stateChange 对象的函数。
  2. updater 可以接收到 stateprops(第一个参数是state,第二个参数是props)
  3. callback 是可选的回调函数,它在数据和视图都更新完毕后 (render 调用后) 才被调用
  4. 这种情形,传入的2个参数都是函数

总结:

  1. 对象式的 setState 是函数式的 setState 的简写方式(语法糖)。
  2. 使用原则:
    • 如果新状态不依赖于原状态,比如this.setState({count:90}) 使用对象方式
    • 如果新状态依赖于原状态,使用函数方式
    • 如果需要在 setState() 执行后获取最新的状态数据, 要在第二个 callback 函数中读取

懒加载

  • 从react中引入lazy函数,修改导入路由组件的方式(竟然不是从react-router中导入的吗)
  • 导入Suspense组件包括Route,传入组件尚未被加载的时候,展示的结构或者组件
1
2
3
import React, { Component,lazy,Suspense } from 'react'
const Message = lazy(()=>import('./message'))
const News = lazy(()=>import('./news'))
1
2
3
4
5
<Suspense fallback={<h3>loading</h3>}>
<Route path='/about/message' component ={Message}></Route>
<Route path='/about/news' component={News}></Route>
<Redirect to='/about/message'></Redirect>
</Suspense>

Hooks

React.useState

这个钩子的作用就是,让函数式组件也可以有自己的状态,并且可以对状态进行读写。

每次修改状态,都会重新调用函数式组件,但是由于对useState特殊处理,并不会重新初始化变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from "react"
export default function useState(){
//进行数组解构,数组解构可以任意取名
//其实就等效于0:name,1:setName,本质和对象结构是一样的
const [name, setName] = React.useState('tom')
const [age, setAge] = React.useState(0)
function handlerChangeName(){
setName((name)=>{return name==='cindy'?'tom':'cindy'})
}
function handlerChangeAge(){
setAge(age=>age+1)
}
return (
<div>
姓名:{name}
年龄:{age}
<button onClick={handlerChangeName}>修改姓名</button>
<button onClick={handlerChangeAge}>修改年龄</button>
</div>
)
}

React.useEffect

Effect Hook 可以让你在函数组件中执行副作用操作,用于模拟类式组件中的生命周期钩子

React 中的副作用操作:

  • 发 ajax 请求数据获取
  • 设置订阅 / 启动定时器
  • 手动更改真实 DOM

语法和说明:

1
2
3
4
5
6
7
useEffect(() => {
// 在此可以执行任何带副作用的操作
return () => {
// 在组件卸载前执行
// 在此做一些收尾工作,比如清除定时器/取消订阅等
}
}, [stateValue]); // 如果指定的是 [], 表示不监听任何状态的改变,回调函数只会在第一次 render() 后执行
1
2
3
4
5
6
7
8
React.useEffect(()=>{
let timer = setInterval(()=>{
setAge(age=>age+1)
},1000)
return ()=>{
clearInterval(timer)
}
},[])

可以把 useEffect Hook 看做如下三个函数的组合:

  • componentDidMount():如果第二个参数穿入的是空数组,表示不监听任何状态改变,则效果就相当于这个生命周期钩子
  • componentDidUpdate():如果第二个参数穿入的不是空数组,监听的状态改变后,也会触发传入的回调函数。
  • componentWillUnmount():传入的回调函数返回的函数,其作用就相当于这个钩子

可以看出在函数式组件中使用useEffect来模仿类式组件中的生命周期钩子,还是比较麻烦的。

React.useRef

RefHook可以在函数组件中存储/查找组件内的标签或任意其它数据
语法:const refContainer = React.useRef()
作用:保存标签对象,功能与React.createRef()一样

1
2
3
4
5
6
7
8
9
10
import React from "react"
export default function useState(){
const target = React.useRef()
return (
<div>
<input type="text" ref={target} />
<button onClick={()=>{alert(target.current.value)}}>点击提示</button>
</div>
)
}

React.Fragments

1
import { Fragment } from 'react'

用来解决react组件中只能有一个根标签的问题,Fragment标签不会被渲染,就类似vue3中的fragment或者template

React.createContext

这个api主要用来解决祖先组件给后代组件传值,就类似vue中的provide和inject

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
import React, { Component } from 'react'
//这个context应该放在所有后代组件都能访问到的位置
const context = React.createContext()
//然后使用context.Provider来包裹子组件
const Provider = context.Provider

export default class Content extends Component {
state = {name:'tom'}
render() {
return (
<div>
我是content组件
{//然后B组件还有B组件的所有后代组件都可以接收到}
{//必须通过value属性传值}
<Provider value={this.state.name}>
<B></B>
</Provider>
</div>
)
}
}
class B extends Component {
render() {
return (
<div>
我是B组件
<C></C>
</div>
)
}
}
class C extends Component {
//后代组件想要接收这个值,就必须声明,然后就能在this.context中拿到这个值
//在这个例子中,this.context的值就是'tom'
static contextType = context
render() {
console.log(this.context)
return (
<div>我是C组件{this.context}</div>
)
}
}

组件优化

Component的2个问题

只要执行setState(),即使不改变状态数据,组件也会重新render(),效率低
只要当前组件重新render(),就会自动重新render子组件,纵使子组件没有用到父组件的任何数据,效率低

1
2
3
4
5
6
7
8
9
10
11
12
13
//父组件
import React, { Component } from 'react'
import Child from '../Child'
export default class Parent extends Component {
state = {count:0}
render() {
console.log('Parent is rendered')
const {count} = this.state
return (
<div>Parent<Child></Child><button onClick={()=>{ this.setState({count:count+1}) }}>+</button></div>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//子组件
import React, { Component } from 'react'

export default class Child extends Component {
render() {
console.log('Child is rendered')
return (
<div>Child</div>
)
}
}

效率高的做法:只有当组件的state或者props数据发生改变时,才重新render()
原因:Component中的shouldComponentUpdate()总是返回true
解决:

  • 重写shouldComponentUpdate()方法:比较新旧state或props数据,如果有变化才返回true,如果没有返回false

    shouldComponentUpdate() 接收两个参数:

    • nextProps: 下一次的 props
    • nextState: 下一次的 state

    需要在这个方法中比较当前的 propsstate(通过 this.propsthis.state)与下一次的 propsstate。如果返回 true,组件会重新渲染;如果返回 false,组件不会重新渲染。问题是如何进行对象之间的比较呢?直接使用this.props === nextProps这种方式吗?我测试过,即便父组件给子组件传入的只是一个常量,父组件触发render之后,子组件的nextProps也部等于this.props,虽然这2个是内容完全相同的对象,但是地址不同。

  • 使用PureComponentPureComponent重写了shouldComponentUpdate(),只有state或props数据有变化才返回true

  • 注意:

    • PureComponent只是进行state和props数据的浅比较,本质上就是只比较对象的第一层级属性值。对于基本数据类型(如字符串、数字、布尔值等),它直接比较这些值是否相等;而对于引用数据类型(如对象和数组),则比较它们的引用地址是否相同。
    • 不要直接修改state数据,而是要产生新数据,否则视图不会更新。
  • 项目中一般使用PureComponent来优化上述问题

render 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
import React, { Component } from 'react'

export default class Parent extends Component {
render() {
return (
<div>Parent<A></A></div>
)
}
}
class A extends Component {
state = {name:'A'}
render() {
return (
<div>A <B name={this.state.name}></B> </div>
)
}
}
class B extends Component {
render() {
return (
<div>B</div>
)
}
}

上述例子中,A和B的父子组件关系是显而易见的,因为我们直接把<B></B>写在了A组件中,此时如果A组件想要给B组件传值,直接在B组件标签上添加属性即可。

但是除此以外,让A,B组件形成父子关系的方式还有如下方式:

1
2
3
4
5
6
7
8
9
10
11
import React, { Component } from 'react'

export default class Parent extends Component {
render() {
return (
<div>Parent<A>
<B></B>
</A></div>
)
}
}

此时想要在A组件中展示B组件,还需要添加代码:

1
2
3
4
5
6
7
8
class A extends Component {
render() {
console.log(this.props)
return (
<div>A{this.props.children}</div>
)
}
}

其中this.props.children的值是一个对象,结构较为复杂。

此时想要实现A组件向B组件传值,貌似就难以实现了,反而,实现Parent组件向B组件传值变得简单了。

其实,要实现A,B组件的父子关系,还可以通过另一中方法实现:

1
2
3
4
5
6
7
8
9
import React, { Component } from 'react'

export default class Parent extends Component {
render() {
return (
<div>Parent<A render = {(name)=><B name={name}/>}></A></div>
)
}
}

然后只需要修改A组件代码:

1
2
3
4
5
6
7
8
9
class A extends Component {
s
render() {
console.log(this.props)
return (
<div>A{this.props.render()}</div>
)
}
}

就能实现A,B组件的父子关系,这种方法还有一个好处就是,能实现A组件给B组件传递参数

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
import React, { Component } from 'react'

export default class Parent extends Component {
render() {
return (
<div>
Parent
<A render={(name)=><B name={name}></B>}></A>
</div>
)
}
}
class A extends Component {
state = {name:"A"}
render() {
return (
<div>A{this.props.render(this.state.name)}</div>
)
}
}
class B extends Component {
render() {
return (
<div>B</div>
)
}
}

我们可以发现,这一点其实很像vue中的插槽,A,B组件,在我们未使用它们之前,它们并没有明确的父子组件关系,但是我们可以借助render props实现给组件动态传递结构。

error boundary

错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面

特点:只能捕获后代组件生命周期中(比如render中)产生的错误,不能捕获组件自己产生的错误,和其他组件在合成事件、定时器中产生的错误。

使用方式:getDerivedStateFromError配合componentDidCatch

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
import React, { Component } from 'react'

export default class Parent extends Component {
state = {error:null}
static getDerivedStateFromError(error){
return {error}
}
componentDidCatch(){
console.log('后代组件出现了错误')
}
render() {
return (
//这里真的特别像vue中的条件渲染
<div>Parent{this.state.error?<h3>网络出现了问题</h3>:<Child></Child>}</div>
)
}
}


class Child extends Component {
// state = {arr:[{id:'1', name:'tom'}, {id:'2', name:'cindy'}]}
state = {arr:'123'}
render() {
return (
<div>{this.state.arr.map(e=><h3 key={e.id}>{e.name}</h3>)}</div>
)
}
}

组件通信总结

组件间的关系

  • 父子组件
  • 兄弟组件(非嵌套组件)
  • 祖孙组件(跨级组件)

几种通信方式

  • props
    • children props
    • render props
  • 消息订阅-发布
    pubs-sub、event等等
  • 集中式管理:
    redux、dva等等
  • ConText:生产者-消费者模式

比较好的搭配方式
父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)

React Router6

概述

  1. React Router 以三个不同的包发布到 npm 上,它们分别为:
    1. react-router: 路由的核心库,提供了很多的:组件、钩子。
    2. react-router-dom: 包含react-router所有内容,并添加一些专门用于 DOM 的组件,例如<BrowserRouter>等。
    3. react-router-native: 包括react-router所有内容,并添加一些专门用于ReactNative的API,例如 <NativeRouter> 等。
  2. 与React Router 5.x 版本相比,改变了什么?
    1. 内置组件的变化:移除 <Switch/>,新增 <Routes/> 等。
    2. 语法的变化:component={<About />} 变为 element={<About />} 等。
    3. 新增多个hook:useParams、useNavigate、useMatch 等。
    4. 官方明确推荐函数式组件了!!!

变化

Routes与Route

  • 移除 <Switch/>,新增 <Routes/> ,而且是必须使用<Routes/> 包括<Route/>,默认匹配到了组件就不会继续匹配。

  • 使用Route注册组件的语法也改变了:

    1
    2
    <Route path='/about' component={About}></Route>  //旧版
    <Route path='/about' element={<About/>}></Route> //新版

    而且后面使用Route来注册子路由的方式也不推荐使用了,转为使用路由表。

删除了Redirect,添加了Navigate,只要 <Navigate> 组件被渲染,就会修改路径,切换视图。

replace 属性用于控制跳转模式(push 或 replace,默认是 push)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState } from 'react';
import { Navigate } from 'react-router-dom';

export default function Home() {
const [sum, setSum] = useState(1);

return (
<div>
<h3>我是Home的内容</h3>
{/* 根据sum的值决定是否切换视图 */}
{sum === 1 ? (
<h4>sum的值为{sum}</h4>
) : (
<Navigate to="/about" replace={true} />
)}
<button onClick={() => setSum(2)}>点我将sum变为2</button>
</div>
);
}
  • <Navigate>组件的作用是只要被渲染就会修改路径,切换视图。

  • replace 属性用于控制跳转模式,默认是 push

  • 示例代码展示了如何根据 sum 的值来决定是否使用 <Navigate> 组件进行路径切换。

  • 如果 <Navigate to='/about' /> 始终存在于组件中(即没有通过条件判断来控制它的渲染),那么无论用户点击哪个 <NavLink>,页面都会被强制跳转/about,最好的做法应该是写在路由表中:

    1
    2
    3
    4
    {
    path:'/',
    element: <Navigate to='/about'></Navigate>
    }

在react router6中,activeClassName已经被废弃,下面是新的语法介绍:

1
2
<NavLink className={computedClassName} to="/about">About</NavLink>
<NavLink className={computedClassName} to="/home">Home</NavLink>
1
2
3
function computedClassName({isActive}){
return isActive ?'list-group-item atguigu':'list-group-item'
}

useRoutes

在React Router6中,貌似想要使用嵌套路由,就必须使用路由表,即借助useRoutes

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
// routes/index.jsx
import About from '../pages/about';
import Content from '../pages/content';
import Message from '../pages/about/message'
import News from '../pages/about/news'
export const routes = [
{
path:'/about',
element: <About></About>,
children: [
{
path: 'message',
element: <Message></Message>
},
{
path: 'news',
element: <News></News>
}
]
},
{
path:'/content',
element:<Content></Content>
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//App.js
import './App.css';
import {NavLink, useRoutes} from 'react-router-dom'
import { routes } from './routers'
function App() {
let element = useRoutes(routes)
return (
<div className='container'>
<div className='left'>
<NavLink to='/about'>About</NavLink>
<NavLink to='/content'>Content</NavLink>
</div>
<div className='main'>
{/* 在App.jsx组件中,通过"展示路由表"来确定路由出口 */}
{element}
</div>
</div>
);
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// pages/about/index.jsx
import React from 'react'
import {NavLink,Outlet} from 'react-router-dom'
import './index.css'

export default function About() {
return (
<div>
<div className='header'>
{/*路由路径使用相对路径,相对于父组件的路由路径 */}
<NavLink to='message'>message</NavLink>
<NavLink to='news'>news</NavLink>
</div>
<div className='content'>
{/* 在其他路由组件中,通过书写Outlet来确定子路由组件的出口 */}
<Outlet></Outlet>
</div>
</div>
)
}

useParams

被用来在函数式组件中,获取传递过来的动态路由参数。

调用这个函数,不需要传入参数,会返回一个动态路由参数对象

1
2
3
4
5
6
7
8
9
10
//about/news/detail/index.jsx
import React from 'react'
import { useParams } from 'react-router-dom'

export default function Detail(){
const params = useParams()
return (
<div>Detail:{params.id}</div>
)
}

useSearchParams

被用来在函数式组件中,获取传递过来的查询参数。

调用这个函数,也不需要传入任何参数,返一个数组;第一个参数是一个URLSearchParams对象,只能调用这个对象的get方法取得对应的值;第二个参数是一个函数,用来修改当前页面的查询参数,用的不多,会触发组件的重新渲染。

1
2
3
4
5
6
7
8
9
10
11
import React from 'react'
import { useSearchParams } from 'react-router-dom'
export default function Message(){
const [search, setSearchParams] = useSearchParams()
return (
<div>
Message:{search.get('name')}
<button onClick={()=>{setSearchParams('name=cindy')}}>点击修改查询参数</button>
</div>
)
}

useMatch

useLocation

调用这个函数,不需要传入值,返回一个location对象,这个对象的结构和类式路由组件中的this.props.location的结构是相同的。

我们就可以通过这个函数,来接受传入函数式路由组件中的state

1
2
3
4
5
<div className='header'>
//传递state
<NavLink to='message?name=tom&age=21' state={{name:'sun'}}> message </NavLink>
<NavLink to='news'>news</NavLink>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react'
import { useSearchParams,useLocation } from 'react-router-dom'
export default function Message(){
console.log('render')
const [search, setSearchParams] = useSearchParams()
console.log(useLocation().state)//输出{name: 'sun'}
return (
<div>
Message:{search.get('name')}
<button onClick={()=>{setSearchParams('name=cindy')}}>点击修改查询参数</button>
</div>
)
}

useNavigate

无论是普通组件还是路由组件,都可以使用这个函数实现编程式导航。

直接调用useNavigate,返回一个navigate函数。

navigate(to, options) 接受两个参数:

  1. **to**:
    • 类型:stringobject
    • 表示要导航的目标路径。
    • 如果是字符串,则表示路径(如 /about)。
    • 如果是对象,可以包含以下属性:
      • pathname: 目标路径(如 /about)。
      • search: 查询参数(如 ?id=123)。
      • hash: 锚点(如 #section1)。
      • state: 传递的状态数据。
  2. **options**:
    • 类型:object
    • 可选配置项:
      • replace: true:替换当前的历史记录条目,而不是添加一个新的条目。
      • state:传递状态数据(与 to 对象中的 state 等效)。

useNavigate 还支持基于相对路径的导航。例如,你可以通过传递一个负数来返回上一页。

1
2
3
4
5
6
7
8
9
const navigate = useNavigate();

const goBack = () => {
navigate(-1); // 返回上一页
};

const goForward = () => {
navigate(1); // 前进一页
};

其他

  • useInRouterContext:用来判断一个组件是否在路由环境中,简单的来说,是否被BrowserRouter或者HashRouter包裹。

  • useNavigationType:返回当前的导航类型(用户是如何来到当前页面的)。返回值:POP、PUSH、REPLACE。POP是指在浏览器中直接打开了这个路由组件(刷新页面)。

  • useOutLet:用来呈现当前组件中渲染的嵌套路由

    1
    2
    3
    4
    const result = useOutlet()
    console.log(result)
    //如果嵌套的路由组件没有挂载,则result为null
    //如果嵌套路由已经挂载,则展示嵌套的路由对象
  • useResolvedPath:用来解析路径,传入任意一个路径,返回一个解析后的对象。