写在前面
无论是 React 还是 vue,都需要用到数据,作为一个组件,与其相关的数据有两个,一个是内部数据,一个是外部数据。内部数据无疑是在定义组件时其内部定义和使用的数据,外部数据是在组件使用时,通过标签属性的方式传递给组件的数据。和 Vue 的数据对应着介绍,React 的内部数据是 state,和 Vue 的 data 数据相对应,都是组件内部数据。React 的外部数据是 props (property的复数简写),和 Vue 的 props 一样,都是组件接受的外部数据。
React 中的两种组件的数据的使用方式有所不同,因此在特定的组件使用特定的数据定义和获取方式。
关注数据主要关注三个点:如何初始化数据、如何读数据、如何写数据。
1. 外部数据 props
1.1 如何传递外部数据(初始化 props)
无论是在 vue 中还是 react 中,组件的使用都是以 XML 语法的自定义标签的方式使用,如
<Welcome></Welcome>
<Welcome />
因此在组件使用时传参,只能通过标签全局属性的方式传递,在组件内部用 props 对象以键值对的方式接受全部的属性数据。如:
<Welcome name="hello" msg="i am msg" />
当要传递的属性值为变量时,是用 { } 括起来写 js 语法即可。如
const a = 10;
return(
<Welcome name={a} />
)
1.2 类组件(读 props)
this.props
类组件使用外部数据时不需要额外传参,React 已经自动绑定到类组件的 this.props 上,因此,直接从 this.props 上获取使用即可。获取的键值的键名和传入时的键名必须一样,一一对应,如
class Welcome extends React.Component{
render(){
return (
<div className="wel">
外部数据name: { this.props.name }
外部数据msg: { this.props.msg }
</div>
)
}
}
1.3 函数组件(读 props)
function xxx (props){ props.xx } ,直接参数 props 获取
函数组件里没有 this,React 将外部数据 props 对象作为函数组件的第一个参数传入,在函数组件内接受第一个参数,一般命名为 props ,和外部数据 props 对象一样的名字,然后在函数组件内部使用。如
function Welcome(props){
return (
<div className="wel">
外部数据name: { props.name }
外部数据msg: { props.msg }
</div>>
)
}
1.4 外部数据不可写
外部数据只能由定义它的组件写,在使用它的组件内部只有使用权,没有修改权。
2. 内部数据 state
既然是内部数据,那么就不像外部数据一样可以传递出去,内部数据是在组件内部定义,并在组件内部使用的数据。
2.1 类组件
(1) 初始化 state
每个组件都有自己独立的内部数据,因此在类组件中,state 内部数据是放在构造函数中作为私有属性定义的。
class Welcome extends React.Component{
/*初始化*/
constructor(){
super();
this.state = {
n: 0
}
}
/*初始化*/
render(){
return (
<div>
/*读数据*/
n: {this.state.n}
/*读数据*/
</div>
)
}
}
(2) 读 state
读 state 就是上面的在 render() 函数中使用 this.state 的方式读取。
(3) 写 state
React 的写内部数据 state 就比较绕脑了,我们在前面的 React初体验 中有介绍,React 是不会像 Vue 一样对数据进行监听,并在数据进行变化时刷新视图的,React 采取的方式是,在数据变化需要更新视图时,重新调用渲染视图的 render 函数,将视图重新渲染。因此需要手动调用 render 函数。
但有了 babel-loader 以后,手动 render 的过程被 babel-loader 做了,babel 提供给 jsx 一些可以自动调用 render 的 API,其中的 写 state 的 API:setState() 就是一个典型的用在类组件中写内部数据的接口,调用该接口,会自动 render。
class Welcome extends React.Component{
/*初始化*/
constructor(){
super();
this.state = {
n: 0
}
}
/*初始化*/
/*写state*/
add(){
//this.state.n += 1; 该种方式不行,React 不会自动render,必须使用特定的 API
/*
this.state.n += 1;
this.setState(this.state);
*///该种方式不推荐,最好不要改变以前的对象,当数据改变时产生一个新的对象,
//最好遵循数据不可变原则,新对象容纳新的数据。
this.setState({n: this.state.n+1});
}
/*写state*/
render(){
return (
<div>
/*读数据*/
n: {this.state.n}
/*读数据*/
<button onClick={()=>this.add()}>+1</button>
</div>
)
}
}
PS补充:
实际上,setState 函数是一个异步函数,不是语句执行时就立即执行的,因此会受到一些误解,如:
add(){
this.setState({n: this.state+1});
console.log(this.state.n);//得到的不是加一后的值,是旧的state,因为 setState 是异步的
//不会立马执行,而是在其所在作用域的语句都执行完后再调用执行,因此是先输出再调用的
}
异步函数 setState 的调用时间不确定,因此前端大佬们都统一使用了一种新的方式 setState,就是使用函数
add(){
this.setState(state=>{
return {n: this.state+1});
}//此处的state是传递过来的旧的state对象,返回一个新的对象
this.setState(state=>{
const n = state.n + 1;
console.log(n);//要想输出是修改后的,就直接先修改,再输出修改的值,这才是正确的修改得到的值
//return {n:n};当键和值名字一样时可简写
return {n}
})
}
这种使用函数的方式是在改变的过程中输出改变,而不是在异步函数语句后输出改变,不会混淆,因此,当数据简单时直接用对象,当复杂时用函数
2.2 函数组件
函数组件的 state 数据的设置和获取是提供了一个接口 React.useState(初始值),用于设置 state 的初始值,函数里面的参数为初始值,函数返回一个数组,数组的第一个是读数据方式,第二个是写数据方式。读数据变量命名为 xx 时,写数据方式一般命名为 setXX。
const Welcom = ()=>{
const [n,setN] = React.useState(0);//析构赋值
//等价于
/*
const state = React.useState(0);
const n = state[0];
const setN = state[1];
*/
return (
<div className="wel">
n: {n}
<button onClick={()=>setN(n+1)}>+1</button>
</div>
)
}
同样的,setN 也是一个异步函数,不会立马执行。和 setState 不同的是,setState 是等一会后会改变 state ,而 setN 是永远不会改变 n,而是产生新的 n。
3. 复杂 state 的处理
上面的 state 对象都是有一个数据属性的情况,那么 state 对象有多个数据属性时该如何处理?也是要分成类组件和函数组件来说。
3.1 类组件
class Welcome extends React.Component{
constructor(){
super();
this.state = {
n: 0,
m: 0
}
}
addN(){
this.setState({n: this.state.n + 1});
//此处虽然没有设置 m 属性,但是 m 属性不会被设置成 undefined ,会使用上次的值
}
addM(){
this.setState({m: this.state.m + 1})
}
render(){
return (
<div className="wel">
n: {this.state.n}
<button onClick={() => this.addN()}> n+1 </button>
m: {this.state.m}
<button onClick={() => this.addM()}> m+1 </button>
</div>
)
}
}
因此,setState() 函数会先将之前的 state 对象复制一份,然后会根据设置的新的 state 对象来使用 diff 算法对比,并进行智能合并,从而得到新的 state。所以,在类组件中,对于复杂 state 的处理只需要改变自己需要改变的即可,即对 state 进行局部修改会智能合并。
注意:类组件的 setState() 只能自动合并第一层属性,不能自动合并第二层属性,也就是说第二层赋值时如果不写全就会出现数据丢失。因此要改变第二层属性,要么手动都设置值,要么将之前的属性使用 … 展开符拷贝一份,或者使用 Object.assign() 将之前的对象拷贝一份
class Welcome extends React.Component{
constructor(){
super();
this.state = {
n: 0,
user: {
/*这是重点,拷贝一份*/
...this.state.user,
/*这是重点,拷贝一份*/
name: "jack",
age: 18
}
}
}
changeUser(){
/*或者使用 Object.assign 方法拷贝*/
const user = Object.assign({},this.state.user);
/*或者使用 Object.assign 方法拷贝*/
user.name = "rose";
}
}
3.2 函数组件
(1)推荐写法
const Welcom = ()=>{
const [n,setN] = React.useState(0);
const [m,setM] = React.useState(0);
return (
<div className="wel">
n: {n}
<button onClick={()=>setN(n+1)}>+1</button>
m: {m}
<button onClick={()=>setM(m+1)}>+1</button>
</div>
)
}
(2)不推荐写法
//错误写法,该种方式的 setState 不会智能合并,不写n,n就会成undefined
const Welcom = ()=>{
const [state,setState] = React.useState({
n: 0,
m: 0
});
return (
<div className="wel">
n: {state.n}
<button onClick={()=>setState(state.n+1)}>+1</button>
m: {state.m}
<button onClick={()=>setState(state.m+1)}>+1</button>
</div>
)
}
//需要手动拷贝旧的 state
const Welcom = ()=>{
const [state,setState] = React.useState({
n: 0,
m: 0
});
return (
<div className="wel">
n: {state.n}
/*正确修改 ...state 展开符传入旧的state*/
<button onClick={()=>setState(...state, state.n+1)}>+1</button>
m: {state.m}
<button onClick={()=>setState(...state, state.m+1)}>+1</button>
/*正确修改*/
</div>
)
}
3.3 复杂 state 的处理总结
- 类组件的 setState 会自动合并第一层属性,第二层属性的合并需要手动合并
- 函数组件的 setX 完全不会自动合并,需要手动合并
- 手动合并拷贝之前的 state 的方式为 …操作符 或者 Object.assign()