1

概念引入

对于React来说, 没有State就没有页面的渲染, 我们也将什么都看不到

咋一听怎么那么唬人?不过的确是这样,正如标题所言State是UI的灵魂。我们都知道React的核心思想之一是组件化,将页面所展示的东西按一定的规则分割成很多份并进行一一封装最后抽象成我们现在所称为的"组件", 就好像我们搭积木一样,一个城堡就是通过一个个小方块堆叠在一起的。但是FaceBook觉得如果仅仅是简单的封装那么和普通的moudle有什么区别?和咸鱼又有什么区别?于是FaceBook给这些"组件"赋予了灵魂(之一) -- State
什么叫State?顾名思义就是状态的意思。每个组件都有自己的状态,比如开关的闭合、颜色的切换和显示与隐藏等等都会涉及到状态的切换。首先卖个关子,下面我们一起来复习下小学(还是初中?)的一枚数学知识。

y=f(x) -->(假如这是一个一元一次函数)

Are you kidding me?这是要侮辱在座的智商?不不不,请放下手里40米的大刀,笔者想抛个砖引个玉。
这是再简单不过的了,它表示y是关于x的函数。函数在数学中是十分的严谨,x与y是一一对应关系换句话说就是同一个x代入运算得到的永远是同一个y;同一个y代入运算得到的永远是同一个x,这个特性很像Redux中的Reducer,本质上是一个纯函数

初识State

那么如果我们把这个公式带到React中会有什么样的化学反应呢?

UI=f(State)

有木有感觉眼前一亮,Excuse me?竟然把State和UI通过一个公式关联起来?其实本质上就是这么简单。
同时,我们还可以继续用函数的思想去思考它:

  1. 输入特定的State只能输出特定的UI
  2. 根据特定的UI就能反推出相应的State

当然实际结果也是如此,React组件所渲染出来的东西与State有直接而且唯一的关系,换句话说就是State决定组件显示什么而且只有State才能决定组件显示什么

比如上面提到的一个组件可能有很多切换的动作,开关、颜色、显示与消失等等...本来这种切换的动作需要我们自己通过操作DOM来实现,但是FaceBook在设计React之初就把 直接操作DOM 这条路给堵死了(但仍给我们提供的必要的接口已备不时之需,后续文章会有相应内容),究竟为什么要这么做?因为我们都知道前端开发中特别消耗性能的是DOM操作,一旦处理不当就会影响整个页面的展示效果,因此FaceBook一不做二不休直接搞出了一个State出来,让开发者去输入State随后React自己去操作相应的DOM。

这么做有两个好处

  1. 使得State成为页面的唯一数据来源和页面元素变换的唯一依据。
  2. 提高页面的渲染性能(当然这不是React高效渲染的决定性因素)。

State大大提高了开发者对React组件的开发效率而不用担心页面性能问题,可谓是一举多得。

实例展示:静态State

下面我们来开发一个简单的文字展示组件:

import React, {Component} from 'react'       ---line 1

class Show extends Component {               ---line 2

    constructor(props) {                     ---line 3
        super(props);
        this.state = {                       ---line 4
            content: 'Hello World'
        }
        //this.propName = propValue;         ---line 5
    }

    render() {                               ---line 6
        return (
            <p>{this.state.content}</p>      ---line 7
        )
    }
}

export default Show;
首先一起来分析下这段代码:
line 1: 日常导包
line 2: ES6创建对象的方法。强烈推荐这么写
line 3: 该组件的构造方法,如果组件有属性默认值那么就需要写构造函数
line 4: 这里表示该组件有自己的State属性而且它还是一个字面量对象,所以与该组件有关的所有State都应该写在这个字面量对象中。从代码中看出该组件有一个State对象content,它包含着这个组件需要展示的一段文字。
line 5: 如果我们想给这个组件定义State以外的属性,那么就可以项这行所写一样,不过需要放在this对象中,这样才能在组件中通过this对象读取到。
line 6: render方法是最终构建组件结构的地方,因为组件究竟长什么样子,需要在这里写。
line 7: 正如这个组件需要做的事情,我们在render方法中返回这个p标签用来显示文字信息。因为所需要的文字信息保存在State对象中,State又保存在this对象中,所以如何去获取文字信息在这里不需要过多赘述。另外需要注意的是,render方法return的节点只能是一个,不能是多个。如果你的组件结构复杂,请在最外层用div这样的标签包一下然后再返回

下面看具体效果:

clipboard.png
是不是很简单?

实例展示:动态State

接下来再说一下状态变化,因为状态就是用来更改的,如果不更改那和咸鱼有什么区别?

先思考下:React状态应该如何更改?

这个问题笔者第一次遇到的时候第一反应就是:直接改啊!!!(然后被piapiapia打脸),先试下吧:

import React, {Component} from 'react'

class Show extends Component {

    constructor(props) {
        super(props);
        this.state = {
            content: 'Hello World'
        }
    }

    changeState = () => {
        this.state.content = 'I\'m React State';
    };

    render() {
        return (
            <div>
                <p>{this.state.content}</p>
                <button onClick={this.changeState}>变变变</button>
            </div>
        )
    }
}

export default Show;

我们创建一个函数用来更改响应的State,然后实际运行的时候发现不管怎么点按钮都毫无作用?Why?

因为React给我们提供了专门用于更改状态的方法, this.setState()

我们来重新试一下:

import React, {Component} from 'react'

class Show extends Component {

    constructor(props) {
        super(props);
        this.state = {
            content: 'Hello World'
        }
    }

    changeState = () => {
        this.setState({
            content: 'I\'m React State'
        })
    };

    render() {
        return (
            <div>
                <p>{this.state.content}</p>
                <button onClick={this.changeState}>变变变</button>
            </div>
        )
    }
}

export default Show;
这个方法需要我们传入一个字符量对象,key是我们需要更改的那个State,这里是content; value是我们所期望要更改的值。( [\] 是转移符)。

clipboard.png

可以看出页面中那行字变成了我们所期望的文字。所以正如我们所说:

  1. 更改State要使用this.setState()方法。
  2. 一旦更改了State,会触发组件的重新渲染。实际上是运行一次组件中的render方法

异步的setState

这个点笔者想不出合适的引入点,所以就直接抛出来了。这个问题很有趣,因为巧合的是笔者的一个萌妹子同事在学习React时候恰好遇到这个问题,代码大概是这样:

onchange = () => {
    this.setState({
        name: 'Demo'
    });

    this.props.change(this.state.name)//调用父组件通过props传过来的方法
};

她的本意是在本组件更改了name这个State后再通过调用父组件的方法实现父组件name的重新渲染。我们看出this.props.change(this.state.name)传入的参数是直接从State中取的,但是实际运行的时候却不是如想象中那样同时更改,现象是第一次点击时候本组件成功渲染,但是父组件并没有同时渲染;第二次点击时候父组件才渲染成对应的名字。
为什么呢?
因为this.setState这个方法不是同步的而是异步的,了解JavaScript中Event Loop机制的朋友都知道,如果一段js代码中有异步的代码那么会将其放在一个队列中,等待这段代码其余代码运行完后再从那个队列中取出异步代码运行。this.setState机制也和它差不多,当我们set一个State后React并不会立即去更改对应的State,而是在合适的时机下进行更改甚至为了提高性能会将多个setState过程合并成一个。为了证明这个异步机制,我们通过打印的方式做个试验:

import React, {Component} from 'react'

class Show extends Component {

    constructor(props) {
        super(props);
        this.state = {
            content: 'Hello World'
        }
    }

    changeState = () => {
        console.log(`1 -- ${this.state.content}`);
        this.setState({
            content: 'I\'m React State'
        });
        console.log(`2 -- ${this.state.content}`);
        console.log('end');
    };

    render() {
        return (
            <div>
                <p>{this.state.content}</p>
                <button onClick={this.changeState}>变变变</button>
            </div>
        )
    }
}

export default Show;

控制台打印结果如下:

clipboard.png

可以看出,页面正常渲染了说明对应的State已经更改了,但是控制台显示的信息却没有更改后的现象,所以可以确定真正的setState并不是调用this.setState()方法的瞬间,而是在之后的某个时间。所以有个问题需要注意:不要用当前的State去计算下一个State,因为你不能保证当前的State是最新的

但如果有个需求,需要在更改State后立即执行某个动作怎么办?

正常来说我们无法预知真正的setState是在何时,所以React理所当然得给我们提供了办法,那么就是this.setState的第二个参数,第二个参数是一个方法,当对应的State修改成功后会立即执行,我们修改下代码:

...
changeState = () => {
    console.log(`1 -- ${this.state.content}`);
    this.setState({
        content: 'I\'m React State'
    }, () => {
        console.log(`3 -- ${this.state.content}`);
    });
    console.log(`2 -- ${this.state.content}`);
    console.log('end');
};
...

看结果咯:

clipboard.png

与预期一致,没毛病!!!

State的不可变

看到这个小标题,估计很多人会很懵逼,前面还说不更改的State和咸鱼有什么区别怎么到这里就要不可变了?其实是混淆了。
官方的建议是将State的所有对象当做是不可变对象,一旦每个对象更改了那么需要重新创建这个对象。举例子说,前面的代码中有:

this.state = {
    content: 'Hello World'
}

当我们更改了content的值,用"I'm React State"替换了原有的"Hello World"。其实在这里,content对用的value不仅仅是内容上的变化也是地址上的变化,这种在基本变量上体现不出来,比如我们有个State要保存一个列表内容那么就得是个数组(字面量对象亦如此):

this.state = {
    navis: ['React','Vue','Angular']
}

这个时候如果我们直接将navis的值拿出来push一个元素进去然后setState:

addNavi = () => {
    this.setState({
        navis: this.state.navis.push('React-Native')
    })
};

结果是页面并没有重新渲染,Why? 因为React在对比navis新的和老的两个值时候发现它们的地址都没变化就认为它们内容也没变化就不会重新渲染。这是个坑!!!。所以此时State对象的不可变原则就有作用了,解决方案有两个:

1、 复制原来的值,push完后进行setState。
addNavi = () => {
    let navisCopy = this.state.navis.slice();
    this.setState({
        navis: navisCopy.push('React-Native')
    })
};

这样就能正常运行了,因为navis对应的值不仅仅在内容上变了,地址也变化了,React检测到变化后就进行了重新渲染。

2、第三方插件

  1. Immutable.js
  2. immutability-helper
  3. immutability-helper-x

至于为什么需要这么做?

  1. State数据更明确,方便管理和开发调试。
  2. 为了页面渲染性能的考虑,有助于在shouldComponentUpdate中进行比较并确定是否重新渲染。

Bingo...本期的博文就结束了,这期笔者也精心准备了很久,希望大家都能喜欢!!


风吹过的夏夜
295 声望31 粉丝

前端工程师