写在前面
在 React 中,定义组件的方式有两种,一个是 class 类组件,一个是函数组件。class 类组件的实现相比于函数组件要复杂。
1. return React 元素
React 组件必须是返回 React 元素的物件,因此无论是函数组件还是类组件都必须有 return React元素
。
在 class 类组件的返回 React 元素的位置是在 render() 函数中,也就是说,类组件中必须有一个 render() 函数,在 render() 函数中必须有 return 的值,这个值必须是虚拟 DOM(React 元素)。
class xxx extends React.Component{
render(){
return (React组件)
}
}
无论是函数组件还是类组件,return 的 React 元素的语法必须是由一个标签包裹起来的所有虚拟 DOM 内容,返回多个独立的标签是错误的,如下:
render(){
return(
<div> first </div>
<div> second </div>
) //会报错
}
解决此种错误的方式有两种,一种是使用一个 div 标签将其包裹起来,另外一种方式就是使用 React 提供的 <React.Fragment>
将其包裹起来,但是其渲染到页面时是不会有 <React.Fragment>
的,如下:
render(){
return(
<React.Fragment>
<div> first </div>
<div> second </div>
</React.Fragment>
)
}
上述的 <React.Fragment>
还有一种简写方式 <>
,如下:
render(){
return(
<>
<div> first </div>
<div> second </div>
</>
)
}
2. 两种创建 class 组件的方式
在 React 中有两种创建 class 组件的方式,区分这两种方式的原因是 js 语法版本的不同。在 ES5 时代,没有 class 这个概念,因此 React 就提供了创建 class 类组件的函数接口,如下:
import React from 'react'
const A = React.createClass({
render(){
return(
<div>Hello Word</div>
)
}
})
export default A
在新的 ES6 语法出来后,支持了 class。因此,用 ES6 语法定义的类组件的语法如下:
import React from 'react'
class A extends React.Component{
constructor(props){
super(props);
}
render(){
return (
<div>Hello Word</div>
)
}
}
export default A
目前大部分浏览器都支持了 ES6 语法,因此都在使用 class 语法定义 React 的类组件,对于一些不支持 ES6 语法的浏览器,可以使用 webpack + babel 将 ES6 语法转为 ES5 语法。
3. props 外部数据
3.1 外部传入 props
外部数据是怎么传入到组件的?首先,无论是 React 还是 Vue ,组件的使用都是作为自定义 xml 标签的方式使用的,如 <Person/>
,那么在使用组件时向组件传递数据信息就是通过 xml 中的标签内的属性值传递的,如 <Person name="test" age="{18}">hi</Person>
,那么 {name:'test', age: 18, children:'hi'}
就会作为 Person 组件的外部数据对象 props 传递给 Person 组件。也就是说 <Person>
组件的 props 对象为:
props = {
name: 'test',
age: 18,
children: 'hi' //React 中的 props 默认有一个 children 属性,用于接收组件中间的内容
}
3.2 类内初始化 props
props 在类组件内的初始化是在 constructor 函数中进行的,如下:
constructor(props){
super(props); // 会将 props 挂在 this 上
}
如果 class 组件中的 constructor 函数中只有上述初始化 props 的三行的形式时,可以省略不写 constructor,上述三行是默认的。但如果在 constructor 函数中有其他语句时,上述三行就必须写完整。
constructor(props){
super(props);
this.state = {
n: 0
}
}
3.3 读取 props
在 props 初始化完成后,this.props 变量就保存了 props 外部数据对象的地址,在 class 类组件内部就可以通过 this.props 变量访问外部数据。如下:
render(){
return(
<div>
姓名是:{this.props.name}
年龄为:{this.props.age}
<div>
{this.props.children}
</div>
</div>
)
}
3.4 写 props
组件的 props 外部数据一般是由其父组件的 state 内部数据传递过去的,因此在组件内不允许修改 props,想要修改必须通知创建该数据的父元素进行修改。
3.5 props 的作用
1. 接收外部的数据
数据只能读不能写,外部数据只能由父组件传递
2. 接受外部的函数
在恰当的时机调用该函数,该函数也一般是父组件的函数
4. state 内部数据
4.1 初始化 state 内部数据
每个组件都有自己独立的内部数据,因此在类组件中,state 内部数据是放在构造函数中作为私有属性定义的
class Welcome extends React.Component{
/*初始化*/
constructor(){
super();
this.state = {
n: 0
}
}
/*初始化*/
render(){
return (
<div>
/*读数据*/
n: {this.state.n}
/*读数据*/
</div>
)
}
}
4.2 读 state(this.state)
读 state 就是上面的在 render() 函数中使用 this.state
的方式读取。
4.3 写 state(this.setState())
React 的写内部数据 state 就比较绕脑了,我们在前面有介绍,React 是不会像 Vue 一样对数据进行监听,并在数据进行变化时刷新视图的,React 采取的方式是,在数据变化需要更新视图时,重新调用渲染视图的 render 函数,将视图重新渲染。因此需要手动调用 render 函数。
但有了 babel-loader 以后,手动 render 的过程被 babel-loader 做了,babel 提供给 jsx 一些可以自动调用 render 的 API,其中的 写 state 的 API:setState()
就是一个典型的用在类组件中写内部数据的接口,调用该接口,会自动 render。
this.setState(???, fn)
这里的第一个参数可以是新的 state 对象,也可以是一个函数。第二个参数是一个回调函数,会在 setState 成功后回调该函数。
this.setState()
函数会自动将新 state 与旧 state 进行 一级合并,因此只需要传入新 state 中修改的部分属性即可。
class Welcome extends React.Component{
/*初始化*/
constructor(){
super();
this.state = {
n: 0,
m: 0
}
}
/*初始化*/
/*写state*/
add(){
//this.state.n += 1; 该种方式不行,React 不会自动render,必须使用特定的 API
/*
this.state.n += 1;
this.setState(this.state);
*///该种方式不推荐,最好不要改变以前的对象,当数据改变时产生一个新的对象,
//最好遵循数据不可变原则,新对象容纳新的数据。
this.setState({n: this.state.n+1}); // m 会自动 merge
}
/*写state*/
render(){
return (
<div>
/*读数据*/
n: {this.state.n}
/*读数据*/
<button onClick={()=>this.add()}>+1</button>
</div>
)
}
}
PS补充:
实际上,setState 函数是一个异步函数,不是语句执行时就立即执行的,因此会受到一些误解,如:
add(){
this.setState({n: this.state+1});
console.log(this.state.n);//得到的不是加一后的值,是旧的state,因为 setState 是异步的
//不会立马执行,而是在其所在作用域的语句都执行完后再调用执行,因此是先输出再调用的
}
异步函数 setState 的调用时间不确定,因此前端大佬们都统一使用了一种新的方式 setState,就是使用函数
add(){
this.setState(state=>{
return {n: this.state+1});
}//此处的state是传递过来的旧的state对象,返回一个新的对象
this.setState(state=>{
const n = state.n + 1;
console.log(n);//要想输出是修改后的,就直接先修改,再输出修改的值,这才是正确的修改得到的值
//return {n:n};当键和值名字一样时可简写
return {n}
})
}
这种使用函数的方式是在改变的过程中输出改变,而不是在异步函数语句后输出改变,不会混淆,因此,当数据简单时直接用对象,当复杂时用函数
5. 生命周期钩子
所谓生命周期钩子,无论是 react 还是 vue 中的钩子,是指一些特定的函数,这些特定函数会在特定的时间被自动调用。
5.1 componentWillReceiveProps
该钩子是在外部属性 props 里的数据变化时被自动调用的特殊函数,componentWillReceiveProps(newProps, nextContent),第一个参数为新的 props。
该钩子已经被弃用,更名为 UNSAFE_componentWillReceiveProps
。因此平常不用,知道有这个钩子就行。
5.2 constructor
React 组件在创建的时候会调用 constructor() 函数进行数据的初始化,初始化 props 和 state。这里的初始化 state 的语句必须是 this.state = ...
的形式,不能使用 this.setState() 的形式初始化 state。
在 constructor() 函数中还有一个作用就是用来为函数绑定 this 的。官网的介绍。如下:
constructor(){
this.onClick = this.onClick.bind(this);
}
onClick(){
this.setState({
n: this.state.n + 1
})
}
constructor() 函数当只有初始化 props 时,可省略不写。
5.3 render
该钩子用于将 React 元素渲染到根 DOM 节点中,必须要返回一个 React元素。
render() 里面可以使用 if / else,问号表达式(?:)、若想用 for 循环用 map()。
5.4 shouldComponentUpdate
该钩子翻译过来就是,是否更新组件。该函数返回 true 时表示不会阻止 UI 更新,返回 false 表示阻止 UI 更新。
shouldComponentUpdate(newProps, newState)
其作用就是它允许我们手动判断是否要进行组件 UI 更新,我们可以根据应用场景灵活地设置返回值,以避免不必要的更新。
如果不想使用该钩子手动监听判断是否进行组件 UI 更新,其实可以使用 React 提供了一种它帮我们自动检查是否需要重新 render 的判断。在 render 前对新数据和旧数据的每一个属性都进行对比,若全都相等就不再 render,就将类组件改为继承 React.PureComponent
如下:
class A extends React.PureComponent{
//....
}
PureComponent 会在 render 之前对比新 state 和旧 state 的每一个 key,以及新 props 和旧 props 的每一个 key。
如果所有 key 的值全都一样,就不会 render;如果有任何一个 key 的值不同,就会 render。
5.5 componentDidMount
在元素插入到页面后执行的代码,这些代码依赖 DOM,也就是说这些代码执行的前提条件就是元素必须已经出现在页面中了才能正确执行。比如想要获取一个元素的宽度,则必须等到该元素插入到页面中才能获取到。
首次渲染会执行此钩子。
可以在此处发起 AJAX 请求
5.6 componentDidUpdate
该钩子是用于放置视图更新后执行的代码。
首次渲染不会执行此钩子
componentDidUpdate(preProps, preState)
也可以在此处发起 AJAX 请求
5.7 componentWillUnmount
组件将要被移除页面然后被销毁时执行的代码。
unmout 的组件会被移出页面,也会在内存中销毁,因此就不能再次 mount。