1、添加外部数据 props
和 Vue 类似,React 的组件之间也需要进行传值。父组件需要在 子组件的占位符 上通过 赋值 的形式将值传递给子组件,子组件通过 props 属性拿到父组件传递的值,但是函数组件和类组件在使用外部数据时会有所不同:
类组件 可在其 constructor 接收父组件传递来的数据,直接通过 this.props.xxx 读取 父组件传递给子组件的 xxx 信息。
函数组件 可在其 参数 中接收到父组件传递来的数据,然后直接 读取 参数 props.xxx 即可。
在组件传值时,无论是使用函数组件还是类组件,如果是字符串用 " ",如果是变量用:{ }。下面我们看一个例子:
import React from "react"; import ReactDOM from "react-dom"; function App() { let n = 1; return ( <div className="App"> 爸爸 <Son messageForSon="儿子你好" /> <!--如果给子组件需要传递变量:messageForSon={n}--> </div> <!--如果给子组件需要传递函数:onClick={this.onClick}--> ); <!--外部数据被包装为一个对象{messageForSon:'',...}--> } class Son extends React.Component { <!--类组件--> constructor(props){ <!--如果没有额外的代码,这三行可省略,不写也可以拿到外部数据--> super(props); <!--如果没有额外的代码,这三行可省略,不写也可以拿到外部数据--> } <!--这样做的效果就是,this.props就是外部数据的地址了--> render() { return ( <div className="Son"> 我是儿子,我爸对我说「{this.props.messageForSon}」<!--this.props.xxx 获取外部数据--> <Grandson messageForGrandson="孙贼你好" /> <!--如果给子组件需要传递变量:messageForSon={n}--> </div> ); } } const Grandson = props => { <!--函数组件可以接收外部传来的 props,这里的props是形参--> return ( <div className="Grandson"> 我是孙子,我爸对我说「{props.messageForGrandson}」<!--props.xxx 获取外部数据--> </div> ); }; const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
概括 props 的作用:props 是用来接收父组件传递来的数据,但是这个数据是只读的,不能改写。当然 props 不仅可以接收外部数据,它还可以接收 外部函数,该函数一般是父组件传递过来的,子组件可以在恰当的时机对其进行调用。
2、使用内部数据 state
Vue 的组件内部数据声明在其构造选项 options 的 data 属性中,通过 {{ }} 和 this.xxx 对内部数据进行读写。React 的内部数据则是需要用 state 声明,和使用外部数据 props 一样,React 中类组件和函数组件在使用内部数据时是有所不同的:
类组件 在 constructor() 构造器中通过 this.state = { 键值对 } 的形式声明内部数据。在组件内通过 this.state.xxx 的方式读取组件的内部数据,使用 this.setState({ 键值对 }) 改写组件内部数据。注意:this.setState() 是异步的,不会立即改变 n。
函数组件 用 React.useState(初始值) 返回数组的方式来声明内部数据:const [n, setN] = React.useState(0);。其中数组的第一项为其内部数据 n,第二项 setN() 是改写数据 n 的方法。注意:setN() 是异步的,而且它会声明一个新的 n 覆盖旧的 n。
import React from "react"; import ReactDOM from "react-dom"; function App() { return ( <div className="App"> 爸爸 <Son /> </div> ); } class Son extends React.Component {//类组件 constructor() { //需要在constructor()构造器中 super(); this.state = { //通过 this.state = {} 的形式声明内部数据 n: 0 }; } add() { //this.state.n += 1 不行,不像Vue this.setState({ n: this.state.n + 1 }); //因为React没有对内部数据做监听,只能调用 this.setState() //最好使用这种:this.setState( (state) =>{ return state.n + 1}); } //因为这种方式写两遍可以实现 +2,但是对象的方式却不行 render() { return ( <div className="Son"> 儿子 n: {this.state.n} <!--类组件通过 this.state.n 的形式读数据--> <button onClick={() => this.add()}>+1</button> <Grandson /> </div> ); } } const Grandson = () => { const [n, setN] = React.useState(0); return ( <div className="Grandson"> 孙子 n:{n} <button onClick={() => setN(n + 1)}>+1</button> </div> ); }; const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
补充:为什么在 React 的类组件中,不能通过 this.state.xxx +=1;这种形式改写其内部属性?因为 React 不像 Vue 对其内部数据进行了篡改和监听,一旦监听到数据改变就会去刷新视图。React 如果通过 this.state.xxx +=1;这种形式改写数据,视图是不知道的,自然就不会刷新视图了,所以 React 需要通过 this.setState({ 键值对 }) 的形式改写组件内部数据并刷新视图。
建议:在 class 类组件中,如果需要 this.setState() 改写数据,其内部最好传递一个 函数,让函数去改写数据。比如:this.setState({ n: this.state.n + 1 }); 最好能改写成 this.setState( (state) => { return { n: state.n + 1 } }); 的形式。这是因为 this.setState() 是一个异步更新 UI 的过程,如果让你在 this.setState() 下打印 n 的值,代码会先执行打印操作,再去更新 UI,这就导致你拿到的 n 是一个旧的值。如果改成函数的形式,你就可以再函数内部进行打印 n 的操作,如:this.setState( (state) => { const n = state.n+1; return { n: n }); 。
React 的理念是 数据不可变,它会通过 setN() 声明 新数据,而 Vue 的理念是 数据响应式,它提倡改变原数据。
3、复杂 state 的具体使用
上面简单介绍了在类组件和函数组件中 state 的声明及读写方法,但是如果 state 中的数据比较复杂,则情况会有些不一样,比如 state 中有多种类型的数据,包括对象类型,对象中还包括一些简单数据。下面我们来具体分析一下。
类组件 中如果 state 含有多个数据,在改变其中一个数据值的情况下,其它数据会默认自动 延用 之前的值。需要注意的是,上述这种延用机制并不是绝对的,类组件的 setState() 只能自动合并第一层属性(shallow merge): React 只会检查新 state 和旧 state 第一层的区别,并把新 state 缺少的数据从旧 state 里拷贝过来。比如 state 中有一个 person 对象,对象中有 name 和 age 两个属性。这种情况下如果只通过 setState() 改写 name 属性,则 age 属性会被设置为 undefined。
解决第二层合并的问题,可以在 person 对象内部属性前写:...this.state.person 。或者在修改属性的函数内部写上:const user=Object. assign({ }, this. state. user); 或 const user={ ...this.state.user }; 代码。
class Son extends React.Component { constructor() { super(); this.state = { //state中含有 m 和 n 两个数据 n: 0, m: 0 }; } addN() { this.setState({ n: this.state.n + 1 }); //m 不会被覆盖为 undefined,会自动延用之前的值 } //或者写成:this.setState({...this.state , n: this.state.n + 1 }) addM() { this.setState({ m: this.state.m + 1 }); //n 不会被覆盖为 undefined,会自动延用之前的值 } //或者写成:this.setState({...this.state , m: this.state.m + 1 }) render() { return ( <div className="Son"> n: {this.state.n} <button onClick={() => this.addN()}>n+1</button> <!--点击按钮调用 addN() 使 n+1--> m: {this.state.m} <button onClick={() => this.addM()}>m+1</button> <!--点击按钮调用 addM() 使 m+1--> </div> ); } }
函数组件 中如果 state 中有多个数据。需要分情况讨论。如果是下面第一种声明变量的形式(m 和 n 分开),那就单个设置就好了,不会有别的问题。如果将数据声明在一个对象中的话({ n:0,m:0 }),则在改变其中一个数据值的情况下,其它数据 不会 默认自动 延用 之前的值,如果非要使用这种方式,可以在 setState() 时将 state先拷贝一份:setState({...state,n:state.n+1})。
函数组件在二级属性上也是不会进行合并的,如果修改一个对象类型的数据的第一个属性值,其余的会被置为 undefined,需要开发者手动使用 ... 操作符进行手动合并。
const Grandson = () => { const [n, setN] = React.useState(0); //不推荐将 m、n 写到一个对象中 const [m, setM] = React.useState(0); //那样在设置单个属性时,另一个会被置为 undefined return ( //const [state, setState] = React.useState({n:0,m:0}); 不推荐 <div className="Grandson"> //如果这种形式,在每次setState时需要:setState({...state,n:state.n+1}) n:{n} <button onClick={() => setN(n + 1)}>n+1</button> m:{m} <button onClick={() => setM(m + 1)}>m+1</button> </div> ); };
4、关于两种组件的注意事项
类组件中 this.state.n += 1无效?
解释:其实经过上述这行代码之后的 n 已经改变了,只不过 UI 不会自动更新而已,需要调用 setState() 才会触发 UI 更新(异步更新)。因为 React 没有像 Vue 监听 data 一样监听 state,React 不知道数据什么时候发生改变,所以只能开发人员手动调用 seteState() 刷新视图。
类组件中的 setState() 是异步更新 UI 的
在调用 setState() 之后,state 不会马上改变,立马读取 state 其实是旧的值。更推荐的方式是 setState(函数) 的形式。
类组件 this.setState(this.state)不推荐?
React 的理念是数据不可变,它不推荐开发人员修改旧的 state(不可变数据),然后将修改完的 this.state 传递给 setState() 。而是推荐这种 setState({ n:this.state.n+1 })方式,声明新的 n 数据。
函数组件 setx(新值)
函数组件和类组件在改写内部数据方面比较相似,也要推荐开发人员通过 setx(新值)来更新 UI。
函数组件跟类组件不同的地方
类组件中无论是使用数据还是调用方法,都需要使用 this 关键字,而函数组件没有 this,一律用参数和变量。