zoukankan      html  css  js  c++  java
  • react之state&生命周期

    在元素渲染章节中,我们了解了一种更新UI界面的方法,通过调用ReactDOM.render()修改我们想要的
    元素

    import ReactDOM from 'react-dom'
    
    class ClockCom extends React.Component{
    	render(){
    		return(
    			<div>
    				<h1>this clock component</h1>
    				<h2>It is {this.props.time.toLocaleTimeString()}</h2>
    			</div>
    		)
    	}
    }
    function tick(){
    	ReactDOM.render(<ClockCom time={new Date()} />,document.getElementById("clock-com"))
    }
    setInterval(tick,1000)
    

    在上述代码中我们封装了一个clockCom的class组件,每次组件更新时候render方法都会被调用,但只要在相同的DOM节点中渲染,就
    仅有一个ClockCom组件的class实例被创建使用
    但是在实际的React项目中一个单页面web应用ReactDOM.render通常只调用一次。那么在react
    组件中,我们需要更新UI,这时我们就需要用到state了。

    向class组件中添加局部的state

    我们通过以下三步将date从props移动到state中

    1. 把render()方法中的this.props.date替换成this.state.date
    2. 在class构造函数中,为this.state赋值,通过super方式将props传递到父类的构造函数中,class组件应该始终使用props
      参数来调用父级的构造函数
    3. 移除元素的date属性
      代码如下:
    class ClockCom extends React.Component{
    	construcor(props){
    		super(props)
    		this.state = {time:new Date()}
    	}
    	render(){
    		return(
    			<div>
    				<h1>this clock component</h1>
    				<h2>It is {this.state.time.toLocaleTimeString()}</h2>
    			</div>
    		)
    	}
    }
    ReactDOM.render(<ClockCom />,document.getElementById('clock-com'))
    

    接下来,设置ClockCom的计时器并每秒更新它

    将生命周期方法添加到Class中

    在具有许多组件的应用程序中,当组件被销毁时,释放所占用的资源是非常重要的。当ClockCom组件第一次
    被渲染到DOM中的时候,就为其设置一个计时器,这在React中被称为"挂载(mount)"

    同时,当DOM中Clock组件被删除的时候,应该清除计时器,这在React中被称为"卸载(unmount)"

    我们可以为class组件声明一些特殊方法,当组件挂载或卸载的时候就去执行这些方法

    componentDidMount(){
    }
    componentWillUnmount(){
    }
    

    这些方法叫做生命周期方法

    componentDidMount()方法会在组件已经被渲染到DOM中后运行,所以,最好在这里设计计时器
    componentWillUnmount()方法中,组件即将卸载,可以在这里清除定时器

    完整代码如下

    class ClockCom extends React.Component{
    	constructor(props){
    		super(props)
    		this.state = {time:new Date()};
    	}
    	componentDidMount(){
    		this.timerID = setInterval(() => this.tick(),1000)
    	}
    	componentWillUnmount(){
    		clearInteval(this.timerID)
    	}
    	tick(){
    		this.setState({
    			time:new Date
    		})
    	}
    	render(){
    		return(
    			<div>
    				<h1>this clock component</h1>
    				<h2>It is {this.state.time.toLocaleTimeString()}</h2>
    			</div>
    		)
    	}
    }
    ReactDOM.render(<ClockCom />,document.getElementById("clock-com"))
    

    概括一下发生了什么和这些方法的调用顺序:

    1. 被传给ReactDOM.render()的时候,React会调用
      ClockCom组件的构造函数。因为ClockCom需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化
      this.state。我们之后会更新state。
    2. 之后React会调用组件的render()方法。这就是React确定该在页面展示什么的方式。
      然后React更新DOM来匹配ClockCom渲染输出。
    3. ClockCom的输出被插入到DOM中后,React就会调用ComponentDidMount()生命周期方法。
      在这个方法中,ClockCom组件向浏览器请求设置一个计时器来每秒调用一次组件的tick()方法
    4. 浏览器每秒都会调用一次tick()方法。在这方法之中,ClockCom组件会通过调用
      setState()来计划进行一次UI更新。得益于setState()的调用,React能够知道state已经改变了,然后会重新
      调用render()方法来确定页面上该显示什么。这一次,render()方法中的
      this.state.time就不一样了,如此一来就会渲染输出更新过的时间。React
      也会相应的更新DOM。
    5. 一旦ClockCom组件从DOM中被移除,React就会调用componentWillUnmount()生命周期,这样计时器就停止了

    这样我们就实现了一个时钟的组件。从上述例子中我们来学习react的state和生命周期

    什么是state

    state可被视为React组件中的一个集合,这个集合的内容是是该组件UI中可变状态的数据。所谓可变状态的数据,就是在当前
    组件中可以被修改(或被更新)的数据。

    组件对state的要求

    • state必须是能代表一个组件UI呈现的完整状态集:组件UI的任何改变,
      都可以从state的变化中反映出来
    • state必须是代表一个组件UI呈现的最小状态集:state中的所有状态都是用于反映组件UI的变化,没有任何
      多余的状态,也不需要通过其他状态计算而来的中间状态。

    变量能否作为state的依据

    组件中用到的一个变量应不应该作为一个组件的state,可以通过下面的4条依据进行判断

    1. 这个变量是否通过props从父组件中获取?如果是,那么它不适合以state来表示。
    2. 这个变量是否在组件的整个生命周期中都保持不变?如果是,那么他不适合以state来表示
    3. 这个变量是否可以通过其他状态(state)或者属性(props)计算得到?如果是,那么它不适合以state来表示
    4. 这个变量是否在render方法中作为一个用于渲染的数据?如果不是,那么它不适合以state来表示。这种情况下,
      这个变量更适合定义为组件的普通属性,例如在组件中用到的定时器,就应该直接定义为this.timer,而不是this.state.timer
    5. 另外要考虑这个状态需不需要状态提升到父组件中

    使用react经常会遇到几个组件需要共用状态数据的情况。这种情况下,我们最好将这部分共享的状态提升
    至他们最近的父组件当中进行管理。react的状态提升主要就是用来处理父组件和子组件的数据传递的;他们可以
    让我们的数据流动的形式是自定向下单向流动的,所有组件数据都是来自于他们的父辈组件,也都是有父辈组件来统一存储和修改,在传入子组件当中的

    state与props的区别

    在react组件中,我们都需要用到数据,需要改变数据以实现刷新视图。我们知道React的数据是自顶向下单向流动
    的,也就是从父组件传到子组件,组件的数据存储在props和state中,这两个属性有什么区别呢?

    让我们来看下面的代码

    class SetStateCom extends React.Component{
    	constructor(props){
    		super(props)
    		this.state = {
    			count:0
    		}
    		this.handleSomething = this.handleSomething.bind(this)
    	}
    	render(){
    		return(
    			<div>
    				<button onClick={this.handleSomething}>+</button>
    				<span>{this.state.count}</span>
    				<button>-</button>
    			</div>
    		)
    	}
    	incremnetCount(){
    		this.setState({count:this.state.count+1})
    		
    	}
    	handleSomething(){
    		this.incremnetCount()
    		this.incremnetCount()
    		this.incremnetCount()
    		console.log(this.state.count)
    		
    	}
    }
    ReactDOM.render(<SetStateCom />,document.getElementById('set-state'))
    

    上面代码中,我们给state定义了一个count,定义了一个方法incrementCount来增加state.count的值,
    给按钮绑定了一个事件handleSomething,点击执行三次incrementCount方法,但是在我们点击按钮后,执行 结果并不是
    我们所设想的那样,在次点击时候,没有点击的时候,this.state.count的值是0,第一次点击的时候this.state.count
    值是1,第二次点击的时候this.state.count的值是2...这是为什么呢?

    思考一下调用setState()时候发生了什么?
    React首先会将你传递给setState的参数对象合并到当前state对象中,然后
    会启动所谓的reconciliation,即创建一个新的React Element tree
    (UI层面的对象表示),和之前的tree做比较,基于你传递给setState的对象找出发生的变化,然后更新DOM.

    所以调用setState并不一定会即时更新state

    考虑到性能问题,React可能会将多次setState调用批处理(batch)为一次state的更新

    这又意味着什么呢?

    首先,'多次setState()调用'的意思是在某个函数中调用了多次setState(),就像上述代码

    incremnetCount(){
    	this.setState({count:this.state.count+1})
    	console.log(this.state.count)
    }
    handleSomething(){
    	this.incremnetCount()
    	this.incremnetCount()
    	this.incremnetCount()
    	console.log(this.state.count)
    	
    }
    

    面对这种多次setState()调用的情况,为了避免重复做上述大量的工作,React并不会
    真的完整调用三次'set-state';相反,它会把这些部分更新打包装好,一次搞定。

    在这里传递setState()的纯粹是个对象。现在,假设React每次遇到多次
    setState()调用都会作上述批处理过程,即每次调用setState()时传递给他的所有对象合并为一个对象,
    然后用这个对象去做真正的setState()

    在JavaScript中,对象合并可以这样写

    const singleObject = Object.assign(
    	{},
    	objectFormSetState1,
    	objectFormSetState2,
    	objectFormSetState3
    )
    

    这种写法叫做object组合(composition)

    在JavaScript中,对象'合并(merging)'或者叫对象组合(composing)的工作机制如下L
    如果传递给Object.assign()的多个对象有相同的键,那么最后一个对象的值会覆盖之前的

    const me = {name:'juce'},
    	  you = {name:'coke'},
    	  we = Object.assign({},me,you);
    	  we.name === 'coke';//true
    	  console.log(we);//{name:'coke'}
    

    因为you是最后一个合并进we中的,因此you的name属性值会覆盖me的
    name属性值,所以最后输出的we的name为you的name值

    综上所述,如果你多次调用setState()函数,每次都传递给它一个对象,那么React就会将
    这些对象合并。也就是说,基于你传进来的多个对象,React会组合出一个新对象。
    如果这些对象有同名的属性,那么就会取最后一个对象的属性值

    这意味着handleSomething函数的在点击时结果会是1而不是3。因为React并不会按照
    setState()的调用顺序即时更新state,而是首先会将所有的对象合并到一起

    需要搞清楚的是,给setState()传递对象本身是没有问题的,问题出在当你想要基于之前的state计算下一个
    state值时还给setState()传递对象

    正确的做法是

    让函数式setState来拯救

    将上面incremnetCount函数改为如下代码

    incremneCount(){
    	this.setState((state) => {
    		return {
    			count:state.count + 1
    		}
    	})
    }
    

    执行结果,第一次点击结果为3,和我们预想的一样

    因此props和state区别就是:

    state在当前组件中是可变的,满足组件UI变化的需求

    props对于子组件来说是只读的

    如何正确修改state

    • 不要直接给state赋值
    this.state.time = new Date()
    

    只有在组件的构造函数中初始化state的时候才允许这样直接赋值;其他绝大多数时候,应该使用setState(),在本文的最后,

    正确的写法如下:

    this.setState({
    	time:new Date()
    })
    

    state的更新可能是异步的

    react可以将多个setState调用合并成一个调用来提高性能。同时,Props的更新机制也是同理。这就是"异步更新"。
    因为this.props和this.state可能是异步更新,你不应该依靠他们的值来计算下一个状态

    弥补这个缺憾:

    我们不能直接通过this.statethis.props获得state和props的最新状态,但是在this.setState的时候,
    state和props的最新状态可以通过一个回调函数来获得:

    this.setState((preState,props) =>({
    	counter:preState.quantity + 1 + props.xxx
    }))
    

    上述回调函数的第一个参数preState可捕获到最新的上一个state;第二个参数props可捕获到最新的props

    state的更新是一个浅合并的过程

    当调用setState修改组件状态时,只需要传入发生改变的state,而不必组件完整的state,因为组件state的更新是一个浅合并的过程。
    例如,一个组件初始化时的状态为:

    this.state = {
    	title:'React',
    	content:'React is an wonderful Js library!'
    }
    

    如果你只需要修改title,你应该:

    this.setState({
    	title:'reactJS'
    })
    

    React会合并新的title到原来的组件状态中,同时2保留原有的状态content,合并后的state的结果为

    {
    	title:'reactJS',
    	content:'React is an wonderful Js library!'
    }
    

    React中的immutability(不变性)

    React官方建议把State当做是不可变对象,State中包含所有状态都应该是不可变对象
    当State中的某个状态发生变化,我们应该重新创建这个状态对象而不是直接修改原来的状态。state
    根据状态类型可以分为三种。

    1. 数字,字符串,布尔值,null,undefined这五种不可变类形。
    this.setState({
    	num:1,
    	string:'hello',
    	ready:true
    })
    
    1. 数组类型
      js数组类型为可变类型。加入有一个state是数组类型,例如students,修改students的
      状态应该保证不会修改原来的状态,例如新增一个数组元素,应使用数组的concat方法或ES6的数组扩展
      语法
    class ArrayDemo extends React.Component{
    	constructor(props){
    		super(props)
    		this.state = {
    			students:['liman','gaoxi','huangjia']
    		}
    		this.changeStudents = this.changeStudents.bind(this)
    	}
    	render(){
    		return(
    			<div>
    				<div>
    					{this.state.students.map((student,i) => <div key={i}>{student}</div>)}
    				</div>
    				<button onClick={this.changeStudents}>改变students</button>
    			</div>
    		)
    	}
    	changeStudents(){
    		this.setState({
    			students:this.state.students.concat('xiaoqin')
    		})
    		console.log(this.state)
    	}
    }
    ReactDOM.render(<ArrayDemo />,document.getElementById('array'))
    

    上面代码中,我们向数组中添加新的一项,如果用push,原数组会发生改变,但是在react,不会更新状态,会报错,因此使用concat来实现

    使用ES6数组的扩展来实现,将上面代码改成如下

    changeStudents(){
    	this.setState(preState => ({
         students:preState.students.concat('xiaoqin')
       }))
    	console.log(this.state)
    }
    

    从数组中截取部分作为新状态时,应使用slice方法;当从数组中过滤部分元素后,作为新状态
    时,使用filter方法。不应该使用push、pop、shift、unshift、splice等方法修改数组
    数组类型的状态,因为这些方法都是在原数组的基础上修改的。应当使用不会修改原数组而返回一个新数组
    的方法,例如concat、slice、fliter等

    当从students中截取部分元素作为新状态时候,使用数组的slice方法:
    方法一:将state先赋值给另外的变量,然后使用slice创建新数组

    var students = this.state.students
    this.setState({
    	students:students.slice(1,3)
    })
    

    方法二:使用preState、slice创建新数组

    this.setState((preState)=>({
    	students:preState.students.slice(1,3)
    }))
    

    当数组从students中过滤部分元素后,作为新状态时,使用数组的fliter方法
    方法一:将state先赋值给另外的变量,然后使用filter创建新数组

    var students = this.state.students
    this.setState({
    	students:students.fliter(item =>{
    		return item != 'xiaoqin'
    	})
    })
    

    方法二:使用preState、filter创建新数组

    this.setState((preState)=>({
    	students:preState.students.fliter((item) =>  {
    		return item !='xiaoqin'
    	})
    }))
    
    1. 普通对象
      对象也是可变类型,修改对象类型的状态的时,应保证不会修改原来的状态。可以使用ES6的Object.assign方法或者对象扩展语法
    class ObjectDemo extends React.Component{
    	constructor(props){
    		super()
    		this.state = {
    			school:{
    				classNum:7,
    				teacher:'wangfayue',
    				students:50
    			}
    		}
    		this.changeSchool = this.changeSchool.bind(this)
    	}
    	render(){
    		return(
    			<div>
    				<div>
    					{Object.keys(this.state.school).map(key =>(
    						<div key={key}>{key}:{this.state.school[key]}</div>
    					))}
    				</div>
    				<button onClick={this.changeSchool}>修改对象</button>
    			</div>
    		)
    	}
    	changeSchool(){
    		this.setState((preState) => ({
    			school:Object.assign({},preState.school,{slogn:'good good study day day up'})
    		}))
    	}
    }
    ReactDOM.render(<ObjectDemo />,document.getElementById("object"))
    

    使用ES6对象扩展语法,上面代码改为

    changeSchool(){
    	var slogn = 'day day study'
    	this.setState(preState => ({
    		school:{...preState.school,slogn}
    	}))
    }
    

    react组件生命周期

    看上面的图片,我们可能理解不了什么,让我们来看下demo

    class LifeCicle extends React.Component{
    	constructor(props){
    		super(props)
    		this.state = {
    			txt:'hello world',
    			name:'react'
    		}
    		console.log('constructor: ',this)
    		this.changeTxt = this.changeTxt.bind(this)
    		this.unloade = this.unloade.bind(this)
    	}
    	static getDerivedStateFromProps(props,state){
    		console.log(props,state)
    		console.log('getDerivedStateFromprops: ',this)
    		return null
    	}
    	getSnapshotBeforeUpdate(prevProps,prevState){
    		console.log(prevProps,prevState)
    		console.log('getSnapshotBeforeUpdate: ',this)
    		return prevProps
    	}
    	changeTxt(){
    		this.setState({
    			txt:'hello react'
    		})
    	}
    	unloade(){
    		ReactDOM.unmountComponentAtNode(document.getElementById('life-cycle'))
    	}
    	render(){
    		console.log('render: ',this)
    		return(
    			<div>
    				<div>{this.props.user}</div>
    				<div>{this.state.txt}</div>
    				<div>{this.state.name}</div>
    				<button onClick={this.changeTxt}>修改txt</button>
    				<button onClick={this.unloade}>卸载组件</button>
    			</div>
    		)
    	}
    	componentDidMount(){
    		console.log('componentDidMount: ' ,this)
    	}
    	shouldComponentUpdate(nextProps){
     		console.log(nextProps)
    		console.log('shouldComponentUpdate: ',this)
    		if(nextProps){
    			return nextProps
    		}
    	}
    	componentDidUpdate(){
    		console.log('componentDidUpdate: ',this)
    	}
    	componentWillUnmount(){
    		console.log('componentWillUnmount',this)
    	}
    }
    ReactDOM.render(<LifeCicle user='dehenliu' />,document.getElementById('life-cycle'))
    

    执行结果如下:
    一开始没做任何操作的结果

    点击修改txt按钮结果

    点击卸载组件按钮结果

    从上面执行结果,我们可以知道一开始就执行constructor,getDerivedStateFromProps,render,componentDidMount
    函数,在点击changeTxt按钮后,更新state状态,会执行getDerivedStateFromprops,shouldComponentUpdate,render,
    getSnapshotBeforeUpdate,componentDidUpdate函数,点击'卸载组件'后,执行componentWillUnmount,根据这些结果我们可以
    将react的生命周期分为三个阶段

    • 挂载阶段
    • 更新阶段
    • 卸载阶段

    挂载阶段

    挂载阶段,也可以理解为组件的初始化阶段,就是将我们的组件插入到DOM中,只会发生一次,这个阶段的生命周期函数调用如下

    • constructor
    • getDerivedStateFromProps
    • render
    • componentDIDMount
    constructor

    组件构造函数,第一个被执行

    如果没有显示定义它,会拥有一个默认的构造函数

    如果显示定义了构造函数,我们必须在构造函数第一行执行super(props),否则我们无法在
    构造函数里拿到this对象

    在构造函数里,一般做两件事情

    • 初始化state对象
    • 给自定义方法绑定this

    禁止在构造函数中调用setState,可以直接给state设置初始值

    getDerivedStateFromProps

    static getDerivedStateFromProps

    一个静态方法,所以不能在这个函数里面使用this,这个函数有两个参数props和state,
    分别指接收到的新参数和当前的state对象,这个函数会返回一个对象用来更新当前state
    对象,如果不需要更新可以返回null

    该函数会在挂载时候,接收到新的props,调用了setState和forceUpdate时被调用

    render

    react中最核心的方法,一个组件中必须要有这个方法

    返回类型有一下几种

    • 原生的DOM,如div
    • React组件
    • Fragment(片段)
    • Portals(插槽)
    • 字符串和数字,被渲染成text节点
    • Boolean和null,不会渲染任何东西

    render函数是纯函数,里面只做一件事,就是返回需要渲染的东西,不应该
    包含其他的业务逻辑,如数据请求,对于这些业务逻辑请移到componentDidiMount
    和componentDidUpdate中

    componentDidMount

    组件装载之后调用,此时,我们可以获取到DOM节点并操作,比如对canvas,svg的操作,
    服务器请求,订阅都可以卸载这个里面,但是记得在componentWillUnmount中取消订阅

    在componentDidMount中调用setState会触发一次额外的渲染,多调用了一次render
    函数,但是用户对此没有感知,因为他是在浏览器刷新屏幕前执行的,但是我们应该在开发中
    避免它,因为它会带来一定的性能问题,我们应该在constructor中初始化我们的state
    对象,而不应该componentDidMount调用state方法

    更新阶段

    更新阶段,当组件的props改变了,或组件内部调用了setState或者forceUpdate发生,会发生多次

    这个阶段的生命周期函数调用如下

    • getDerivedStateFromProps
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate
    • componentDidUpda
    getDerivedStateFromProps

    这个方法在挂载阶段已经讲过,在更新阶段,无论我们接收到新的属性,调用了setState还是调用了forceUpdate,
    这个方法都会被调用

    shouldComponentUpdate

    shouldComponentUpdate(nextProprs,nextState)

    有两个参数nextPropsnextState,表示新的属性和变化之后的
    state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新
    渲染,默认返回true

    注意当我们调用forceUpdate并不会触发此方法

    因为默认返回true,也就是只要接收到新的属性和调用了setState都会触发
    重新的渲染,这会带来一定的性能问题,所以我们需要将this.props与nextProps
    以及this.state与nextState进行比较来决定是否返回false,来减少
    重新渲染

    getSnapshotBeforeUpdate

    getSnapshotBeforeUpdate(prevProps,preState)

    这个方法在render之后,componentDidUpdate之前调用,有两个参数
    nextPropsnextState,表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个
    参数传给componentDidUpdate,如果你不想要返回值,请返回null,不写的
    话控制台会有警告,这个方法一定要和componentDidUpdate一起使用,否则控制台也会有警告

    componentDidUpdate

    componentDidUpdate(prevProps, prevState, snapshot)
    该方法在getSnapshotBefroeUpdate方法之后被调用,有三个参数
    prevProps,prevState,snapshot,表示之前的props,之前的state和
    snapshot。第三个参数是getSnapshotBefore返回的

    在这个函数里我们可以操作DOM,和发起服务器请求,还可以setState,但是注意一定
    要用if语句控制,否则会导致无限循环

    卸载阶段

    卸载阶段,当我们组件被卸载或者销毁了

    这个阶段的生命周期函数只有一个

    • componentWillUnmount
    componentWillUnmount

    当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,
    清理无效的DOM元素等垃圾清理工作

    注意不要在这个函数里去调用setState,因为组件不会重新渲染了

  • 相关阅读:
    而字歌
    排列组合的学习,基础入门,选修2-3
    金句集(目前9句)
    简析几何叉乘与安培力的内在逻辑
    浅谈参变分离的妙用
    日语
    Tarjan-SCC-NOIP2015message
    WebAPI身份验证
    简单记录在Visual Studio 2013中创建ASP.NET Web API 2
    从两个平方算法到分治算法-java
  • 原文地址:https://www.cnblogs.com/dehenliu/p/12616948.html
Copyright © 2011-2022 走看看