> 文章列表 > React教程详解一(props、state、refs、生命周期)

React教程详解一(props、state、refs、生命周期)

React教程详解一(props、state、refs、生命周期)

文章略长,耐心读完,受益匪浅哦~

 目录

前言

简介

JSX

面向组件编程

state

props

refs

组件生命周期


前言

简介

React框架由Facebook开发,和Vue框架一样,都是用于构建用户界面的JavaScript库;

它有如下三个特点:

  1. 采用组件化模式,声明式编程,提高开发效率及组件复用率
  2. 在React Native中可以使用React语法进行移动端开发
  3. 使用虚拟DOM和优秀的Diffing算法,尽量减少与真实DOM的交互;

如下是引入react的简单尝试:

// test.html
<body><!-- 引入react核心库 --><script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <!-- 引入react-dom库,用于支持react操作DOM --><script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script><!-- 引入babel,用于将jsx解析为js --><script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script><div id="test"></div><!-- 记住这里的类型是text/babel,下面写的是jsx语法 --><script type="text/babel">// 创建虚拟DOMconst VDOM = (<div><h1>React初尝试</h1></div>)ReactDOM.render(VDOM, document.getElementById('test')) // 渲染虚拟DOM到页面</script>
</body>

JSX

JSX(JavaScript XML)是react定义的一种类似于XML的js扩展语法,用于简化创建虚拟DOM的过程;下面列出一些简单语法:

  • 定义虚拟DOM时,不要写引号;
  • 标签中混入JS表达式时要用{ };
  • 样式的类名指定要用className;
  • 内联样式,要用style={{key: value;...;}}的方式,其中最外层{}表示是js表达式,内存{}表示个对象;
  • 样式的名称要用小驼峰命名,如fontSize
  • 只有一个根标签
  • 标签必须闭合
  • 标签首字母若为小写,则会将标签转为html中同名标签;若首字母为大写,则意味着是个组件

在使用中,利用babel将jsx编译成js使用;

面向组件编程

在react中,组件分为函数式组件和类式组件;

  • 函数式组件

函数式组件即为在创建虚拟DOM时使用函数定义;

// test.html
<body><script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script><script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script><div id="test"></div><script type="text/babel">// 函数式组件,名为test组件function Test() {console.log(this) // undefined,babel编译后开启了严格模式// 返回虚拟DOMreturn (<div><h1>React初尝试</h1></div>)}ReactDOM.render(<Test/>, document.getElementById('test')) // 渲染test组件到页面</script>
</body>

 执行ReactDOM.render之后,发生了什么?

  1. React解析组件标签,找到Test组件
  2. 发现Test组件是用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后渲染到页面上

在无hook之前,函数式组件中只有props这一种属性;

  • 类式组件

类式组件即为在创建虚拟DOM时使用类定义;

// test.html
<body><script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script><script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script><div id="test"></div><script type="text/babel">// 创建类式组件,皆要继承于React.Componentclass Demo extends React.Component {render() {return (<div><h1>React初尝试</h1></div>)}}ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面</script>
</body>

其中render函数是定义在Demo组件的原型对象对象上,供实例使用;

执行了ReactDom.render之后发生了什么?

  1. React解析组件标签,找到Demo组件;
  2. 发现组件是类式组件,随后React自动new出来该类的实例,并通过该实例调用原型上的render方法
  3. 将render方法返回的虚拟DOM转为真实DOM,随后渲染到页面上

类式组件上有三大属性:state,props,refs,下面分别进行介绍;

state

state是由多个key-value组成的对象;通过更新组件的state来更新页面(重新渲染页面);

state中的数据不能被直接更改,要通过调用setState({key: value})方法来更改;

如下案例为通过改变state中的workDay属性渲染页面显示工作日/休息日:

<body><script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script><script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script><div id="test"></div><script type="text/babel">class Demo extends React.Component {// 构造函数传参为props,在react中创建类组件时,若写构造函数,则要写super,否则会引起内部bugconstructor(props) {super(props)this.state = {workDay: true} // 设置状态// 未避免调用changeDay方法时this指向为undefined,因此利用bind将this指向变为实例对象,并将改变this指向后的函数赋值给实例对象的change属性this.change = this.changeDay.bind(this)}render() {return (<div><h1 onClick={this.change}>今天是{this.state.workDay? '工作日': '休息日'}</h1></div>)}// 该方法原本是给Demo类的实例对象使用的,若作为事件的回调函数,则this指向为undefined(babel为严格模式)changeDay() {this.setState({workDay: !this.state.workDay}) // 要利用setState方法修改state值}}ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面</script>
</body>

 如注释所说,若利用类组件方法创建组件,若在类中写了构造函数,则接收props参数的同时要写上super(props),以防止报错~

在构造函数中可以创建state对象,用以控制页面显示~

在定义事件函数时,若写在类中作为普通函数(则该方法如changeDay,只有作为类的实例对象被调用时,this才有值,即为该实例对象),作为事件被调用时,此时this为undefined。为解决此问题,可以在构造函数中改变this指向并赋值给另一属性,则此时的this就是类的实例对象,则可以正常被使用~

可以看到Demo实例上有change方法,原型对象上有changeDay方法;

备注:此处看不懂的要去复习ES6哦 JavaScript高级教程(面向对象编程)_迷糊的小小淘的博客-CSDN博客

修改state属性时,不能采用直接赋值的形式,要调用原型对象中的方法setState实现;

当然,state方法还有简单写法(不通过构造函数的方式),因为在继承类中,可省略构造函数constructor,同时若在类中直接写赋值语句,也可以将该属性给每个实例对象使用~所以可以将state直接写在类中,将事件回调函数用属性赋值的方式进行定义;

<script>class Person {constructor(name, age) {this.name = namethis.age = age}workerType = '程序媛'say = function(){console.log(`我的工作是${this.workerType}`)}}let person = new Person('人类', 100)console.log(person)person.say()
</script>

因此用在此例中如下:

  <script type="text/babel">// 创建类式组件,皆要继承于React.Componentclass Demo extends React.Component {state = {workDay: true} // 设置状态change = () => {console.log(this); // 此处一定要写成箭头函数形式, this的指向才为实例对象this.setState({workDay: !this.state.workDay})}render() {return (<div><h1 onClick={this.change}>今天是{this.state.workDay? '工作日': '休息日'}</h1></div>)}}ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面</script>

关于state的总结如下:

  • state可定义在构造函数中,也可利用赋值语句方法赋值;
  • state不可直接修改,需要调用原型对象中的setState方法;
  • 类组件中的render方法this指向组件实例对象(由react自动创建出来),组件初始化时会被渲染一次,随后在每次组件更新时被调用;
  • 类组件中的构造函数只会被调用一次;
  • 类组件中普通函数定义改变this指向有两种方法:

                ① 在构造函数中利用bind等方法改变this指向;

                ② 利用箭头函数+函数赋值的方式

props

        prop用于存放组件中的标签属性;props用于存放所有组件中的标签属性;

<script type="text/babel">class Demo extends React.Component {render() {return (<ul><li>{this.props.name}</li><li>{this.props.age}</li><li>{this.props.grade}</li></ul>)}}ReactDOM.render(<Demo name="小红" age="18" grade="100"/>, document.getElementById('test')) // 渲染虚拟DOM到页面const properties = {name: '小明', age: '19', grade: "90"}ReactDOM.render(<Demo {...properties}/>, document.getElementById('test1')) // 渲染虚拟DOM到页面</script>

复用组件时,利用props可传入不同的标签属性用于展示~

可以限制属性的类型,默认值及是否必输特性,需引入prop-types库;通过给类组件的propTypes和defaultProps进行赋值;

注意:

自 React v15.5 起,React.PropTypes 已移入另一个包中。请使用 prop-types 库 代替。

<body><script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script><script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script><!-- 引入prop-types库 --><script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script><div id="test"></div><div id="test1"></div><script type="text/babel">class Demo extends React.Component {render() {const {name, age, grade} = this.propsreturn (<ul><li>{name}</li><li>{age}</li><li>{grade}</li></ul>)}}Demo.propTypes = {name: PropTypes.string.isRequired, // 设置name为字符串类型且为必输项age: PropTypes.number, // 设置age为数字类型grade: PropTypes.number // 设置grade为数字类型}Demo.defaultProps = {age: 18, // 设置age默认值为18grade: 90 // 设置grade默认值为90}ReactDOM.render(<Demo name="小红" grade={100}/>, document.getElementById('test')) // 渲染虚拟DOM到页面const properties = {name: '小明', age: 19}ReactDOM.render(<Demo {...properties}/>, document.getElementById('test1')) // 渲染虚拟DOM到页面</script>
</body>

当然,也可以通过static关键字对其进行限制;

<script type="text/babel">class Demo extends React.Component {static propTypes = {name: PropTypes.string.isRequired, // 设置name为字符串类型且为必输项age: PropTypes.number, // 设置age为数字类型grade: PropTypes.number // 设置grade为数字类型}static defaultProps = {age: 18, // 设置age默认值为18grade: 90 // 设置grade默认值为90}render() {const {name, age, grade} = this.propsreturn (<ul><li>{name}</li><li>{age}</li><li>{grade}</li></ul>)}}ReactDOM.render(<Demo name="小红" grade={100}/>, document.getElementById('test')) // 渲染虚拟DOM到页面const properties = {name: '小明', age: 19}ReactDOM.render(<Demo {...properties}/>, document.getElementById('test1')) // 渲染虚拟DOM到页面</script>

函数式组件也可以使用props,通过给函数传参的方式拿到props:

<body><script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script><script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script><!-- 引入prop-types库 --><script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script><div id="test"></div><div id="test1"></div><script type="text/babel">function Demo(props) {const {name, age, grade} = propsreturn <ul><li>{name}</li><li>{age}</li><li>{grade}</li></ul>}Demo.propTypes = {name: PropTypes.string.isRequired, // 设置name为字符串类型且为必输项age: PropTypes.number, // 设置age为数字类型grade: PropTypes.number // 设置grade为数字类型}Demo.defaultProps = {age: 18, // 设置age默认值为18grade: 90 // 设置grade默认值为90}ReactDOM.render(<Demo name="小红" grade={100}/>, document.getElementById('test')) // 渲染虚拟DOM到页面const properties = {name: '小明', age: 19}ReactDOM.render(<Demo {...properties}/>, document.getElementById('test1')) // 渲染虚拟DOM到页面</script>
</body>

对props的总结如下:

  • 组件标签的所有属性都保存在props中
  • props属性是可读的,不能对其修改(会报错)
  • 可以利用propTypes以及defaultProps对属性类型、是否必输、默认值进行设置

refs

利用refs来表示某标签节点本身,避免操作dom获得标签;有三种形式:

  • 通过字符串赋值方式
<input ref="input">{name}</input>
<script type="text/babel">class Demo extends React.Component {state = {age: 17}addAge = () => {const newage  = Number(this.refs.span.innerText) + 1 // 利用this.refs.span可以拿到span节点this.setState({age: newage})}render() {return (<div><span ref="span">{this.state.age}</span><br/><button onClick={this.addAge}>加一</button></div>)}}ReactDOM.render(<Demo name="小红" age={10}/>, document.getElementById('test')) // 渲染虚拟DOM到页面</script>

该方法使用简单,但是要少用(有bug,以后可能会被弃用);

  • 回调函数方式的ref定义

可以在定义ref使用回调函数,传入的参数恰恰是该节点本身;将该参数赋值给其它变量获取即可;

<input ref={ (ele) => {this.input1 = ele} }>{name}</input>
 <script type="text/babel">class Demo extends React.Component {state = {age: 17}addAge = () => {const newage  = Number(this.span.innerText) + 1 // 利用this.refs.span可以拿到span节点this.setState({age: newage})}render() {return (<div><span ref={(ele) => {this.span = ele}}>{this.state.age}</span><br/> <button onClick={this.addAge}>加一</button></div>)}}ReactDOM.render(<Demo name="小红" age={10}/>, document.getElementById('test')) // 渲染虚拟DOM到页面</script>

利用回调函数方式定义ref时,在更新过程中会被执行两次:一次传入参数为null,第二次才是正常的节点元素,这是因为在每次更新时会创建一个新的函数实例,所以React清空旧的ref并设置新的,当然该差别在开发中没啥影响;见官网说明:Refs and the DOM – React

  • createRef创建ref容器

Refs是使用React.createRef()创建的,并通过ref属性附加到React元素。在构造组件时,通过将Refs分配给实例属性,以便可以在整个组件中引用它们;创建一个标记一个,React.createRef()与标签是一对一关系;使用时须通过.current拿到该节点元素;

 <script type="text/babel">class Demo extends React.Component {state = {age: 17, age1: 19}myRef = React.createRef() // 创建标记加的span节点myRef1 = React.createRef() // 创建标记减的span节点addAge = () => {const newage  = Number(this.myRef.current.innerText) + 1 // 利用this.myRef.current可以拿到span节点this.setState({age: newage})}subAge = () => {const newage  = Number(this.myRef1.current.innerText) - 1 // 利用this.myRef1.current可以拿到span节点this.setState({age1: newage})}render() {return (<div><span ref={this.myRef}>{this.state.age}</span><br/><button onClick={this.addAge}>加一</button><br/><span ref={this.myRef1}>{this.state.age1}</span><br/> <button onClick={this.subAge}>减一</button></div>)}}ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面</script>

对于refs的总结:

有三种方式使用refs

  • 字符串形式的ref
<input ref="input">{name}</input>
  • 回调形式的ref
<input ref={ (ele) => {this.input1 = ele} }>{name}</input>
  • 使用React.createRef()创建
myRef = React.createRef()
<input ref={ this.myRef }></input>

节点元素需通过this.myRef.current拿到;

在开发中,要尽量减少对ref的使用,可以通过event.target拿到发生事件的DOM元素;

组件生命周期

组件生命周期是指组件对象从创建到死亡会经历一些特定阶段。又称为组件生命钩子; 

  图片来源React lifecycle methods diagram

 上图展示了reactV16.4以后所有的生命周期函数,

  • 组件挂载时,依次会执行类的构造函数construtor、getDerivedStateFromProps、render函数及componentDidMount

组件挂载通过ReactDOM.render()触发;

static getDerivedStateFromProps(props, state)

 getDerivedStateFromProps会在调用render方法之前被调用,它应返回一个对象来更新state,若返回null则表示不更新任何内容(不写该函数表示返回null)。该函数适用于state的值在任何时候都取决于props。

componentDidMount()方法会在组件挂载后(插入到DOM树中)立即调用。一般在此阶段发送网络请求、开启定时器、订阅消息等;

<script type="text/babel">class Demo extends React.Component {state = {age: 17}myRef = React.createRef() // 创建标记加的span节点addAge = () => {const newage  = Number(this.myRef.current.innerText) + 1 // 利用this.myRef.current可以拿到span节点this.setState({age: newage})}render() {return (<div><span ref={this.myRef}>{this.state.age}</span><br/><button onClick={this.addAge}>加一</button><br/></div>)}static getDerivedStateFromProps() {console.log('getDerivedStateFromProps')return {age: 21} 
// 此处返回state对象中的age为21时,则不论点多少次按钮,age都不会被改变; 若返回null,则age会被修改}componentDidMount() {console.log('componentDidMount') // 在此处设置定时器,发送请求,订阅消息等}}ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面</script>
  • 组件更新时,依次执行getDerivedStateFromProps、shouldComponentUpdate、render、getSnapShotBeforeUpdate、componentDidUpdate

组件更新由组件内部setState或者父组件更新触发;

shouldComponentUpdate()返回布尔值,用以判断React组件的输出是否受当前state或props更改的影响,默认值是true,表示state每次发生变化组件都会重新渲染;该方法在首次渲染或使用forceUpdate()时不会被调用;但是官网建议慎用哦~

此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。

getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate()方法在最近一次渲染输出(提交到DOM节点)之前调用。它使得组件能在发生更改之前从DOM中捕获一些信息,如滚动位置。此方法的任何返回值都将作为参数传递给componentDidUpdate();该方法并不常用;

componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate()方法会在组件更新后立即调用。首次渲染不会执行该方法。如果组件实现了getSnapshotBeforeUpdate()方法,则它的返回值将作为第三个参数传递给componentDidUpdate(),否则该参数为undefined;

  <script type="text/babel">class Demo extends React.Component {state = {age: 17}myRef = React.createRef() // 创建标记加的span节点addAge = () => {const newage  = Number(this.myRef.current.innerText) + 1 // 利用this.myRef.current可以拿到span节点this.setState({age: newage})}render() {return (<div><span ref={this.myRef}>{this.state.age}</span><br/><button onClick={this.addAge}>加一</button><br/></div>)}static getDerivedStateFromProps(props, state) {console.log('getDerivedStateFromProps')return props}componentDidMount() {console.log('componentDidMount')}shouldComponentUpdate() {console.log('shouldComponentUpdate')return true// 此处设为false则点击按钮不会被更新,下面两个方法就不会被执行,点击true则正常更新}getSnapshotBeforeUpdate(prevProps, prevState) {console.log('getSnapshotBeforeUpdate')return 123}componentDidUpdate(prevProps, prevState, snapshot) {console.log('componentDidUpdate')return 456}}ReactDOM.render(<Demo />, document.getElementById('test')) // 渲染虚拟DOM到页面</script>
  • 组件卸载时,执行componentWillUnmount()

组件卸载通过ReactDOM.unmountComponentAtNode触发

componentWillUnmount()用在组件卸载及销毁之前直接调用,一般在此阶段执行必要的清理操作,如清除定时器、取消网络请求或清除在componentDidMount()中创建的订阅;

下图展示了常用的声明周期函数,在日常开发中经常被使用。

图片来源React lifecycle methods diagram

备注:旧版本中有几个弃用的声明周期函数,如 componentWillUnmount() 、componentWillUpdate() 、componentWillReceiveProps(),了解即可~


To be continue~