github源码地址
承接上文,现在想添加新的功能,使得该应用有更多的交互性。每当点击数字旁边的蓝色向上三角投票按钮(其实这个三角就是用来投票的地方),旁边的数字(票数)就会增加1(票)。这里就涉及到了新的知识点---React的事件。可以参考一下官方文档关于React事件的描述React Event
首先要明确一点:props是不可修改的!!!尽管一个子组件可以获取props
,但是它不能修改props
。子组件是不能拥有其 props
的,在我们的app中,父组件ProductList拥有props
,然后传递给子组件Product。React赞成的是一种称之为单项数据流的模式,也就是说,数据是自顶之下传播(propagation)
给各个组件。
我们知道父组件是通过props
的方式和子组件通信传递数据,那么子组件通过什么方式将数据传递给父组件?
我们可以用props
来向下传递一个函数,props
可以传递数字,字符串,对象,函数,甚至是组件。我们在ProductList中给每个Product上传递一个函数,每当Product投票按钮,由函数回调的方式将数据从子组件向上传递到父组件。
实战一下:
ProductList.js:
class ProductList extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
this.handleProductUpVote = this.handleProductUpVote.bind(this)
}
handleProductUpVote(productId) {
}
const productComponents = sortedProducts.map(product => {
return (
<Product
key={`product-${product.id}`}
id={product.id}
title={product.title}
description={product.description}
url={product.url}
votes={product.votes}
submitterAvatarUrl={product.submitterAvatarUrl}
productImageUrl={product.productImageUrl}
onVote={this.handleProductUpVote}/>
)
})
这样,ProductList通过props向Product传递一个函数,Product就可以用this.props
的方式调用这个函数。
这里:this.handleProductUpVote = this.handleProductUpVote.bind(this)
需要给类方法手动绑定this
,在JavaScript中,类方法是默认没有绑定this的,如果方法上没有绑定this,那么调用的时候会undefined
。当然,也可以使用ES6的箭头函数解决这个问题:
handleProductUpVote = (productId) => {
// todo...
}
然后,Product组件接受传递的函数:
handleUpVote() {
this.props.onVote(this.props.id)
}
在Product的投票按钮上绑定该事件:
constructor(props) {
super(props);
this.state = {}
this.handleUpVote = this.handleUpVote.bind(this)
}
<a onClick={this.handleUpVote}>
<i className="large caret up icon"></i>
</a>
当用户点击投票按钮的时候,会触发如下函数调用链:
- 用户点击投票按钮
- React调用Productd的
handleUpVote
-
handleUpVote
调用其props
的onVote
,onVote
方法是父组件ProductList内部的方法,这用通过回调可以将id传递给父组件
接下来要做的,就是如何更新投票的数据。
但是目前的问题是,哪里可以执行这种更新数据的操作?目前为止,我们的app没有一个存储和管理数据的地方。products.json
中的数据只是我们的样本数据,而并非app内部的数据。这就引出了一个新的概念:state。
使用state
由前面我们可以知道,props
是不可变的,且是属于父组件的。而state
是由单个组件所属的,父组件可以有自己的state
,子组件也可以有自己的state
。this.state
是组件私有的,而且我们可以使用this.setState()
来改变state
中的数据。
当一个组件的props或者state发生了变化,那么组件就会自我重新渲染。
我们可以将组件中需要变化的数据认为是有状态的。在ProductList组件中,我们需要改变products的数据(投票的数值数据),所谓我们可将products认为是有状态的(stateful)
。然后,ProductList会将这个state
数据以props
的形式传递给子组件。
现在让我们改造一下ProductList组件:
- 将products变成有状态的
- 给products添加数据
- 以props的形式传递该数据
constructor(props) {
super(props);
this.state = {
products: []
}
this.handleProductUpVote = this.handleProductUpVote.bind(this)
}
componentDidMount = () => {
this.setState({
products: products
});
}
const sortedProducts = this.state.products.sort((a, b) => a.votes - b.votes)
const productComponents = sortedProducts.map(product => {
return (
<Product
key={`product-${product.id}`}
id={product.id}
title={product.title}
description={product.description}
url={product.url}
votes={product.votes}
submitterAvatarUrl={product.submitterAvatarUrl}
productImageUrl={product.productImageUrl}
onVote={this.handleProductUpVote}/>
)
})
初始化组件的时候设置“空”(empty)
的状态是一个比较好的实践方式。然后等节点挂载好以后设置初始数据。也就是在componentDidMount
中设置具体的数据。
这里的componentDidMount
是React生命周期函数,这里的是组件挂载以后的生命周期,具体细节后面会阐述。
通过this.setState()设置state
使用this.state = xxx
的方式改变state数据有效吗?答案显然是不可以的。因为this.state=xxx
仅仅改变了state的数据,而this.setState
的工作不仅仅是改变state数据,而且还会重新渲染组件。所以,this.setState
主要有两个工作:
- 改变state数据
- 当state发生变化后重新渲染组件
更新state和不可变性(immutability)
现在,我们想要实现这个功能:当用户点击投票按钮的时候,product中的votes数据会增加。我们刚刚讨论过可以使用this.setState()
来修改state。尽管一个组件可以更新其state,但是我们应该将this.state中的对象视为不可变的。如果我们将一个数组或者对象视为不可变的,那么我们不能对其做任何修改。例如:
this.setState({nums: [1, 2, 3]})
如果我们想要在state中的num
数组添加一个4,我们可能会使用push()
来做:
this.setState({nums: this.state.nums.push(4)})
表面上来看,我们仍然保持着this.state
的不可变性。但是实际上,push()
已经修改的原有的数组:
console.log(this.state.nums) // [1, 2, 3]
this.state.nums.push(4)
console.log(this.state.nums) // [1, 2, 3, 4]
这是一个不好的实践方式。部分原因是setState()
实际上是异步的。所以不能保证React何时将更新state,重新渲染组件,具体的细节以后阐述。
那如何解决这个问题?对于数组,可以使用ES6中的拓展运算符(spread)
。对于对象,可以使用ES6中的Object.assign
。可以参考:spread运算符,Object.assign。
实践一下:
在ProductList组件中修改handleProductUpVote()
:
handleProductUpVote(productId) {
let products = [...this.state.products] // 数组的拓展运算符
products.forEach(product => {
if (product.id === productId) {
product.votes = parseInt(product.votes) + 1
}
})
this.setState({
products: products
});
}
这样我们的voting-app
就可以实现投票的功能了。而且投票数发生变化以后,会自动排序。具体可以参考github上的代码,并且运行一下。