Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React学习:状态(State) 和 属性(Props) #29

Open
pfan123 opened this issue Mar 13, 2019 · 0 comments
Open

React学习:状态(State) 和 属性(Props) #29

pfan123 opened this issue Mar 13, 2019 · 0 comments

Comments

@pfan123
Copy link
Owner

pfan123 commented Mar 13, 2019

React :元素构成组件,组件又构成应用。
React 核心思想是组件化,其中组件通过属性 (props) 和 状态 (state) 传递数据。

State 与 Props 区别

props 是组件对外的接口,state 是组件对内的接口。组件内可以引用其他组件,组件之间的引用形成了一个树状结构(组件树),如果下层组件需要使用上层组件的数据或方法,上层组件就可以通过下层组件的 props 属性进行传递,因此 props 是组件对外的接口。组件除了使用上层组件传递的数据外,自身也可能需要维护管理数据,这就是组件对内的接口 state。根据对外接口 props 和对内接口state,组件计算出对应界面的 UI。

主要区别:

  • State 是可变的,是一组用于反映组件UI变化的状态集合;
  • 而 Props 对于使用它的组件来说,是只读的,要想修改 Props,只能通过该组件的父组件修改。
    在组件状态上移的场景中,父组件正是通过子组件的 Props,传递给子组件其所需要的状态。

Props的使用

当一个组件被注入一些属性(Props )值时,属性值来源于它的父级元素,所以人们常说,属性在 React 中是单向流动的:从父级到子元素。

1.props (属性) 默认为 “true”

如果你没给 prop(属性) 传值,那么他默认为 true 。下面两个 JSX 表达式是等价的:

<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />

通常情况下,我们不建议使用这种类型,因为这会与 ES6 中的对象 shorthand 混淆 。ES6 shorthand 中 { foo } 指的是 { foo: foo } 的简写,而不是 { foo: true } 。这种行为只是为了与 HTML 的行为相匹配。
(举个例子,在 HTML 中,< input type=“radio” value=“1” disabled />< input type=“radio” value=“1” disabled=“true” /> 是等价的。JSX 中的这种行为就是为了匹配 HTML 的行为。)

2.props扩展

如果你已经有一个 object 类型的 props,并且希望在 JSX 中传入,你可以使用扩展操作符 … (JSX spread 用法) 传入整个 props 对象。这两个组件是等效的:

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

显然下面的方法更方便:因为它将数据进行了包装,而且还简化了赋值的书写

State

State 是什么

React 的核心思想是组件化,而组件中最重要的概念是 State,State 是一个组件的UI数据模型,是组件渲染时的数据依据。

状态(state) 和 属性(props) 类似,都是一个组件所需要的一些数据集合,但是state是私有的,可以认为state是组件的“私有属性(或者是局部属性)”。

如何判断是否为 State ?

组件中用到的一个变量是不是应该作为组件 State,可以通过下面的 4 条依据进行判断:

  • 这个变量是否是通过 Props 从父组件中获取 ?如果是,那么它不是一个状态。
  • 这个变量是否在组件的整个生命周期中都保持不变 ?如果是,那么它不是一个状态。
  • 这个变量是否可以通过其他状态(State)或者属性 (Props) 计算得到 ?如果是,那么它不是一个状态。
  • 这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个普通属性,例如组件中用到的定时器,就应该直接定义为 this.timer,而不是 this.state.timer。

并不是组件中用到的所有变量都是组件的状态!当存在多个组件共同依赖一个状态时,一般的做法是状态上移,将这个状态放到这几个组件的公共父组件中。

如何正确使用 State

1.用setState 修改State

直接修改 state,组件并不会重新触发 render()
// 错误
this.state.comment = 'Hello';

正确的修改方式是使用setState()

// 正确
this.setState({comment: 'Hello'});

2.State 的更新是异步的

  • 调用 setState 后,setState 会把要修改的状态放入一个队列中(因而组件的 state 并不会立即改变);
  • 之后 React 会优化真正的执行时机,来优化性能,所以优化过程中有可能会将多个 setState 的状态修改合并为一次状态修改,因而 State 更新可能是异步的。
  • 所以不要依赖当前的 State,计算下个 State。当真正执行状态修改时,依赖的this.state并不能保证是最新的State,因为 React 会把多次 State 的修改合并成一次,这时,this.state 将还是这几次 State 修改前的State。
    另外需要注意的事,同样不能依赖当前的 Props 计算下个状态,因为 Props 一般也是从父组件的 State 中获取,依然无法确定在组件状态更新时的值。

综上所述:
this.props 和 this.state 可能是异步更新的,你不能依赖他们的值计算下一个 state (状态)

例:这样 counter (计数器) 会更新失败

// 错误
this.setState({
  counter: this.state.counter + this.props.increment,
});

要弥补这个问题,使用 setState() 的另一种形式,它接受一个函数而不是一个对象。这个函数有两个参数:
(1)第一个参数: 是当前最新状态的前一个状态(本次组件状态修改前的状态)
(2)第二个参数:是当前最新的属性props

// 正确
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

//注意:下面这样是错的
this.setState((prevState, props) => { //没将{}用()括起来,所以会解析成代码块
  counter: prevState.counter + props.increment
});

如果你还不懂没关系,看下面例子:
我们现在渲染出一个button,想每点击一下,counter就+3
看下面代码:

class App extends React.Component {
  state = {
    counter: 0,
  }
  handleClick = () => {
    const { counter } = this.state;
    //或者 const counter = this.state.counter;
    this.setState({ counter: counter + 1 });
    this.setState({ counter: counter + 1 });
    this.setState({ counter: counter + 1 });
  }
  render() {
    return (
      <div>
        counter is: {this.state.counter}
        <button onClick={this.handleClick} >点我</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

每点击一下,加 +1,并不是 +3

之所以+1,不是 +3,是因为 state 的更新可能是异步的,React 会把传入多个 setState 的多个 Object “batch” 起来合并成一个。合并成一个就相当于把传入 setState 的多个 Object 进行 shallow merge,像这样:

const update = {
    counter: counter + 1,
    counter: counter + 1,
    counter: counter + 1
    //因为上面三句话都一样,所以会当一句话执行
 }

我们可以这么做就会成功:看下面

class App extends React.Component {
  state = {
    counter: 0,
  }
  handleClick = () => {
    this.setState(prev => ({ counter: prev.counter + 1 }));
    this.setState(prev => ({ counter: prev.counter + 1 }));
    this.setState(prev => ({ counter: prev.counter + 1 }));
    //这样是错的 this.setState(prev => {counter: prev.counter + 1});
    //这样是错的 this.setState(prev => {counter:++prev.counter});
    //这样是错的 this.setState(prev => {counter:prev.counter++});
  }
  render() {
    return (
      <div>
        counter is: {this.state.counter}
        <button onClick={this.handleClick} >点我</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

之所以成功是因为:传入多个 setState 的多个 Object 会被 shallow Merge,而传入多个 setState 的多个 function 会被 "queue" 起来,queue 里的 function 接收到的 state(上面是 prev )都是前一个 function 操作过的 state。

3.State更新会被合并

官方文档看不懂不要紧,直接举个例子你就懂了。

例如一个组件的状态为:

this.state = {
  title : 'React',
  content : 'React is an wonderful JS library!'
}

当只需要修改状态 title 时,只需要将修改后的 title 传给 setState:

this.setState({title: 'Reactjs'});

React 会合并新的 title 到原来的组件状态中,同时保留原有的状态 content,合并后的 State 为:

{
  title : 'Reactjs',
  content : 'React is an wonderful JS library!'
}

4.setState里顺序更新

  // history 为数组
   this.setState({
       history: history.concat([1]),  //(1)
       current: history.length,       //(2)
       nextPlayer: !nextPlayer,       //(3)
  });

执行 setState 时:先更新 history,然后再用更新改变后的 history 计算 current 的值,最后再更新 nextPlayer

根据 State 类型 更新

当状态发生变化时,如何创建新的状态?根据状态的类型,可以分成三种情况:

1.状态的类型是不可变类型(数字,字符串,布尔值,null, undefined)

这种情况最简单,直接给要修改的状态赋一个新值即可

// 原state
this.state = {
  count: 0,
  title : 'React',
  success:false
}

// 改变state
this.setState({
  count: 1,
  title: 'bty',
  success: true
})

2.状态的类型是数组

数组是一个引用,React 执行 diff 算法时比较的是两个引用,而不是引用的对象。所以直接修改原对象,引用值不发生改变的话,React 不会重新渲染。因此,修改状态的数组或对象时,要返回一个新的数组或对象。
(1)增加
如有一个数组类型的状态 books,当向 books 中增加一本书 (chinese) 时,使用数组的 concat 方法或 ES6 的数组扩展语法

// 方法一:将 state 先赋值给另外的变量,然后使用 concat 创建新数组
let books = this.state.books; 
this.setState({
  books: books.concat(['chinese'])
})

// 方法二:使用preState、concat创建新数组
this.setState(preState => ({
  books: preState.books.concat(['chinese'])
}))

// 方法三:ES6 spread syntax
this.setState(preState => ({
  books: [...preState.books, 'chinese']
}))

(2)截取
当从 books 中截取部分元素作为新状态时,使用数组的 slice 方法:

// 方法一:将state先赋值给另外的变量,然后使用slice创建新数组
let books = this.state.books; 
this.setState({
  books: books.slice(1,3)
})

// 方法二:使用preState、slice创建新数组
this.setState(preState => ({
  books: preState.books.slice(1,3)
}))

(3)条件过滤
当从 books 中过滤部分元素后,作为新状态时,使用数组的 filter 方法:

// 方法一:将state先赋值给另外的变量,然后使用filter创建新数组
var books = this.state.books; 
this.setState({
  books: books.filter(item => {
    return item != 'React'; 
  })
})

// 方法二:使用preState、filter创建新数组
this.setState(preState => ({
  books: preState.books.filter(item => {
    return item != 'React'; 
  })
}))

注意:不要使用 push、pop、shift、unshift、splice 等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而 concat、slice、filter 会返回一个新的数组。

3.状态的类型是普通对象(不包含字符串、数组)
对象是一个引用,React 执行 diff 算法时比较的是两个引用,而不是引用的对象。所以直接修改原对象,引用值不发生改变的话,React 不会重新渲染。因此,修改状态的数组或对象时,要返回一个新的对象。
使用 ES6 的 Object.assgin 方法

// 方法一:将state先赋值给另外的变量,然后使用Object.assign创建新对象
var owner = this.state.owner;
this.setState({
  owner: Object.assign({}, owner, {name: 'Jason'})
})

// 方法二:使用preState、Object.assign创建新对象
this.setState(preState => ({
  owner: Object.assign({}, preState.owner, {name: 'Jason'})
}))

使用对象扩展语法(object spread properties)

// 方法一:将state先赋值给另外的变量,然后使用对象扩展语法创建新对象
var owner = this.state.owner;
this.setState({
  owner: {...owner, name: 'Jason'}
})

// 方法二:使用preState、对象扩展语法创建新对象
this.setState(preState => ({
  owner: {...preState.owner, name: 'Jason'}
}))

综上所述: 创建新的状态对象的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。

State 向下流动

我们说 props 是组件对外的接口,state 是组件对内的接口。
一个组件可以选择将 state(状态) 向下传递,作为其子组件的 props(属性):

<MyComponent title={this.state.title}/>

这通常称为一个“从上到下”,或者“单向”的数据流。任何 state(状态) 始终由某个特定组件所有,并且从该 state(状态) 导出的任何数据 或 UI 只能影响树中 “下方” 的组件。

如果把组件树想像为 props(属性) 的瀑布,所有组件的 state(状态) 就如同一个额外的水源汇入主流,且只能随着主流的方向向下流动。

嵌套组件树生命周期

父组件:

class Parent extends PureComponent {
  constructor(props) {
    super(props);
    console.log('Parent constructor');
  }
  
  getDerivedStateFromProps() {
    console.log('Parent shouldComponentUpdate');
  }
  
  shouldComponentUpdate() {
    console.log('Parent shouldComponentUpdate');
  }

  componentDidMount() {
    console.log('Parent componentDidMount');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('Parent componentDidUpdate(prevProps, prevState)');
  }

  componentWillUnmount() {
    console.log('Parent componentWillUnmount');
  }

  render() {
    console.log('Parent render');
    return (
      <div className="root">
          <h3>This is Parent</h3>
           <Child />
      </div>
    );
  }
}

子组件:

class Child extends PureComponent {
  constructor(props) {
    super(props);
    console.log('Child constructor');
  }

  getDerivedStateFromProps() {
    console.log('Child shouldComponentUpdate');
  }

  shouldComponentUpdate() {
    console.log('Child shouldComponentUpdate');
  }

  componentDidMount() {
    console.log('Child componentDidMount');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('Child componentDidUpdate(prevProps, prevState)');
  }

  componentWillUnmount() {
    console.log('Child componentWillUnmount');
  }

  render() {
    console.log('Child render');
    return (
      <div className="child">
        <h4>I am a Child</h4>
      </div>
    );
  }
}

运行后结果如下:

Parent constructor

Parent getDerivedStateFromProps

Parent shouldComponentUpdate

Parent render

Child constructor

Child getDerivedStateFromProps

Child shouldComponentUpdate

Child render

Child componentDidMount

Parent componentDidMount

此时可以分析出,当父组建 render 时遇到子组件,然后进入子组件的生命周期,当执行完子组件生命周期中的componentDidMount 时会回到父组建继续执行父组建未完成的生命周期。

由上面父子嵌套组件的生命周期流程,可以推断继续验证多级组件嵌套的流程。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant