组件实例的有三大核心属性: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 的语法糖。
如果新状态不依赖于原状态,使用对象方式;如果新状态依赖于原状态,使用函数方式。
setState(nextState, [callback])
:对象式的 setState。
参数:- nextState:将要设置的新状态,该状态会和当前的 state 合并。
- callback:可选参数。该回调函数会在状态更新完毕,且界面也更新后调用。
this.setState({ message:'Hello,JS', }, () => { console.log(this.state.message) })
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 中执行,以提升性能,这被称为批处理。 原因是:
- 可以显著地提升性能:如果每次调用
setState()
都进行一次更新,意味着render()
方法会被频繁地调用、界面会被频繁地渲染,效率很低。最好的方法就是获取到多个更新之后进行批量更新。具体实现是:调用
setState()
后,会先将要更新的内容放到一个队列中去;等到真的要更新的时候,才会从队列中取出要更新的内容,依次按照顺序进行合并;全部合并完成之后, 调用一次render()
方法。 - 如果同步更新了 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 有三种状态管理的方式:
- 组件自己管理自己的 State。
- 使用 Context 共享状态。
- 使用 Redux 管理应用的状态。
Props:
当 React 元素是自定义组件时,它会将接收到的标签属性及子组件转换为单个对象传递到组件内部,这个对象被称之为 props。
props 是组件外部向组件内部传递的数据,是只读的。
state 和 props 的区别:
- props 由父组件传入,而 state 由组件本身管理。
- 组件不能修改 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
的值有四种类型:
- undefined:调用组件时没有子节点。
- 字符串值:调用组件时有一个文本节点。
- object:调用组件时有一个 ReactElement 节点。
- 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 和类组件实例有三种方式:
- 字符串形式的 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> } }
- 回调函数形式的 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> } }
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)