React 深入系列3:Props 和 State

news/2024/6/28 19:34:20
文:徐超,《React进阶之路》作者

授权发布,转载请注明作者及出处


React 深入系列3:Props 和 State

React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。

React 的核心思想是组件化的思想,而React 组件的定义可以通过下面的公式描述:

UI = Component(props, state)

组件根据props和state两个参数,计算得到对应界面的UI。可见,props 和 state 是组件的两个重要数据源。

本篇文章不是对props 和state 基本用法的介绍,而是尝试从更深层次解释props 和 state,并且归纳使用它们时的注意事项。

Props 和 State 本质

一句话概括,props 是组件对外的接口,state 是组件对内的接口。组件内可以引用其他组件,组件之间的引用形成了一个树状结构(组件树),如果下层组件需要使用上层组件的数据或方法,上层组件就可以通过下层组件的props属性进行传递,因此props是组件对外的接口。组件除了使用上层组件传递的数据外,自身也可能需要维护管理数据,这就是组件对内的接口state。根据对外接口props 和对内接口state,组件计算出对应界面的UI。

组件的props 和 state都和组件最终渲染出的UI直接相关。两者的主要区别是:state是可变的,是组件内部维护的一组用于反映组件UI变化的状态集合;而props是组件的只读属性,组件内部不能直接修改props,要想修改props,只能在该组件的上层组件中修改。在组件状态上移的场景中,父组件正是通过子组件的props,传递给子组件其所需要的状态。

如何定义State

定义一个合适的state,是正确创建组件的第一步。state必须能代表一个组件UI呈现的完整状态集,即组件对应UI的任何改变,都可以从state的变化中反映出来;同时,state还必须是代表一个组件UI呈现的最小状态集,即state中的所有状态都是用于反映组件UI的变化,没有任何多余的状态,也不需要通过其他状态计算而来的中间状态。

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

  1. 这个变量是否是通过props从父组件中获取?如果是,那么它不是一个状态。
  2. 这个变量是否在组件的整个生命周期中都保持不变?如果是,那么它不是一个状态。
  3. 这个变量是否可以通过state 或props 中的已有数据计算得到?如果是,那么它不是一个状态。
  4. 这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个普通属性(除了props 和 state以外的组件属性 ),例如组件中用到的定时器,就应该直接定义为this.timer,而不是this.state.timer。

请务必牢记,并不是组件中用到的所有变量都是组件的状态!当存在多个组件共同依赖同一个状态时,一般的做法是状态上移,将这个状态放到这几个组件的公共父组件中。

如何正确修改State

1.不能直接修改State。

直接修改state,组件并不会重新重发render。例如:

// 错误
this.state.title = 'React';

正确的修改方式是使用setState():

// 正确
this.setState({title: 'React'});

2. State 的更新是异步的。

调用setState,组件的state并不会立即改变,setState只是把要修改的状态放入一个队列中,React会优化真正的执行时机,并且React会出于性能原因,可能会将多次setState的状态修改合并成一次状态修改。所以不能依赖当前的state,计算下个state。当真正执行状态修改时,依赖的this.state并不能保证是最新的state,因为React会把多次state的修改合并成一次,这时,this.state还是等于这几次修改发生前的state。另外需要注意的是,同样不能依赖当前的props计算下个state,因为props的更新也是异步的。

举个例子,对于一个电商类应用,在我们的购物车中,当点击一次购买按钮,购买的数量就会加1,如果我们连续点击了两次按钮,就会连续调用两次this.setState({quantity: this.state.quantity + 1}),在React合并多次修改为一次的情况下,相当于等价执行了如下代码:

Object.assign(previousState,{quantity: this.state.quantity + 1},{quantity: this.state.quantity + 1}
)

于是乎,后面的操作覆盖掉了前面的操作,最终购买的数量只增加了1个。

如果你真的有这样的需求,可以使用另一个接收一个函数作为参数的setState,这个函数有两个参数,第一个参数是组件的前一个state(本次组件状态修改成功前的state),第二个参数是组件当前最新的props。如下所示:

// 正确
this.setState((preState, props) => ({counter: preState.quantity + 1; 
}))

3. State 的更新是一个浅合并(Shallow Merge)的过程。

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

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

当只需要修改状态title时,只需要将修改后的title传给setState

this.setState({title: 'Reactjs'});

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

{title : 'Reactjs',content : 'React is an wonderful JS library!'
}

State与Immutable

React官方建议把state当作不可变对象,一方面是如果直接修改this.state,组件并不会重新render;另一方面state中包含的所有状态都应该是不可变对象。当state中的某个状态发生变化,我们应该重新创建一个新状态,而不是直接修改原来的状态。那么,当状态发生变化时,如何创建新的状态呢?根据状态的类型,可以分成三种情况:

1. 状态的类型是不可变类型(数字,字符串,布尔值,null, undefined)

这种情况最简单,因为状态是不可变类型,直接给要修改的状态赋一个新值即可。如要修改count(数字类型)、title(字符串类型)、success(布尔类型)三个状态:

this.setState({count: 1,title: 'Redux',success: true
})

2. 状态的类型是数组

如有一个数组类型的状态books,当向books中增加一本书时,使用数组的concat方法或ES6的数组扩展语法(spread syntax):

// 方法一:使用preState、concat创建新数组
this.setState(preState => ({books: preState.books.concat(['React Guide']);
}))// 方法二:ES6 spread syntax
this.setState(preState => ({books: [...preState.books, 'React Guide'];
}))

当从books中截取部分元素作为新状态时,使用数组的slice方法:

// 使用preState、slice创建新数组
this.setState(preState => ({books: preState.books.slice(1,3);
}))

当从books中过滤部分元素后,作为新状态时,使用数组的filter方法:

// 使用preState、filter创建新数组
this.setState(preState => ({books: preState.books.filter(item => {return item != 'React'; });
}))

注意不要使用push、pop、shift、unshift、splice等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat、slice、filter会返回一个新的数组。

3. 状态的类型是简单对象(Plain Object)

如state中有一个状态owner,结构如下:

this.state = {owner = {name: '老干部',age: 30}  
}

当修改state时,有如下两种方式:

1) 使用ES6 的Object.assgin方法

this.setState(preState => ({owner: Object.assign({}, preState.owner, {name: 'Jason'});
}))

2) 使用对象扩展语法(object spread properties)

this.setState(preState => ({owner: {...preState.owner, name: 'Jason'};
}))

总结一下,创建新的状态的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。当然,也可以使用一些Immutable的JS库,如Immutable.js,实现类似的效果。

那么,为什么React推荐组件的状态是不可变对象呢?一方面是因为不可变对象方便管理和调试,了解更多可参考这里;另一方面是出于性能考虑,当组件状态都是不可变对象时,我们在组件的shouldComponentUpdate方法中,仅需要比较状态的引用就可以判断状态是否真的改变,从而避免不必要的render方法的调用。当我们使用React 提供的PureComponent时,更是要保证组件状态是不可变对象,否则在组件的shouldComponentUpdate方法中,状态比较就可能出现错误。

下篇预告:

React 深入系列4:组件的生命周期


新书推荐《React进阶之路》

作者:徐超

毕业于浙江大学,硕士,资深前端工程师,长期就职于能源物联网公司远景智能。8年软件开发经验,熟悉大前端技术,拥有丰富的Web前端和移动端开发经验,尤其对React技术栈和移动Hybrid开发技术有深入的理解和实践经验。



美团点评广告平台大前端团队招收20192020年前端实习生(偏动效方向)

有意者邮件:yao.zhou@meituan.com


http://lihuaxi.xjx100.cn/news/273449.html

相关文章

如何采集Nginx的日志?

点击上方“方志朋”,选择“设为星标”回复”666“获取新整理的面试文章由于nginx功能强大,性能突出,越来越多的web应用采用nginx作为http和反向代理的web服务器。而nginx的访问日志不管是做用户行为分析还是安全分析都是非常重要的数据源之一…

html从入门到精通前锋,街篮新手攻略 从入门到精通的心得分享二

街篮毕竟是一款竞技手游,上期介绍了街篮的一些玩法和基本技巧,本期就不再提介绍而是针对实战,以下就是将街篮的实战技巧分享给大家,希望对大家了解街篮有所帮助。(本文为超好玩原创攻略,转载请注明出处)推荐攻略&#…

计算机视觉 | 哥大读博五年总结

点击上方“小白学视觉”,选择加"星标"或“置顶”重磅干货,第一时间送达本文转自|计算机视觉联盟「 开始写这边总结的时候是三月,纽约成了疫情震中,看着新闻报道里的中央公园,中国城,第五大道&…

Chapter 0: 引论

引论我之前就看过了,在我刚买到这本书的时候。 而我买这本书的日子,已经是两年前了。我就是这样子的,我买了好多好多关于技术的书,这些书都是很贵很贵的,可是买完回来之后就看了第一章,然后就一直丢在一边&…

我在MongoDB年终大会上获二等奖文章:由数据迁移至MongoDB导致的数据不一致问题及解决方案...

作者 | 上海小胖来源 | Python专栏(ID:xpchuiit)故事背景企业现状2019年年初,我接到了一个神秘电话,电话那头竟然准确的说出了我的昵称:上海小胖。我想这事情不简单,就回了句:您好,我是小胖&…

容器开启数据服务之旅系列(二):Kubernetes如何助力Spark大数据分析

摘要: 容器开启数据服务之旅系列(二):Kubernetes如何助力Spark大数据分析 (二):Kubernetes如何助力Spark大数据分析 概述 本文为大家介绍一种容器化的数据服务Spark OSS on ACK,允许…

【python教程入门学习】Python爬虫入门学习:网络爬虫是什么

网络爬虫又称网络蜘蛛、网络机器人,它是一种按照一定的规则自动浏览、检索网页信息的程序或者脚本。网络爬虫能够自动请求网页,并将所需要的数据抓取下来。通过对抓取的数据进行处理,从而提取出有价值的信息。 认识爬虫 我们所熟悉的一系列搜…

再见QQ,再见QQ游戏!

整套源码包括:SQLServer数据库安装文件、数据库建库建表sql、服务器端整套源码(含完整核心引擎源码)、105种客户端游戏类型。这套源码含有的游戏类型如下:代码编译和部署方式整套源码我已经全部整理好了,服务端各个服务…