《四》组件中的 State、props、Ref

组件实例的有三大核心属性:State、Props、Ref。

类组件中这三大属性都存在。
函数组件没有实例对象。但由于函数可以接收参数,因此有 Props 属性,但是依然没有 State、Ref 属性(在没有 Hooks 的情况下)。

State:

当组件中的一些数据在某些时刻发生变化时,就需要使用 state 来跟踪状态,通过更新组件的 state 来重新渲染组件,更新对应的页面显示

state 是一个对象,包含多个 key:value 的组合。state 是私有的,只有在当前组件内部可以访问、修改 state。

class App extends React.Component {
   state = {
		message: 'Hello,React',
	}

  	render() {
    	return  <h1>{this.state.message}</h1>
	}
}

state 中的数据要保持不可变性,不要直接去修改它。因为在 React.PureComponent 中,当组件更新时,只会对 state 的第一层进行浅比较,这时如果直接修改的是 state 中的某一个引用类型数据,由于引用地址没有发生变化,React.PureComponent 会认为数据没有改变而不会重新执行 render() 方法。可以通过浅拷贝这个引用类型数据来解决。

由于 React.Component 中不管数据有没有变化,只要 setState() 就会重新执行 render() 方法,因此没有这种问题。

class App extends React.PureComponent {
  state = {
    person: {
      name: 'Lee',
    }
  }

  handeleNameChange = () => {
  	 // 解构赋值的 person 和 state 中的 person 指向同一个对象,PureComponent 会认为数据没有变化,不会进行 render()
    const {person} = this.state
    person.name = 'Mary'
    this.setState({person})

	// this.state.person 和 state 中的 person 指向同一个对象,PureComponent 会认为数据没有变化,不会进行 render()
    // this.state.person.name = 'Mary'
    // this.setState({person: this.state.person})

	// 可以通过浅拷贝一个新对象来解决,此处的 person 和 this.state.person 不再指向同一个对象
	const person = {...this.state.person}
    person.name = 'Mary'
    this.setState({person})
  }

  render() {
    const {person} = this.state
    return <h1 onClick={this.handeleNameChange}>{person.name}</h1>
 }
}

setState()

setState() 可以合并对象,更改 state 中的值;并且会重新执行 render() 方法,渲染组件内容。

setState() 是从 React.Component 中继承而来的。
请添加图片描述

有两种写法:

对象式的 setState 是函数式的 setState 的语法糖。
如果新状态不依赖于原状态,使用对象方式;如果新状态依赖于原状态,使用函数方式。

  1. setState(nextState, [callback]):对象式的 setState。
    参数:
    • nextState:将要设置的新状态,该状态会和当前的 state 合并。
    • callback:可选参数。该回调函数会在状态更新完毕,且界面也更新后调用。
    	this.setState({
    		message:'Hello,JS',
    	}, () => {
    		console.log(this.state.message)
    	})
    
  2. setState(updater, [callback]):函数式的 setState。
    参数:
    • updater:是一个函数,可以接收到最新的 state 和 props 作为参数,返回值为将要设置的新状态。
    • callback:可选参数。该回调函数会在状态更新完毕,且界面也更新后调用。
    	this.setState((state, props) => ({
    		message: 'Hello,JS',
    	}), () => {
    		console.log(this.state.message)
    	})
    
class App extends React.Component {
	state = {
		message: 'Hello,React',
	}

  	handleClick = () => {
    	// setState() 做了两件事:合并状态对象,更改 state 中的值;重新执行 render() 方法,渲染组件内容
   	 	this.setState({message: 'Hello,JS'})
 	 }
  
  	render() {
    	return <h1 onClick={this.handleClick}>{this.state.message}</h1>
  	}
}

即使 state 中的值没有更改,但只要调用了 setState(),就会重新执行 render() 方法。

class App extends React.Component {
	state = {
		message: 'Hello,React',
	}

  	handleClick = () => {
    	// 仍然会再次执行 render() 方法
   	 	this.setState({message: 'Hello,React'})
 	 }
  
  	render() {
    	return <h1 onClick={this.handleClick}>{this.state.message}</h1>
  	}
}
不能直接修改 state 的值:

不能直接修改 state 的值,这样无法让界面发生更新。

React 中并没有实现类似于 Vue2 中的 Object.defineProperty() 或者 Vue3 中的 Proxy 的方式来监听数据的变化。
必须通过 setState() 来明确地告知 React 数据已经发生了变化,React 才会再次执行 render() 方法,并且根据最新的 state 来更新界面。

// 正确。setState() 做了两件事:修改 state 中的值;自动重新执行 render() 方法,渲染组件内容
this.setState({message: 'Hello,JS'})

// 错误。不能直接修改 state,因此这样不会重新渲染组件
this.state.message = 'Hello,JS'
setState() 的更新是合并:

setState() 的更新是合并,而不是替换。

React 源码中是通过 Object.assign(this.state, newState) 来实现 setState() 中的新对象和旧 state 对象的合并的。

class App extends React.Component {
	state = {
		message: 'Hello,React',
		content: 'React is a JavaScript library for building user interfaces',
	}

  	handleClick = () => {
  		// {} 在内存中总是会创建一个新的对象,原先的 state 也是一个对象。此处调用 setState() 时创建了一个包含 message 的新对象,但是原先 state 对象中 content 的值也并没有丢失,所以说明更新的这个动作是合并。
   	 	this.setState({message: 'Hello,JS'})
 	}
  
  	render() {
    	return <h1 onClick={this.handleClick}>{this.state.message}</h1>
  	}
}
setState() 的更新是异步的(批处理):

setState() 的更新是异步的。React 会将多个状态更新,聚合到一次 render 中执行,以提升性能,这被称为批处理。 原因是:

  1. 可以显著地提升性能:如果每次调用 setState() 都进行一次更新,意味着 render() 方法会被频繁地调用、界面会被频繁地渲染,效率很低。最好的方法就是获取到多个更新之后进行批量更新。

    具体实现是:调用 setState() 后,会先将要更新的内容放到一个队列中去;等到真的要更新的时候,才会从队列中取出要更新的内容,依次按照顺序进行合并;全部合并完成之后, 调用一次 render() 方法。

  2. 如果同步更新了 state 的值,但是却还没有执行 render() 函数,那么会导致父组件中的 state 的值已经更新了,子组件中接收到的 props 却仍然是旧的,state 和 props 不能保持同步,会在开发中产生很多问题。
class Grandparent extends React.Component {
  state = {
    message:'Hello,React'
  }

  hahandleClickndle = () => {
  	// React 会把多个 setState() 的调用合并成一个调用。点击 h1,setState() 了三次,但是只执行了一次 render() 方法
    this.setState({message: 'Hello,JS'})
    this.setState({message: 'Hello,JS'})
    this.setState({message: 'Hello,JS'})
  }
 
  render() {
    console.log('render')
    return <h1 onClick={this.handleClick}>{this.state.message}</h1>
 }
}
// 错误
this.setState({
  message:'Hello,JS',
})
console.log(this.state.message) //此时获取到的是旧的 message 值

// 正确
this.setState({
  message:'Hello,JS'
}, () => {
	console.log(this.state.message) //此时获取到的是新的 message 值
})

如果有特殊的情况确实需要依次更新 state,可以使用 flushSync 跳过批处理,强制同步执行。

import {flushSync} from 'react-dom'

class App extends React.Component {
  state = {
    message:'Hello,React'
  }

  handleClick = () => {
    flushSync(() => {
      this.setState({message: 'Hello,JS'})
    })
    console.log(this.state.message) // 'Hello,JS'
  }

  render() {
    return <h1 onClick={this.handleClick}>{this.state.message}</h1>
 }
}

React18 之后, setState() 的更新一定是异步的。
React18 之前,如果 setState() 不是由 React 触发的回调执行的,将是同步的(例如:setTimeout() 、原生 DOM 事件中、Promise.then() 中);其他时候则是异步的(例如:组件的生命周期中或者 React 的合成事件中)。

class App extends React.Component {
 state = {
  message:'Hello,React'
 }

 componentDidMount() {
   // 生命周期函数是由 React 内部调用的,此处的 setState() 是由 React 触发的回调执行的
   this.setState({message: 'Hello,JS'})
   console.log(this.state.message) // 'Hello,React'。异步,获取到的仍然是旧的 state

   const dom = document.getElementById('h')
   dom.onclick = () => {
      // 此处的 setState() 在原生 DOM 事件的回调函数里,是由浏览器执行的
     this.setState({message: 'Hello,JS'})
     console.log(this.state.message) // 'Hello,JS'。同步,获取到的是新的 state
   }
  }
 
 render() {
   return <h1 id='h'>{this.state.message}</h1>
  )
 }
}
class App extends React.Component {
 state = {
  message:'Hello,React'
 }

 handleClick = () => {
   // handleClick 是由 React 合成事件调用的,此处的 setState() 是由 React 触发的回调执行的
   this.setState({message: 'Hello,JS'})
   console.log(this.state.message) // 'Hello,React'。异步,获取到的仍然是旧的 state

   setTimeout(() => {
     // 此处的 setState() 在 setTimeout() 的回调函数里,是由浏览器执行的
     this.setState({message: 'Hello,JS'})
     console.log(this.state.message) // 'Hello,JS'。同步,获取到的是新的 state
   }, 100)
 }
 
 render() {
   return <h1 onClick={this.handleClick}>{this.state.message}</h1>
  )
 }
}

React 中的 State 有三种状态管理的方式:

  1. 组件自己管理自己的 State。
  2. 使用 Context 共享状态。
  3. 使用 Redux 管理应用的状态。

Props:

当 React 元素是自定义组件时,它会将接收到的标签属性及子组件转换为单个对象传递到组件内部,这个对象被称之为 props。

props 是组件外部向组件内部传递的数据,是只读的。

state 和 props 的区别:

  1. props 由父组件传入,而 state 由组件本身管理。
  2. 组件不能修改 props,但可以修改 state。
<Person name='Lee' age={(function(){return 19})()} sex={<li>性别:男</li>} />

// 也可以批量传递标签属性。原生 JS 中扩展运算符是不能展开对象的。由于 React 和 Babel 的原因,扩展运算符可以展开对象,但仅仅适用于标签属性的传递,别的地方不支持。
<Person {...{
	name: 'Lee',
	age: (function(){return 19})(),
	sex: <li>性别:男</li>
}} />
// 类组件
class Person extends React.Component{
	render(){
		const {name, age, sex} = this.props
		return (
			<ul>
				<li>姓名:{name}</li>
				<li>年龄:{age}</li>
				{sex}
			</ul>
		)
	}
}
// 函数组件
function Person(props){
	const {name, age, sex} = props
	return (
		<ul>
			<li>姓名:{name}</li>
			<li>年龄:{age}</li>
			{sex}
		</ul>
	)
}

props.children

组件内部通过 props.children 可以获取到调用组件时的开始标签和结束标签之间的内容。

props.children 的值有四种类型:

  1. undefined:调用组件时没有子节点。
  2. 字符串值:调用组件时有一个文本节点。
  3. object:调用组件时有一个 ReactElement 节点。
  4. array:调用组件时有多个 ReactElement 节点。
import Child from './child'
class Parent extends React.Component {
  render() {
    return (
      <div>
        <Child>
          这是 children 的内容
        </Child>
        // 不写标签体,写成 children 标签属性也可以
        <Child children='这是 children 的内容' />
      </div>
    )
  }
}

class Child extends Component {
  render() {
    return (
      {/* 通过 props.children 可以获取到组件的开始标签和结束标签之间的内容 */}
      <div>{this.props.children}</div>
    )
  }
}

使用 defaultProps 设置默认值:

可以通过 defaultProps 属性来设置 props 中的默认值。当某个 prop 值没有被传入时,就会使用默认值。

// 给组件添加 defaultProps 属性
Person.defaultProps = {
  name: 'Lee'
}


// 类组件中还可以将 defaultProps 声明为静态属性
class Person extends React.Component{
	static defaultProps = {
	  name: 'Lee'
	}
}

使用 propTypes 进行类型检查:

可以通过 propTypes 属性来检查组件接收到的数据类型是否正确。当传入的 prop 值类型不正确时,JavaScript 控制台将会显示警告。

propTypes 类型检查发生在 defaultProps 赋值后,所以类型检查也适用于 defaultProps。
出于性能方面的考虑,propTypes 仅在开发模式下进行检查。

// 自 React v15.5 起,React.PropTypes 已移入 prop-types 包中,使用需要单独引入,引入之后全局就会有 PropTypes 对象,PropTypes 对象提供了一系列验证器
import PropTypes from 'prop-types'

// 给组件添加 propTypes 属性
Person.propTypes = {
    name: PropTypes.string.isRequired,
    speak: PropTypes.func,
}

// 类组件中还可以将 propTypes 声明为静态属性
class Person extends React.Component{
	static propTypes = {
	  name: PropTypes.string.isRequired, 
	}
}

Ref:

通过 Ref 可以获取到原生 DOM 和类组件实例。不要过度使用 Ref。

当 ref 用于 HTML 元素上时,获取到的是底层的 DOM 元素。
当 ref 用于类组件时,获取到的组件实例对象。

因为函数组件没有实例对象,因此无法通过 ref 来获取函数组件。

class Parent extends React.PureComponent {
 childRef = React.createRef()

 handleClick = () => {
   console.log(this.childRef.current) // null
 }

 render() {
   return (
     <>
       <Child ref={this.childRef} /> // 错误
       <button onClick={this.handleClick}>点击获取函数组件</button>
     </>
   )
 }
}

const Child = props => {
 return <h2>这是 Child 组件</h2>
}
export default React.memo(Child)

请添加图片描述

Ant Design 中很多组件都获取不到 ref,可以包裹或内嵌一层自己创建的元素以获取 ref。

通过 Ref 获取原生 DOM 和类组件实例有三种方式:

  1. 字符串形式的 Ref:在 React 元素上绑定一个 ref 字符串,通过 this.refs.ref字符串 即可获取到 React 元素。

    React 不推荐使用字符串形式的 ref,它已过时并可能会在未来的版本中被移除,这种方式存在一些效率上的问题。

    class App extends React.PureComponent {
      handleClick = () => {
      	// 获取 ref
        console.log(this.refs.h1Node) // <h1>标题</h1>
      }
    
      render() {
      	// 绑定 ref
        return <h1 ref='h1Node' onClick={this.handleClick}>标题</h1>
     }
    }
    
  2. 回调函数形式的 Ref:给 React 元素的 ref 属性绑定一个回调函数,那么当元素被渲染之后,回调函数会被执行,且会将当前节点作为其参数传入。
    class App extends React.PureComponent {
      handleClick = () => {
        // 获取 ref
        console.log(this.titleRef)// <h1>标题</h1>
      }
    
      render() {
        // 绑定 ref
        return <h1 ref={el => this.titleRef = el} onClick={this.handleClick}>标题</h1>
      }
    }
    
  3. React.createRef():使用 React.createRef() 创建一个 ref 对象后将其绑定到 React 元素上,通过 ref 对象.current 即可获取到 React 元素。推荐使用。
    class Parent extends React.PureComponent {
       // 创建一个 ref 对象
      childRef = React.createRef()
    
      handleClick = () => {
        // 获取 ref
        // 获取类组件实例后即可访问其实例属性、调用实例方法等
        console.log(this.childRef.current)
      }
    
      render() {
        return (
          <>
            {/* 绑定 ref */}
            <Child ref={this.childRef} />
            <button onClick={this.handleClick}>点击获取子组件实例</button>
          </>
        )
      }
    }
    	
    class Child extends React.PureComponent {
      render() {
        return <h2>这是 Child 组件</h2>
      }
    }
    

Ref 转发:

可以通过 Ref 转发来访问函数组件内部的 React 元素。

React.forwardRef() 是一个高阶函数,可以使用 React.forwardRef() 包裹匿名函数组件,匿名函数组件将会接收到转发过来的 ref 作为其第二个参数,可以将其向下传递给子组件。

class Parent extends React.PureComponent {
  // 1. 创建一个 ref 对象
  hRef = React.createRef()

  handleClick = () => {
    // 5. 获取 ref
    console.log(this.hRef.current)
  }

  render() {
    return (
      <>
        {/* 2. 传递 ref */}
        <Child ref={this.hRef} />
        <button onClick={this.handleClick}>点击获取子组件中的元素</button>
      </>
    )
  }
}

// 3. 接收 ref
const Child = React.forwardRef(
  (props, ref) => {
    // 4. 绑定 ref
    return <h2 ref={ref}>这是 Child 组件</h2>
  }
)
export default React.memo(Child)
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值