一、组件的组和、组件树
想要理解数据是如何在组件树内自上往下流动(Data Flow
),需要先明白React
组件的组合、嵌套和组件树的构成。
注意:自定义的组件都必须要用大写字母开头,普通的
HTML
标签都用小写字母开头。
下面是用React.js
组建的房子为例子来说明一下组件的组和应用。一个房子里面有一个房间和一个厕所。房间里有一台电脑和两条狗,如下:
// 电脑
class Computer extends Component {
render () {
return <h3>this is Computer</h3>
}
}
// 狗
class Dog extends Component {
render () {
return <h3>this is Dog</h3>
}
}
// 房间
class Room extends Component {
render () {
return (
<div>
<h2>this is Room</h2>
<Computer />
<Dog />
<Dog />
</div>
)
}
}
// 厕所
class WC extends Component {
render () {
return <h2>this is WC</h2>
}
}
// 房子
class House extends Component {
render () {
return <div>
<h1>this is House</h1>
<Room />
<WC />
</div>
}
}
复制代码
最后页面会输出如下图:
组件可以和组件组合在一起,组件内部可以使用别的组件。这样的组合嵌套,最后构成一个所谓的组件树。用下图表示这种树状结构它们之间的关系:
二、组件的state
state
是组件的当前状态,React
根据状态state
呈现不同的UI
展示。React.js
的state
就是用来存储这种可变化的状态的。一旦状态(数据)更改,组件就会自动调用render
重新渲染 UI
,这个更改的动作通过this.setState()
方法来触发。
下面是一个简单的修改状态的小例子:
class Like extends Component {
constructor(props) {
super(props)
this.state = { isLike: false }
}
handleClick () {
this.setState({
isLike: !this.state.isLike
})
}
render () {
return (
<h3 onClick={this.handleClick.bind(this)}>
你{this.state.isLike ? '喜欢' : '不喜欢'}她
</h3>
)
}
}
复制代码
isLike
存放在实例的state
对象当中,这个对象在构造函数里面初始化。在这个组件的render
函数内,会根据组件的state
的中的isLike
的不同显示喜欢或不喜欢。setState()
方法由父类Component
所提供。
正确的使用state
和setState()
方法
setState()
方法,它接受一个对象或者函数作为参数。
对象作为参数时
handleClick () {
// 只需要传入需要更新的部分,而不需要入整个对象。
this.setState({
isLike: !this.state.isLike
})
}
复制代码
函数作为参数时
handleClick () {
this.setState(() => {
return { isLike: !this.state.isLike }
})
}
复制代码
不能直接修改state
不能直接修改state
,如果直接修改state
,组件并不会重新触发render
方法。
// 错误
this.state.isLike = !this.state.isLike
// 正确
this.setState({
isLike: !this.state.isLike
})
复制代码
state
的更新是异步的
在调用setState()
的时候React.js
并不会马上修改state
。setState()
只是把要修改的状态放入一个更新队列中。React会优化真正的执行时机。如下:
class AddCount extends Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick () {
this.setState({ count: this.state.count + 1 })
// 0
console.log(this.state.count)
}
render () {
return (
<div>
{/* 1 */}
<h2>{this.state.count}</h2>
<button onClick={this.handleClick.bind(this)}>添加</button>
</div>
)
}
}
复制代码
当点击添加按钮的时候页面输出的是1
,打印出来的却是0
。所以在调用setState()
之后,this.state
不会立即映射为新的值。
解决方案是setState(updater, [callback])
接收更新后可以传入一个回调函数,一旦setState()
完成并且组件重绘之后,这个回调函数将会被调用。
handleClick () {
this.setState({ count: this.state.count + 1 }, () => {
// 1
console.log(this.state.count)
})
}
复制代码
setState()
浅合并
React.js
出于性能原因,可能会将多次setState()
的状态修改合并成一次状态修改。所以不要依赖当前的setState()
计算下个State
。如下的一个计数器:
class AddCount extends Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick () {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
}
render () {
return (
<div>
<h2>{this.state.count}</h2>
<button onClick={this.handleClick.bind(this)}>添加</button>
</div>
)
}
}
复制代码
当点击添加按钮的时候,会发现页面输出的是1
。虽然我们setState()
方法调用了两次。是因为当我们调用setState()
修改组件状态时,组件state
的更新其实是一个浅合并的过程,相当于:
Object.assign(
previousState,
{count: state.count + 1},
{count: state.count + 1},
...
)
复制代码
所以如果后续操作要依赖前一个setState()
的结果的情况下就要使用函数来作为setState()
参数。React.js
会把上一个setState()
的结果传入这个函数,你就可以使用上一个正确的结果进行操作,然后返回一个对象来更新state。
handleClick () {
this.setState({ count: this.state.count + 1 })
this.setState((prevState) => {
// 1
console.log(prevState.count)
return { count: prevState.count + 1 }
})
}}
复制代码
把上次操作setState()
更新的值传入到下一个setState()
里,就可以正确的显示count
了。
state
的Immutable
(不可变性)
React
官方建议把state
当作是的Immutable
(不可变性)对象,state
中包含的所有状态都应该是不可变对象。当state
中的某个状态发生变化,我们应该重新创建这个状态对象,而不是直接修改原来的状态。
关于
Immutable
可以参考Immutable详解及React中实践。
如有一个数组类型的状态 names
,当向name
中添加一个名字时,使用数组的concat
方法或ES6
的扩展运算符:
// 1
this.setState(prevState => ({
names: prevState.names.concat(['lisi'])
}))
// 2
this.setState(prevState => ({
names: [...prevState.names,'lisi']
}))
复制代码
注意不要使用
push
、pop
、shift
、unshift
、splice
等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat
、slice
等返回一个新的数组。
如有一个对象类型的状态 person
,为了不改变原本的对象,我们可以使用Object.assign
方法或者对象扩展属性:
// 1
function updatePerson (person) {
return Object.assign({}, person, { age: 20 })
}
// 2
function updatePerson (person) {
return {...person,age:20}
}
复制代码
创建新的状态对象要避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。
当处理深层嵌套对象时,以immutable
(不可变)的方式更新会很麻烦。可以使用一些Immutable
的JS库,如Immutable.js
来简化开发。
三、组件的props
组件是相互独立、可复用的单元,一个组件可能在不同地方被用到。但是在不同的场景下对这个组件的需求可能会根据情况有所不同,所以要针对相同的组件传入不同的配置项。
React.js
的props
就可以帮助我们达到这个效果。每个组件都可以接受一个props
参数,它是一个对象,包含了所有你对这个组件的配置。
下面是一个可以通过传入props
来控制是不是可以点击修改喜欢或不喜欢的文字:
class Like extends Component {
constructor(props) {
super(props)
this.state = { isLike: false }
}
handleClick () {
this.setState({
isLike: !this.state.isLike
})
}
render () {
const clickable = this.props.clickable
return (
<h3 onClick={clickable ? this.handleClick.bind(this) : () => { }} >
你{ this.state.isLike ? '喜欢' : '不喜欢'}她
</h3>
)
}
}
class App extends Component {
render () {
return (
<div>
<Like clickable={true} />
<Like clickable={false} />
</div>
)
}
}
复制代码
组件内部是通过this.props
的方式获取到组件的参数的,如果this.props
里面有需要的属性我们就采用相应的属性,没有的话就用默认的属性。在使用一个组件的时候,可以把参数放在标签的属性当中,所有的属性都会作为props`对象的键值。
默认配置defaultProps
React.js
也提供了一种方式defaultProps
,可以方便的做到默认配置。
class Like extends Component {
static defaultProps = {
clickable: true
}
//...
}
复制代码
props 不可变
props
一旦传入进来就不可以在组件内部对它进行修改。但是可以通过父组件主动重新渲染的方式来传入新的props
,从而达到更新的效果。如下:
class App extends Component {
constructor(props) {
super(props)
this.state = { clickable: false }
}
handleClickOnChange () {
this.setState({
clickable: !this.state.clickable
})
}
render () {
return (
<div>
<Like clickable={this.state.clickable} />
<Like clickable={this.state.clickable} />
<button onClick={this.handleClickOnChange.bind(this)}>
修改clickable
</button>
</div>
)
}
复制代码
参考
https://zh-hans.reactjs.org/
https://juejin.im/entry/59522bdb6fb9a06b9a516113
https://github.com/camsong/blog/issues/3
https://github.com/lcxfs1991/blog/issues/8
http://huziketang.mangojuice.top/books/react/
https://dev.to/rleija_/master-the-art-of-react-state-and-props-in-5-minutes-3hao
http://product.dangdang.com/25249546.html
本文使用 mdnice 排版