在平时的工作中,自己开发一般都是写着react16.8 提供的hooks写法,也是非常的好用。但是呢? 在最近对老的项目新需求开发,是使用的react的类组件的写法,突然感觉对类组件的知识有点模糊了,好久没有使用了(说明自己的基础不是很扎实)。凑着周末的时间,赶紧抓紧时间来学习一波,回顾react的类组件的写法。
react类组件的constructor函数
ES6中的constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。
那么在react类组价中呢? constructor函数到底写不写?如果不写,在内部会自动添加一个constructor函数。那如果写的话,又有哪些注意事项呢?
解答一:在类组件中写不写constructor函数?
这个一般取决于组件要不要定义属于自己的专属state状态,如果需要有专属的state状态,那么就需要写constructor函数(其实这种说法,也不对, 因为针对实例属性,在react内部中自动添加了state属性,那么就可以把state属性单独提出了),简单来说,就是看你自己的兴趣和习惯。
export default class NoState extends Component {
// 这里没有状态,就不需要使用contructor函数。
render() {
return <div>321321</div>
}
}
export default class HasState extends Component {
constructor(props) {
super(props)
this.state = { // 这里是有状态值
message: ''
}
}
render() {
return <div>321321</div>
}
}
注意事项: 只要写了constructor函数,就一定要写super函数。不写super,this的指向就有问题。
ES5的继承: 实质上是先创造子类的实例对象this,然后再将父类的方法添加到this上面去(Parent.apply(this))
ES6的继承机制:先创造父类的实例对象this,(必须先调用super方法),然后再用子类的构造函数修改this。
在上面的有状态的组件中,因为实例对象中,默认有state,props属性,那么就可以直接把state提出来使用
export default class HasState extends Component {
state = {
message: ''
}
render() {
return <div>321321</div>
}
}
所以,到底要不要写constructor函数,取决于自己的习惯。
解答二:super() 和 super(props)的区别
如果没有写constructor函数,react内部已经添加。如果写了constructor函数,就必须调用super()函数,这样才能继承React.Component类。
所以,在调用super函数的时候到底传不传递 参数props呢?
记住:想在constructor构造函数中,使用this.props,就传递参数;如果不在构造函数中使用this.props,就可以不写。 传递还是不传递,就是看constructor中需要使用不。
export default class Text extends Component {
constructor() {
super()
console.log(this.props) // undefined
}
}
export default class Text extends Component {
constructor(props) {
super(props)
console.log(this.props) // {}
}
}
解答三:如果写super(),那么生命周期中,可以拿到props吗 constructor构造函数也相当于一个生命周期,最开始的时候执行。
那么,如果在写super(), 在构造函数中,拿不到this.props中,那么其他的生命周期呢?
super中的props是否接收,只能影响constructor生命周期能否使用this.props,其他的生命周期已默认存在this.props
react类组件中 this的指向
this的指向,在JavaScript中一直是个难点。需要多花点时间去学习理解。 在react中的类组件定义的时候,是使用 ES6 提供的class写法, 构建一个构造函数。
那么class中的方法中的this是指向什么呢? 指向的是构造函数的实例对象
class Person {
constructor(name) {
this.name = name
}
eat() {
console.log(this)
}
}
const p = new Person('james')
p.eat() // this指向p对象
p.eat() 实例对象p调用了eat方法,所以this指向了p,这里应该没有问题。那么下面的这种情况呢?
const p = new Person('james')
const fn = p.eat
fn()
上面的代码,很好理解,通过实例对象p找到eat方法(这里并没有调用,this的指向是在函数调用的时候才确定的)。
简单来说,就栈中的一个变量 fn 指向了堆内存中的 一个内存空间。
fn() fn函数执行。如果研究过this的指向问题,这是属于函数的直接调用, this的指向是window,没有什么疑问。
但是,这里eat是 class类中的方法,class类中的方法有一个特点:类中的方法开启了局部的严格模式,严格模式下的this,就是undefined。
所以,在上面代码fn()代码执行,eat方法中打印的this就是undefined。
理解上面的this是指向undefined是非常非常的重要,在下面的react组件中,会充分的体现出来。
下面来看看,react组件
// 简单的一个测试组件
export default class Test extends Component {
constructor(props) {
super(props)
this.state = {
message: '测试组件'
}
}
changeMessage() {
console.log(this) // 这里的this是什么呢?
// 下面的代码执行会报错吗?
this.setState({
message: '信息改变'
})
}
render() {
return (
<div onClick={this.changeMessage}>{ this.state.message }</div>
)
}
}
解惑:上面react组件中的changeMessage方法中的this指向的是什么?
一步一步分析:
constructor中的this,毫无疑问就是 构造函数的实例对象
render中的this, render方法是UI的渲染,在源码中就是通过 new的形式去调用的render,所以render中的this也是指向实例对象。
changeMessage中的this呢?
理解:
第一步:在栈中创建了一个changeMessage的变量, 在堆内存中开辟了一块空间,来保存函数体。
第二步:执行 onClick={this.changeMessage}, 这里不就是引用赋值嘛,把changeMessage指向的堆内存中的地址,赋值给onClick,然后执行onClick事件,就执行堆中的函数体。
那么,是不是就很容易确定,this的值为undefined嘛。因为onClick的事件调用属于函数的直接调用,但是呢又由于类中的方法,开启了函数的局部模式,所以this是undefined。
结论: 类组件中的constructor 和 render函数中的this,是实例对象,其他类中的方法的this是undefined。(我不知道,这样说对不对,当然除了箭头函数)。
react类组件中方法定义
export default class Test extends Component {
constructor(props) {
super(props)
this.state = {
message: '测试组件'
}
}
changeMessage() {
console.log(this) // undefined
}
render() {
return (
<div onClick={this.changeMessage}>{ this.state.message }</div>
)
}
}
在上面的changeMessage方法中,this为undefined,那么想要调用 changeMessage方法,使用this,该怎么做呢?
方式一:bind改变this的指向一(构造函数中定义实例方法)
export default class Test extends Component {
constructor(props) {
super(props)
// 实例方法changeMessage
this.changeMessage = this.changeMessage.bind(this)
}
// 原型上的方法
changeMessage() {
console.log(this)
}
render() {
return (
<div onClick={this.changeMessage}>{ this.state.message }</div>
)
}
}
在构造函数中,使用 this.changeMessage = this.changeMessage.bind(this)把原型上的方法,绑定在实例对象上。
那么onClick点击事件changeMessage就是实例对象上的changeMessage(原型链查找规则:如果自身有,就是使用自身的,如果没有就去查找原型链上的方法)
缺点:
- 多定义了一个实例方法,写法麻烦
- 不能传递参数
方式二:bind改变this的指向二(直接在jsx中改变this)
export default class Test extends Component {
state = {
message: 'james'
}
changeMessage() {
console.log(this)
}
render() {
return (
<div onClick={this.changeMessage.bind(this)}>{ this.state.message }</div>
)
}
}
可以知道render方法中的this,就是指向实例对象。那么就可以直接在jsx中绑定this。onClick={this.changeMessage.bind(this)}
优点
- 可以传递参数
方式三:利用箭头函数寻找上层this的规则(jsx中)
export default class Test extends Component {
state = {
message: 'james'
}
changeMessage() {
console.log(this)
}
render() {
return (
<div onClick={() => this.changeMessage()}>{ this.state.message }</div>
)
}
}
onClick={() => this.changeMessage()} 这里的this是指向实例对象, 实例对象上是没有changeMessage的,从而查找原型上,如果有,就使用原型上的changeMessage方法。
changeMessage是一个箭头函数,this在定义的时候,就已经确定,查找最近上层的this指向,就是实例对象。
方式四:利用箭头函数寻找上层this的规则(实验性语法)
export default class Test extends Component {
state = {
message: 'james'
}
// 直接使用箭头函数来定义方法
changeMessage = () => {
console.log(this)
}
render() {
return (
<div onClick={this.changeMessage}>{ this.state.message }</div>
)
}
}
这里的changeMessage 跟上面的state是一回事了,都是赋值语句。state被赋值了一个对象,changeMessage被赋值了一个箭头函数。
state绑定在实例对象上, 那么changeMessage也绑定在实例对象上。
因为是箭头函数,this就是指向实例对象。
总结方法绑定的位置:
- 方式一:实例对象和原型对象都绑定一个相同功能的函数
- 方式二:方法绑定在原型上
- 方式三:方法绑定在原型上
- 方式四:方法绑定在实例对象上
在上面的几种方式中,习惯使用三四种,写法方便一点。
react 类组件中的ref
在类组件中,ref 就是用来获取DOM节点信息的,(功能类似于id一样的效果)
方式一:refs形式(已废弃)
在类组件的实例对象上:refs
context: {}
props: {}
refs: {}
state: {message: '测试组件'}
updater: {isMounted: ƒ, enqueueSetState: ƒ, enqueueReplaceState: ƒ, enqueueForceUpdate: ƒ}
_reactInternalInstance: {_processChildContext: ƒ}
_reactInternals: FiberNode {tag: 1, key: null, stateNode: Test, elementType: ƒ, type: ƒ, …}
isMounted: (...)
replaceState: (...)
refs就是用来保存定义的ref
export default class Test extends Component<any, any> {
btn = () => {
console.log(this.refs) // {btn: Node}
}
render() {
return (
<button ref="btn" onClick={this.btn}>点击</button>
)
}
}
this.refs 就是用来保存 ref 节点的,算了,不说了。 refs这个对象,已经被废弃了(不想了)
方式二:回调形式的ref
// 内联回调函数的形式
export default class Test extends Component<any, any> {
btnNode: HTMLButtonElement | null;
constructor(props: any) {
super(props);
this.btnNode = null;
}
btn = () => {
console.log(this.btnNode)
}
render() {
return (
<button ref={ (node) => this.btnNode = node } onClick={this.btn}>点击</button>
)
}
}
ref 的属性值,是一个回调函数,在render渲染的时候,会自动执行。
参数node 就是 ref 当前处于的DOM节点,然后利用实例对象的一个属性(btnNode)来保存当前的节点。这样,直接拿取实例对象的一个属性值,就拿到所绑定的节点。
react:关于回调refs的说明
如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
内联回调函数的形式,在更新的过程中会执行两次ref的回调函数。第一次,用来清除上一次绑定的ref,所以传递的值为null; 第二次,就是用来重新保存DOM节点。
那么该如何解决呢?
使用绑定函数的方式,就能解决。
export default class Test extends Component<any, any> {
btnNode: HTMLButtonElement | null;
constructor(props: any) {
super(props);
this.btnNode = null;
}
btn = () => {
console.log(this.btnNode)
}
// 绑定函数
saveRef = (node: HTMLButtonElement) => {
this.btnNode = node
}
render() {
return (
<button ref={ (node) => this.btnNode = node } onClick={this.btn}>点击</button>
)
}
}
这样,saveRef方法就始终在实例对象中,重新render的时候,就不会重新ref绑定的函数。那么,ref只有第一次执行render的时候,会执行。
上面就是回调函数ref形式,两种的写法,区别也在这里。
但是大多数情况下,内联函数执行两次是无关紧要的。一般的写法,还是内联函数的形式。
方式三: createRef形式
React 16.3 版本引入的 React.createRef() API。如果react版本高于16.3,就推荐使用createRef,低于的话,就推荐使用回调函数的ref。
export default class Test extends Component<any, any> {
btnNode: {current: HTMLButtonElement | null}
constructor(props: any) {
super(props);
this.btnNode = React.createRef<HTMLButtonElement>()
}
btn = () => {
// 打印出节点
console.log(this.btnNode.current)
}
render() {
return (
<button ref={this.btnNode} onClick={this.btn}>点击</button>
)
}
}
createRef创建的容器保存在实例对象的属性上,这样在整个组件都能引用它们。
- 当 ref 属性用于HTML元素时,构造函数中使用React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。(
原生DOM) - 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。(
class组件) - 你不能在函数组件上使用 ref 属性,因为他们没有实例。(
函数组件)
react 类组件中的生命周期(新)
在 react 16.3提出新的生命周期,16.4就完成了。
即将废弃的生命周期:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
类组件的生命周期,分为三个阶段: 挂载阶段、更新阶段和 卸载阶段。
挂载阶段;
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
上面的几个生命周期,执行顺序,依次排序。
getDerivedStateFromProps
是一个静态函数,需要加上static
语义:
从 props 中获取 state。
作用: 为了让props能更新到组件内部的state,在render之前调用,在初始的时候挂载。
参数: 第一个参数:props, 第二个参数:state; 不能通过this来拿取state中的值
返回值: 如果props对state有影响,返回新的state;如果没有影响,就返回null。
export default class Test extends Component<any, any> {
constructor(props: any) {
super(props);
}
static getDerivedStateFromProps(props: any, state: any) {
console.log(props, state)
return null;
}
render() {
return (
<button>点击</button>
)
}
}
更新阶段;
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
顺序排序。
shouldComponentUpdate
语义: 是否应该更新组件。
作用:
阻止一些无用的state和props的更改,来使组件 reRender,造成性能浪费。
参数: 第一个参数:nextProps, 第二个参数:nextState
返回值: boolean值。如果为true更新组件,为false则不更新组件
export default class Test extends Component<any, any> {
constructor(props: any) {
super(props);
}
shouldComponenetUpdate(nextProps: any, nextState: any) {
// 比较逻辑
return true;
}
render() {
return (
<button>点击</button>
)
}
}
getSnapshotBeforeUpdate
语义: Snapshot: 快照。 在更新之前,检查一下
作用: 在render函数后,获取一些DOM信息(滚动信息)等等。然后返回值,给componentWillUpdate提示,作出相应的动作。
参数: 第一个参数:prevProps, 第二个参数:prevState
返回值: any。返回任意类型的值,componentDidUpdate根据返回值,更新。
export default class Test extends Component<any, any> {
constructor(props: any) {
super(props);
}
static getSnapshotBeforeUpdate(prevProps: any, prevState: any) {
// 表示没有滚动到底部(随便写的)
if (this.state.height !== prevProps.height) {
return 'not-bottom'
}
return false
}
render() {
return (
<button>点击</button>
)
}
}
componentDidUpdate
语义: 组件更新完成
作用: 组件更新的最后一步(更新完成之后,想要的逻辑操作)
参数: 第一个参数:prevProps, 第二个参数:prevState,第三个参数:getSnapshotBeforeUpdate的返回值
export default class Test extends Component<any, any> {
constructor(props: any) {
super(props);
}
componentDidUpdate(prevProps: any, prevState: any, snapShot: any) {
// 举例
if (snapShot === 'not-bottom') {
// 一些列的逻辑操作
}
}
render() {
return (
<button>点击</button>
)
}
}
卸载阶段:
- componentWillUnmount
总结:
类组件的生命周期,就大概就三个常用的,
- componentDidMount: 组件挂载完毕
- componentDidUpdate: 组件更新完毕
- shouldComponentUpdate: 组件是否要更新(性能优化)
其他的生命周期,知道就行了。
react 类组件中的ts定义
export default class Text extends Component<P, S> {
}
P: 对props的限定
S: 对state的限定
收尾总结
这个周末。两个下午的时间都在总结class中的一些知识点(早上的啦,睡懒觉的啦),当然还有很多高级的知识点没有涉及到。我想的是,先了解基础就行了,可以看懂class的写法,就OK了。现在及以后肯定是hooks开发,class写法很少了,当然不排除以后要写。总结的知识点大致就是这样,如果以后有新的知识,就继续往下面下吧,继续更新中。。。