React组件实例三大核心属性State props Refs详解

React
353
0
0
2023-06-26
目录
  • 组件组件实例的三大核心属性-State
  • State 不可以直接修改
  • State 的更新是合并
  • State 的更新可能是异步的
  • 组件实例对象的三大核心属性-Props
  • props.children
  • 使用defaultProps设置默认的prop值
  • 使用propTypes进行类型检查
  • 组件实例对象的三大核心属性-Refs
  • 字符串形式的Ref
  • 回调函数形式的Ref
  • createRef
  • 访问Refs
  • Refs 转发

组件组件实例的三大核心属性-State

状态 state 是组件实例对象最重要的属性之一,它的值是一个对象,可以包含多个 key-value 的组合。

当组件中的一些数据在某些时刻发生变化时,就需要使用 state 来跟踪状态。state 是私有的,并且完全受控于当前组件,除了拥有并设置了它的组件,其他组件都无法访问。

组件被称为状态机,通过更新组件的 state 来重新渲染组件,更新对应的页面显示。

this.props 和 this.state 是 React 本身设置的,且都拥有特殊的含义,但是其实可以向类组件中随意添加不参与数据流的额外字段(比如 this.timerID)。

state 和 props 之间最重要的区别是:

  • props 由父组件传入,而 state 由组件本身管理。
  • 组件不能修改 props,但可以修改 state。
class Weather extends React.Component{
	constructor(props) {
	    super(props)
	    // 初始化 state
	    this.state = {
	      isHot: false,
	    }
	}
	// state可以简写成如下形式
	// 原因是:类中可以直接写赋值语句,实际上就是直接给实例对象上添加属性
	state = {
		isHot: false,
	}
	componentDidMount() {
	    // 更改 state
	    this.setState({
	        isHot: !this.state.isHot,
	     })
	  }
	 ...
}

State 不可以直接修改

初始化 state 之后,在其他地方不可以直接修改 state,而是应该使用 React 内置的一个 API:setState() 来修改。

constructor() 只初始化的时候调用一次。

render() 会调用 1+n 次,1是初始化,n 是状态更新的次数(也就是说,每次 setState() 之后, React 都会调用一次 render())。

// Wrong,此代码不会重新渲染组件
this.state.comment = 'Hello';
// Correct
this.setState({comment: 'Hello'});

setState() 有两种写法:

setState(nextState, [callback]):对象式的 setState。

参数:

  • nextState:将要设置的新状态,该状态会和当前的 state 合并。
  • callback:可选参数,回调函数。该函数会在状态更新完毕,且界面也更新后(render() 后)调用。
	this.setState({
		count: this.state.count +,
	}, () => {
		console.log(this.state.count)
	})

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

参数:

  • updater:是一个函数,可以接收到 state 和 props 作为参数,返回值为将要设置的新状态。
  • callback:可选参数,回调函数。该函数会在状态更新完毕,且界面也更新后(render() 后)调用。
	this.setState((state, props) => ({
		count: state.count +,
	}), () => {
		console.log(this.state.count)
	})

对象式的 setState 是函数式的 setState 的简写方式(语法糖)。这两种写法的使用原则:

如果新状态不依赖于原状态,使用对象方式;如果新状态依赖于原状态,使用函数方式。如果需要在 setState() 执行后获取最新的状态数据,要在第二个参数 callback 函数中读取。

State 的更新是合并

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

constructor(props) {
    super(props);
    // state 包含几个独立的变量
    this.state = {
      isHot: false,
       wind: '微风',
    }
  }
componentDidMount() {
    // 此处调用 setState() 更新了 isHot 的值,但是 wind 的值也并没有丢失,所以说明更新的这个动作是合并
    this.setState({
        isHot: !this.state.isHot,
     })
  }

State 的更新可能是异步的

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

因为 this.props 和 this.state 可能会异步更新,所以不要依赖他们的值来更新下一个状态。要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。

// Wrong
this.setState({
  count: this.state.count +,
})
console.log(this.state.counter) //此时直接读取获取到的仍然是旧的 count 值
// Correct
this.setState({
  count: this.state.count +,
}, () => {
	console.log(this.state.counter) //此时读取获取到的是新的 count 值
})

组件实例对象的三大核心属性-Props

当 React 元素为用户的自定义组件时,它会将所接收的标签属性及子组件转换为单个对象传递给组件,这个对象被称之为 “props”。

props 是 React 组件的输入。它们是组件外部向组件内部传递变化的数据。

props 是只读的,组件无论是使用函数组件还是类组件,都决不能修改自身的 props。

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

props.children

每个组件都可以获取到 props.children,它包含组件的开始标签和结束标签之间的内容。

<Welcome>Hello world!</Welcome>
// 不写标签体,写成 children 标签属性也可以
<Welcome children='Hello world!'></Welcome>
// 在 Welcome 组件中获取 props.children,就可以得到字符串 Hello world!
function Welcome(props) {
  return <p>{props.children}</p>;
}

使用defaultProps设置默认的prop值

可以通过配置特定的 defaultProps 属性来定义 props 的默认值。

// 给组件加上 defaultProps 的属性
Person.defaultProps = {
  title: '我是详情'
}
// 简写:简写的这种方式只适用于类组件,因为函数式组件中是没有 static 的
class Person extends React.Component{
	static defaultProps = {
	  title: '我是详情'
	}
}

使用propTypes进行类型检查

PropTypes 提供一系列验证器,可用于确保组件接收到的数据类型是有效的。当传入的 prop 值类型不正确时,JavaScript 控制台将会显示警告。

propTypes 类型检查发生在 defaultProps 赋值后,所以类型检查也适用于 defaultProps。

出于性能方面的考虑,propTypes 仅在开发模式下进行检查。

// 只要给组件加上 propTypes 属性,React 就会认为是在加规则
Person.propTypes = {
  // React.PropTypes 是 React 内置的属性
  title: React.PropTypes.string.isRequired, // 错误
   // 自 React v.5 起,React.PropTypes 已移入另一个包中,需要的话要使用`import PropTypes from 'prop-types'`引入,引入之后全局就会有了一个对象 PropTypes
    title: PropTypes.string.isRequired, // 正确
    speak: PropTypes.func,
}
/// 简写:简写的这种方式只适用于类组件,因为函数式组件中是没有 static 的
class Person extends React.Component{
	static propTypes = {
	  title: PropTypes.string.isRequired, 
	}
}

组件实例对象的三大核心属性-Refs

PS:勿过度使用 Refs

组件内的标签可以定义 ref 属性来标识自己,都会被收集到组件实例对象的 refs 属性下,这样,通过 this.refs.ref属性 就可以访问到 ref 当前所处的真实节点。

无法在函数式组件上使用 ref 属性。

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

字符串形式的Ref

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

class Demo extends React.Component {
  showData = () => {
     console.log(this) // 打印可以看到组件的实例对象上 this 有 refs 属性,属性值是 key-value 的对象 ,其中有一个key 就是 input,value 是 ref 当前所处的真实节点。
	// 访问 refs
     alert(this.res.input.value)
  }
  render() {
    return (
    	<div>
    	    // 创建、绑定 refs
    		<input ref="input" />
    		<button onClick={this.showData}>点击</button>
    	</div>
    )
  }
}

回调函数形式的Ref

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,第二次才会传入 DOM 元素。这是因为在每次 render 渲染时都会创建一个新的函数实例,所以 React 会首先清空旧的 ref,然后才会设置新的。这个问题大多数情况下是无关紧要的。

初次渲染时不会,因为初次渲染时没有旧的 ref 需要去清空。

class Demo extends React.Component {
  showData = () => {
	// 访问 refs
     alert(this.input.value)
  }
  render() {
    return (
    	<div>
    	    // 创建、绑定 refs
    	    // render 方法执行的时候会自动调用 ref 的回调函数,并且会把当前所处的真实节点作为参数传递进去,然后将这个节点赋值给组件实例自身的一个自定义属性上
    		<input ref={c => this.input = c} />    
    		<button onClick={this.showData}>点击</button>
    	</div>
    )
  }
}

通过将 ref 的回调函数定义成类的绑定函数的方式可以避免上述问题。

class Demo extends React.Component {
 setInputRef = (c) => {
 	// 绑定 refs
 	this.input = c
 }
  showData = () => {
	// 访问 refs
     alert(this.input.value)
  }
  render() {
    return (
    	<div>
    	    // 创建 refs
    	    // 更新时也不会重复触发 setInputRef,因为它已经放在实例自身了
    		<input ref={this.setInputRef} />    
    		<button onClick={this.showData}>点击</button>
    	</div>
    )
  }
}

createRef

React.createRef() 是 React 内置的一个 API,调用后可以返回一个容器,该容器存储被 ref 所标识的节点。该容器是专人专用的。

class Demo extends React.Component {
   // 创建 refs
  myRef = React.createRef()
  showData = () => {
  	// 访问 refs
     alert(this.myRef.current.value)
  }
   render() {
     return (
    	<div>
    	    // 绑定 refs
    	    // 下面一行代码在执行的时候,React 发现了 ref 属性,并且发现属性值是用 createRef 创建出来的一个容器,这时, React 会把当前 ref 所在的那个节点直接存储到那个容器里面
    		<input ref={this.myRef} />    
    		<button onClick={this.showData}>点击</button>
    	</div>
    )
  }
}

访问Refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

ref 的值根据节点

Refs 转发

Refs 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递给子组件。

ref 转发不仅限于 DOM 组件,也可以转发 refs 到 class 组件实例。

const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

FancyButton 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button。这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。

上述代码的执行步骤如下:

  • 通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量;
  • 通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>;
  • React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数;
  • 向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性;
  • 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点;