系列文章目录
第一章:React从入门到进阶之初识React
第一章:React从入门到进阶之JSX简介
第三章:React从入门到进阶之元素渲染
第四章:React从入门到进阶之JSX虚拟DOM渲染为真实DOM的原理和步骤
第五章:React从入门到进阶之组件化开发及Props属性传值
第六章:React从入门到进阶之state及组件的生命周期
第七章:React从入门到进阶之React事件处理
一、State
本节中我们将介绍React组件中的一个新的概念:“State”。在前面元素渲染一章中我们用函数组件做了一个时钟的例子,由于函数式组件是一个静态组件,内容一旦渲染就不能再改了,要想改变函数组件里的内容除了我们后面要讲的使用React HOOKS以外,只能通过重新渲染组件的方式来达到更新内容的目的。先来回顾一下前面章节我们用函数组件做的时钟的小案例
function Clock(props) { return ( <div> <h1>Hello, world!</h1> <h2>It is { props.date.toLocaleTimeString()}.</h2> </div> ); } function tick() { ReactDOM.render( <Clock date={ new Date()} />, document.getElementById('root') ); } setInterval(tick, 1000);
如上代码,我们用函数封装了一个Clock时钟组件,然后利用定时器每隔1秒调用一次tick函数来重新渲染组件从而实现更新时钟的目的,那么这种方式显然不是最优的。React为我们提供了一个更好的办法来更新UI,那就是接下来要登场的“State”
- 什么是State
- State与props类似,但是State是React组件内部定义的一个私有属性,并且完全受控于当前组件
- 在定义组件时,把需要用到的一些属性定义在State对象中,然后通过this.state.[属性名]来访问该属性
- State 一般都只定义在类组件中,因为类组件都会继承自React.Component,而这个类中有个setState方法,我们可以通过调用setState方法来更新类组件中的state属性进而更新页面内容
- 在组件中定义state属性时,只能命名为“state”,这样才能通过setState方法来更新state属性,因为在setState方法中默认会寻找名为state的属性并进行处理,所以如果命名为其它名称,是无法通过setState来更新属性值及DOM内容的
接下来我们把上面时钟案例中的函数组件改造为类组件
class Clock extends ReactComponent{ constructor(){ this.state = { date: new Date().toLocaleTimeString() } } render(){ return <h1>{ this.state.date}</h1> } } ReactDOM.render(<Clock />,document.getElementById('root'));
我们把函数组件转换成了类组件,同样实现了一个时钟案例,只不过目前还是静态的,时间还不会自动更新。接下来我们继续来完善这个Clock组件,设置定时器并每秒更新它。
二、在类组件中使用生命周期方法
- 分析与思考
- 上面的代码中,我们已经实现了一个静态的始终组件,那我们发现当页面第一次加载的时候,显示出来的时间就是当前时间,但是这个时间确实固定不变的,这显然不是我们想要的效果。那么我们应该想办法让时钟走起来。
- 试想当页面第一次渲染加载完成的时候,页面上显示的时间是正确的,那么如果我们在页面第一次加载完成后能够自动执行一个方法并设置一个计时器,来让时钟走起来是不是就可以了呢?
- 另外如果上面的想法可行,我们在页面第一次加载后设置了定时器,那么在页面卸载前我们还应该手动清除定时器,因为我们知道,如果定时器不手动清除的话就有可能会一直存在,从而影响程序的性能,甚至可能导致内存泄漏。那么这就需要在页面卸载完成前也得自动调用一个函数来执行这些操作。
- 其实React还真为我们提供了一套这样的方法,这类方法在React中被称为“生命周期方法”。
接下来,我们就按照我们的想法来使用一下这些生命周期方法
- componentDidMount
- componentDidMount这个方法会在组件第一次渲染完成后执行,所以我们可以把定时器放在这个方法里面
- 因为后面组件被卸载时我们还需要清除定时器,所以在设置定时器时还需要把计时器返回的ID保留下来,便于后面清除使用
- 我们给组件添加一个timer属性,然后把计时器返回的ID保存在timer中
componentDidMount(){ this.timer = setInterval( () => { //这里写更新时间的逻辑 },1000); }
- componentWillUnmount
- componentWillUnmount这个方法将会在组件被卸载之前执行,所以我们可以在这里清除计时器
componentWillUnmount(){ clearInterval(this.timer); }
- 最后,我们再来定义一个tick方法,用于更新组件的state从而更新页面时钟
- 在tick方法中我们通过调用setState方法来更新state中的date值
- 需要注意的是:这里不能直接通过this.state.date = xxx来修改date值,因为这样即使date值被修改了,页面也不会重新渲染
- 只有通过调用setState方法改变date,页面才会重新渲染。因为setState不仅能够修改state中的值,同时还能更新DOM让组件重新渲染。
tick(){ this.setState({ date:new Date().toLocaleTimeString() }); }
下面我们把所有代码进行整合,来完成我们的时钟小案例
class Clock extends ReactComponent{ constructor(){ this.state = { date: new Date().toLocaleTimeString() } } render(){ return <h1>{ this.state.date}</h1> } componentDidMount(){ this.timer = setInterval( () => { this.tick(); },1000); } componentWillUnmount(){ clearInterval(this.timer); } tick(){ this.setState({ date:new Date().toLocaleTimeString() }); } } ReactDOM.render( <Clock />, document.getElementById('root') );
- 捋一捋
接下来我们来捋一捋组件从渲染到被卸载的整个流程
- 当< Clock />被传给ReactDOM.render()方法的时候,React会调用Clock的构造函数,也就是说会创建一个Clock对象的实例。因为Clock需要显示当前时间,所以它会用一个包含当前时间的对象来初始化this.state。之后我们再来更新state来更新当前时间
- 之后React会调用组件的render 方法,这就是React确定应该在页面上显示什么内容的方式。然后React更新DOM来匹配Clock渲染的输出
- 当Clock的输出被插入到DOM中之后,也就是页面渲染完成后,React会调用componentDidMount生命周期方法,在这个方法中,Clock组件向浏览器请求设置一个定时器来每秒调用一次组件的tick方法
- 因为设置了定时器,所以浏览器每隔1秒都会调用一次tick方法,在这个方法中,Clock组件会通过setState方法来进行一次属性值及UI的更新。由于setState的调用,React能够知道state已经发生改变,然后会重新调用render方法来确定页面上该显示什么内容,这一次render中的this.state.date就不一样,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。
- 一旦Clock组件从DOM中被移除,React就会调用componentWillUnmount()方法,这样计时器就停止工作了。
正确的使用State
上面我们已经通过时钟小案例掌握了state和生命周期方法的一些基本使用。下面我们再来说一下state在使用的时候有哪些需要注意的事项
- 不要直接修改State
- 上面已经提到过,我们不能直接通过this.state.xxx = xxx来修改state,这种修改即使state的值变了,组件也不会被重新渲染
- 构造函数(constructor)是唯一可以给this.state属性赋值的地方
- 要想修改state的值,我们应该通过调用setState方法来修改
this.state.name = "hello";//错误的写法 this.setState({ name:'Alvin'});//正确的写法 constructor(){ this.state = { name:'Yinnes'};//正确的写法 }
- State 的更新是异步的
- 出于性能考虑,React可能会把多个setState调用合并成一个调用
- 由于this.props和this.state可能会异步更新,所以不要依赖他们的值来更下一个状态,例如下面的代码可能无法更新计数器:
this.setState( counter: this.state.counter + this.props.increment });
- 要想解决这个问题,可以让setState接收一个函数作为参数而不是对象,然后这个函数接收2个参数:state和props,这样就可以在函数中直接操作state和props了
// Correct this.setState((state, props) => ({ counter: state.counter + props.increment }));
- State的更新会被合并
- 当我们调用setState的时候,React会把我们提供的对象合并到当前的state中。例如现有一个state包含了几个独立的变量:
constructor(props) { super(props); this.state = { posts: [], comments: [] }; }
- 然后我们可以分别调用setState来单独的更新它们:
componentDidMount() { fetchPosts().then(response => { this.setState({ posts: response.posts }); }); fetchComments().then(response => { this.setState({ comments: response.comments }); }); }
- 这里就会把我们提供的response.posts更新给state.posts,把response.comments更新给state.comments。但需要注意的是:这里的合并是浅合并,所以在执行this.setState({comments})时完整保留了this.state.posts, 但是完全替换了 this.state.comments。
数据是向下流动的
- 不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。
- 关于state,除了拥有并设置了它的组件以外,其他组件都无法访问它
- 组件可以选择把它的 state 作为 props 向下传递到它的子组件中
<FormattedDate date={ this.state.date} /> function FormattedDate(props) { return <h2>It is { props.date.toLocaleTimeString()}.</h2>; }
- 上面的代码中:FormattedDate 组件会在其 props 中接收参数 date,但是组件本身无法知道它是来自于 Clock 的 state,或是 Clock 的 props,还是手动输入的
- 这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件(后代组件)
- 如果我们把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。也就是说不管是state还是props只能是父传子,而不能子传父
本节内容就介绍到这里,下一章节中我们将会讲解React中的事件处理