想要理解数据是如何在组件树内自上往下流动(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
,从而达到更新的效果。如下:
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 排版