在React中有两种数据类型: props和state。props是从外层组件传进来的数据,而state则是当前组件自己内部维护的数据。
我们会参考下面的文档来讲解这两者的相同点和不同点。
https://github.com/uberVU/react-guide/blob/master/props-vs-state.md
共同点
从React组件的层次来看他们两者的共同点。
1. React组件的主要职责是将原始数据转换为丰富的HTML。在这一点上,props和state都是导出HTML的原始数据。
2. props和state都是确定性的,如果我们写的组件为同一props和state的组合生成了不同的输出,那么我们肯定在哪里做错了。
3. props和state更改都会触发渲染更新,这里我们讨论同一个组件内的props和state,即props是从外层组件获取的,而state是当前组件自己维护的。
对于这个点,我们既可以看作是共同点,也可以看作是不同点。因为虽然他们都会触发渲染更新,但是如何更改的机制却是不一样的。
4. props和state都是纯JS对象(对象字面量,{},我们会简称为对象;对于[],我们会简称为数组),我们可以用typeof来判断他们,结果都是object。
不同点
不同点会通过参考文档中的一个表格来讲解,这个表格通过六个问题来阐述props和state的不同点,我们会对六个问题进行总结,然后对总结的四个问题进行一一讲解。
Question props state
Can get initial value from parent Component? Yes Yes
Can be changed by parent Component? Yes No
Can set default values inside Component? Yes Yes
Can change inside Component? No Yes
Can set initial value for child Components? Yes Yes
Can change in child Components? Yes No
- 可以从父组件得到初始值吗?
- 可以被父组件修改吗?
- 可以在组件内部设置初始值吗?
- 可以在组件内部修改吗?
- 可以给子组件设置初始值吗?
- 可以在子组件中修改吗?
在使用React和Redux的时候,我们会经常听到一个说法,要使用immutable的数据或者说不要mutate数据。如果不是用这类数据,而直接操作纯JS对象,将会引起一些问题,这主要是由引用传递造成的。我们会在immutable的数据的角度讨论上面的问题,然后在讨论几个使用纯JS对象的问题。
首先我们会问题归类,我们发现1和5是一样,对于6,我个人觉得给的答案是错误的。因为对于子组件,当前组件传递过去的props和state都变成了子组件的props,所以对于子组件来说,应该是无法区分当前组件传递过去的是props还是state的。
对于问题6,是我个人的意见,读者有不同的意见也可以和我一起探讨,一起进步。
我们将对前四个问题进行讲解,但会在一个前提下,props和state这两个js对象里面的字段对应的都是基本类型,我们只会使用string来做实例。
让我们按照第三个文章中的结构新建Chapter4.jsx,添加下面的代码:
import React from 'react';
import Example from './Example1';
export default class Chapter4 extends React.Component {
render() {
return (
<div>
<Example />
</div>
)
}
}
我们会在Example1.jsx中进行讲解。即,下面四个问题的代码会放在Example1.jsx中。
问题1: 可以从父组件得到初始值吗?
import React from 'react';
class OutSide extends React.Component {
render() {
return (
<div>
<InSide outsideProps={'I am from out side'}/>
</div>
)
}
}
class InSide extends React.Component {
constructor(props) {
// 如果某个class继承自React.Component, 我们需要在constructor函数,调用一下super函数
super(props);
this.state = {
insideState: props.outsideProps
};
}
render() {
const { insideState } = this.state;
const { outsideProps } = this.props
return (
<div>
{`insideState: ${insideState}`}
<br />
{`outsideProps: ${outsideProps}`}
</div>
)
}
}
export default OutSide;
我们可以看到InSide组件的insideState和outsideProps的初始值都是从OutSide中出入的outsideProps的值。在页面上我们可以看到InSide组件显示的两个由字符串模版生成的字符串。
因此我们可以从父组件得到初始值props和state的初始值。
问题2. 可以被父组件修改吗?
class OutSide extends React.Component {
constructor(props) {
super(props);
this.state = {
outsideState: 'before change'
};
}
changeOutsideProps = () => {
this.setState({outsideState: 'just change it'});
}
render() {
const { outsideState } = this.state;
return (
<div>
<button onClick={this.changeOutsideProps} >CHANGE</button>
<InSide outsideProps={outsideState}/>
</div>
)
}
}
这里出现的() => {}是ES6中的arrow function特性,我们会在之后的文章《普通组件》的地方具体讲解。
我们给outsideProps设置了一个初始值'before change',然后通过点击“CHANGE”按钮来修改outsideProps的值'just change it',我们可以看出InSide的props改变了,而state没有变化。
所以我们可以从父组件修改自组件的props,而不能从父组件修改自组件的state。
当如我们在componentWillReceiveProps调用setState的话,也可以通过父组件来修改state。
问题3. 可以在组件内部设置初始值吗?
import React from 'react';
class OutSide extends React.Component {
render() {
return (
<div>
<InSide />
</div>
)
}
}
class InSide extends React.Component {
constructor(props) {
super(props);
this.state = {
insideState: 'state is set inside'
};
}
render() {
const { insideState } = this.state;
const { outsideProps } = this.props
return (
<div>
{`insideState: ${insideState}`}
<br />
{`outsideProps: ${outsideProps}`}
</div>
)
}
}
InSide.defaultProps = {
outsideProps: 'props is set inside'
};
export default OutSide;
们并没有从OutSide组件传入props,而是直接通过InSide.defaultProps来设置默认的props值。因此我们可以在组件内部设置初始值的props。同时我们在constructor对state进行了初始值的设置。
因此我们可以在组件内部分别对state和props设置初始值。
问题4. 可以在组件内部修改吗?
import React from 'react';
class OutSide extends React.Component {
render() {
return (
<div>
<InSide />
</div>
)
}
}
class InSide extends React.Component {
constructor(props) {
super(props);
this.state = {
insideState: 'state is set inside'
};
}
changeOutsideProps = () => {
this.props.outsideProps = 'try set';
this.setState({insideState: 'state is changed'});
}
render() {
const { insideState } = this.state;
const { outsideProps } = this.props
return (
<div>
<button onClick={this.changeOutsideProps} >CHANGE</button>
<br />
{`insideState: ${insideState}`}
<br />
{`outsideProps: ${outsideProps}`}
</div>
)
}
}
InSide.defaultProps = {
outsideProps: 'props is set inside'
};
export default OutSide;
在上面的例子中,我们试着通过点击“CHANGE”按钮来改变和props和state。点击按钮后发现页面并没有发生变化,难道是两个都不能改变吗?让我们打开开发者工具,切换到Console标签,然后刷新页面重新点击按钮。在点击按钮后,我们发现了控制台打印了一个TypeError。
我们从error中看出outsideProps是一个readonly的属性,我们试着对它进行赋值,就会抛出异常。
让我们修改上例中的changeOutsideProps,我们用try catch语句来捕获TypeError,从而使函数继续运行下去。
changeOutsideProps = () => {
try {
this.props.outsideProps = 'try set';
} catch (e) {
console.log('属性不能被重新设置');
}
this.setState({insideState: 'state is changed'});
}
让我们打开开发者工具,切换到Console标签,然后刷新页面重新点击按钮。在点击按钮后,我们发现了控制台打印了“属性不能被重新设置”,同时页面中的state也被改成了“state is changed”。
从这两个例子中,我们可以看出props不能可以在组件内部修改,但state可以在内部修改。
当问题4遇到了引用传递
还记得我们在讲解实例强调过的“要使用immutable的数据或者说不要mutate数据”吗。如果不是使用这种数据的时候,会遇到引用传递造成的问题,同时我们还可以修改props中的数据。让我们来看下面的例子。
import React from 'react';
class OutSide extends React.Component {
constructor(props) {
super(props);
this.state = {
outsideState: {
willChange: 'outsideState before',
},
helpChange: 'what i can do'
};
}
changeOutsideState = () => {
this.setState({helpChange: 'help me render'});
}
render() {
const { outsideState } = this.state;
return (
<div>
<button onClick={this.changeOutsideState} >CHANGE OUTSIDE</button>
<br />
{outsideState.willChange}
<hr />
<InSide outsideProps={outsideState} />
</div>
)
}
}
class InSide extends React.Component {
constructor(props) {
super(props);
this.state = {
insideState: 'insideState before'
};
}
changeOutsideProps = () => {
this.props.outsideProps.willChange = 'it can be changed';
this.setState({insideState: 'it can be changed'});
}
render() {
const { insideState } = this.state;
const { outsideProps } = this.props
return (
<div>
<button onClick={this.changeOutsideProps} >CHANGE INSIDE</button>
<br />
{`insideState: ${insideState}`}
<br />
{`outsideProps: ${outsideProps.willChange}`}
<br />
</div>
)
}
}
export default OutSide;
这是一个非常有意思的例子。让我们点击“CHANGE INSIDE”按钮。
我们发现insideState和outsideProps都被修改了。让我们接着点击“CHANGE OUTSIDE”按钮。
我们发现父组件的outsideState也被改变了。因为js中对象的引用传递,导致outsideProps引用了outsideState,对outsideProps里面的字段的修改也就是对outsideState对象字段的修改。从这个例子,我们也可以看到setState会触发页面的重新渲染。
让我们注释changeOutsideProps中的函数,我们在点击“CHANGE INSIDE”按钮的时候,虽然inside组件的props中的字段变了,但是由于没有setstate,页面没有重新渲染。因为页面没有变化。
当我们点击"CHANGE OUTSIDE"按钮。这时候outsideProps和outsideState在页面中部分都变化了。对于inside组件来说,我们可以看出props的改变也会触发页面的重新渲染。
那么我们如何不让子组件修改父组件的数据,即如何解决引用传递的问题。
让我们修改OutSide组件的render方法。
render() {
const { outsideState } = this.state;
return (
<div>
<button onClick={this.changeOutsideProps} >CHANGE OUTSIDE</button>
<br />
{outsideState.willChange}
<hr />
<InSide outsideProps={JSON.parse(JSON.stringify(outsideState))} />
</div>
)
}
我们对outsideState在传递过程中包了一层JSON.parse(JSON.stringify(outsideState))的方法,这个方法会深拷贝一个对象,从而得到一个新的地址的“一模一样”的对象。但是json的两个方法嵌套性能并不是很好。
我们在平时开发的过程中,必须注意js中的引用传递及深浅拷贝问题。
浅拷贝
浅拷贝只会拷贝对象的第一层子字段们(特意加了们)。因此如果我们对于从浅拷贝得到的新对象的第一层子字段们进行重新赋值,不会对原对象造成影响。
但是如果第一层子字段们中的某个字段又是一个对象,当我们修改第一层子字段们中的某个字段指向的对象时,我们将对原来的对象造成影响。
在js中我们可以通过下面的方式实现浅拷贝(当然还有其他途径可以实现)
1. Object.assign
2.es6的spread parameter...(注意与Rest Parameter的区别,他们的样子一样,但是用的场合不一样)
3. lodash中的_.clone函数
深拷贝
深拷贝就是递归对所有的字段都进行拷贝,通过深拷贝的新对象就是和原对象完全隔离了,对新对象的任何修改都不会影响原来的对象。
在js中我们可以通过下面的方法来实现深拷贝(当然还有其他途径可以实现)
1. Object.assign的叠加使用
2. es6的spread parameter...的叠加使用
3. lodash提供的深拷贝函数_.cloneDeep
4. JSON.stringify和JSON.parse函数的叠加
5. immutable.js类库,这个是facebook公司提供的一个类库
在本章的实例中,我们一直再用setState出发页面的重新渲染,但是我们可以:
用redux完全代替setState促发页面渲染的机制
在没有引入Redux之前,我们可能会一直使用setState来将改变了的state进行重新渲染。而Redux则是利用props改变来重新渲染页面。redux通过高阶组件来用完成props和state的映射,来进行React组件的开发。这个知识点,我们会在之后介绍Reudx的文章中进行具体地讲解。