当面试官直接问:你是如何理解单向数据流的?

5,747 阅读10分钟

组件状态

状态可以简单的理解为数据,与 props 类似,但是 state 是私有的,并且完全受控于当前组件,因此组件状态指的就是一个组件自己维护的数据

刚刚我们也提到了一个非常重要的点:数据驱动UI 。意思也很简单,就是页面所展示的内容,完全是受 状态 控制的,这也就是所谓 MVVM 的理念。UI 的改变,全部交给框架本身来做,我们只需要管理好 “数据(状态)”  就可以了。

那么在 React 中,如何对状态进行管理呢?这就是本文的重点,也是整个 React 学习的重点:组件的状态管理

基本使用

state 的使用是非常简单的,我们在 中声明一个名为 state 的对象,对象中的元素就是当前组件所维护的状态数据,获取展示数据时,只需要在 jsx 中,使用 this.state.xx 的方式获取就可以了。

import React, { Component } from 'react' 

export class States extends Component {
  // 声明 state 对象 
  state = {
    name: 'xiling',
    age: 18
  }

  render() {
    return (
      <>
        <h2>state 状态</h2>
        {/* 使用 this.state.xx 获取数据 */}
        <p>{this.state.name}</p>
        <p>{this.state.age}</p>
      </>
    )
  }
}

export default States

前面我们说,state 数据是可以控制界面的,那么我们如何修改 state 从而让界面发生改变呢?

修改状态

想要修改 state 的值,最直观的方式就是直接使用 this.state={} 的方式直接修改。我们设置一个按钮,当点击按钮时,通过 this.state={} 发现是不起作用的,那应该怎么做呢?

React 给我们提供了专门的 this.setState({}) 方法,我们需要调用 this.setState({}) 方法将需要修改的数据传入才能正确的修改 state 的值。

至于为什么,需要我们理解 React 数据流才能搞懂,这里就不再详细介绍,只需要记住这个规则就可以了。

import React, { Component } from 'react'

export class States extends Component {
  // 声明 state 对象 
  state = {
    name: 'xiling',
    age: 18
  }

  // 箭头函数
  changes = ()=>{
    // console.log(22)
    // this.state.name = 'xiling' // 错误的使用方式
    this.setState({name:'西岭'}) 
  }

  render() {
    return (
      <>
        <h2>state 状态</h2>
        {/* 使用 this.state.xx 获取数据 */}
        <p>{this.state.name}</p>
        <p>{this.state.age}</p>
        <button onClick={this.changes}>改变state</button>
      </>
    )
  }
}

export default States

一旦 state 的值发生了改变,那么 JSX 中使用 state 的地方就会自动发生改变。这里也需要注意一点,因为 setState 方法是类中的属性(方法),我们需要使用 this 进行获取,因此,事件绑定的处理函数就需要使用箭头函数来固定 this 的指向,一定不要使用普通的函数 (类方法) 声明,否则会因为找不到方法而直接报错。

自顶向下的单向数据流

关于数据流的问题,是面试中高频次出现的典型题目,一般情况下面试官会直接问: “你是如何理解单向数据流的 ? ”

注意,这不是一个单独的个体问题,而是数据流问题的综合体,解答这个问题,你需要解释:

  • 什么是数据流?
  • 为什么是自顶向下的?
  • 单向数据流是什么意思?
  • 为什么是单向的?不能是双向的数据流嘛?
  • 单向数据流有什么作用呢?

面试题一旦拆开,你会发现面试官问出来的几乎每一个词都需要解释。宝儿,这个问题,真不简单啊!

那么,我应该怎么解答呢?

说实话,并没有标准答案,因为数据流这个问题,涉及到了框架本身的设计理念,需要你对框架的设计有深入理解,你要站在框架作者的角度看待问题。但是,对于初学者来说,这个问题显然超纲了。

完犊子,那么重要,我又学不了是嘛?不是,你需要学很多遍,这只是第一遍。

开始之前,我们先来看一段普通的 JS 代码:

var datas = {
  name:'lisi',
  age:18
}

var l1 = datas
var l2 = l1
var l3 = l2

l1.age = 20
console.log(l1.age,l2.age,l3.age) // 20 20 20

l3.age = 26
console.log(l1.age,l2.age,l3.age) // 26 26 26

你会发现,无论我们是修改那个变量的 age 属性,其他数据都会跟着改变。

原因也很简单,大家都是共享一个内存数据的。但是,赋值的前后逻辑上,我们可以将 L3 节点看作孙子,L2 节点看做父亲,L1 节点看做爷爷。

任意一个节点的数据改变之后,所有节点的数据都会跟着改变,我们就可以把这种现象看做是数据在“变量节点”上的流动。但是,这样的数据流动,是双向的,拿 L2 这个节点来说,只要数据改变,上层的 L1 节点和下层的 L3 节点都会跟着改变。

虽然这个例子并不恰当,但是回到 React 组件中,道理是一样的,所谓数据的流动就是数据在组件间的传递,前面我们用了很大的篇幅讲解的 组件间的值传递 ,其实就是在讲 数据流 这个概念的具体用法。

那么,我们在数据流前面加上一个“单向”的定语,叫 “单向数据流” 是什么意思呢?其实现在你理解起来很简单,就是数据在某个节点被改变后,只会影响一个方向上的其他节点。

那所谓的自顶向下又怎么解释呢?更简单了,就是数据只会影响到下一个层级的节点,不会影响上一个层级的节点;用上面的例子解释,就是如果 L2 数据改变,只会影响到 L3,不会影响到 L1 或者其他节点。

这就是 “自顶向下的单向数据流” 。那么我们在 React 框架中,就可以明确定义 单向数据流:规范数据的流向,数据由外层组件向内层组件进行传递和更新。

那么,在具体的代码实现中,是怎么体现出来的呢?翠花,上代码:

图有点看不清,接下来,我们看具体代码的演示:

// ========== App ============
import React, { Component } from 'react'
import C1 from './C1'
export class App extends Component {
  state = {
    name: "xiling"
  }
  render() {
    return (
      <div>
        <h1>App</h1>
        <p> APP 中的值:
          <b style={{ color: "red" }}>
            {this.state.name}
          </b>
        </p>
        <C1 toC1={this.state.name}></C1>
      </div>
    )
  }
}

export default App


// ========== C1 ============
import React, { Component } from 'react'
import C2 from './C2'
export class C1 extends Component {


  render() {
    return (
      <div>
        <h2>C1</h2>
        <p> 传入C1 的值(App传入):
          <b style={{ color: "red" }}>
            {this.props.toC1}
          </b>
        </p>
        <C2 toC2={this.props.toC1}></C2>
      </div>
    )
  }
}

export default C1


// ========== C2 ============
import React, { Component } from 'react'
import C3 from './C3'
export class C2 extends Component {

  state = {
    name: this.props.toC2
  }

  changes = () => {
    this.setState({
      name: Math.random()
    })
  }

  render() {
    return (
      <div>
        <h2>C2</h2>
        <button onClick={() => { this.changes() }}>
          修改
        </button>
        <p> 传入C2 的值(C1传入):
          <b style={{ color: "red" }}>
            {this.state.name}
          </b>
        </p>
        <C3 toC3={this.state.name}></C3>
      </div>
    )

  }
}

export default C2


// ========== C3 ============
import React, { Component } from 'react'

export class C3 extends Component {
  render() {
    return (
      <div>
        <h2>C3</h2>
        传入C3 的值(C2传入):
        <b style={{ color: "red" }}>
          {this.props.toC3}
        </b>
      </div>
    )
  }
}

export default C3

最后,我们再来解释,为什么?有什么用?

其实这才是这个问题的核心,不同的技术理解,就会有不同的角度解释,我这里仅一家之言,你且听听罢。

我们设想这样的情景:

父组件的数据通过props传递给子组件,而子组件里更新了props,导致父组件和其他关联组件的数据更新,UI 渲染也会随数据而更新,毫无疑问,这是会导致严重的数据紊乱和不可控。

因此绝大多数框架在这方面做了处理。而 React 在这方面的处理,就是直接规定了 Props 为只读的,而不是可更改的。这也就是我们前面看到的数据更新不能直接通过 this.state 操作,想要更新,就需要通过 React 提供的专门的 this.setState() 方法来做。

单向数据流 其实就是一种框架本身对数据流向的限制。

暂时先说这些吧,等我们学的越多,经验越丰富,对它的理解也就会越深刻,看待它的角度也就越全面。

异步的 setState

setState 是异步还是同步的?

这是一个面试中经常会被问到的经典面试题,在开始讲解异步特性之前,我们需要先明确,从 API 层面上说,它就是普通的调用执行的函数 ,自然是同步 API 。因此,这里所说的同步和异步,指的是 API 调用后,state 的改变或者更新 DOM 的时机,是同步还是异步的。

我们先来看一段代码:

import React, { Component } from 'react'

export class States extends Component {
  state = {
    name:"lisi",
    age:18
  }

  fun= ()=>{
    this.setState({
      name:'xiling'
    })
    console.log(this.state.name)
  }

  render() {
    return (
      <div>
        <h2>State 组件</h2>
        <p>{this.state.name}</p>
        <button onClick={()=>{this.fun()}}>点我</button>
      </div>
    )
  }
}

export default States

代码实现的的功能非常简单,就是在点击按钮后,修改 state 中的 name 属性值,事件处理函数中,是在调用 setState 方法之后,又将 state 的值打印到了控制台,运行代码你会发现, Dom 中的值发生了改变,但是控制台却是之前的结果值,如果你对异步的运行规则比较熟悉,你一定不会感到奇怪,很显然,this.setState() 是异步执行的,调用之后,后面的代码就紧跟着执行了,因此,控制台打印的结果肯定是修改之前。

因此,我们可以确定,this.setState() 确实是异步调用执行的代码

注意,此时,你可以将 this.setState() 当作普通的异步执行代码 (再次强调 JSX 就是 JS)。

那么,如果我想调整代码获取异步执行结果,应该怎么做呢?既然可以看作为普通的异步代码,想想也知道,其实 this.setState() 方法还提供了第二个参数,我们可以传入一个回调函数,这个函数就是异步结束后执行的回调函数,具体代码修改如下:

  fun= async ()=>{
    await this.setState({
      name:'xiling'
    },()=>{
      console.log(this.state.name)
    })
  }

回调函数处理异步结果本来就是传统的异步编码方式,但是,我们也知道,异步中的回调函数有一个最大的问题就是 “回调地狱”,那么既然是异步,我们是不是就可以封装一个 Promise 了呢?

当然可以,但是,封装一个 Promise 就显得有些复杂,最简单的方式就是借助 ES 2017 中异步调用函数 async / await 来实现,我们直接修改事件处理函数即可,具体语法规则如下:

 fun= async ()=>{
    await this.setState({
      name:'xiling'
    })
    console.log(this.state.name)
  }

setState 的执行逻辑

在使用 this.setState() 进行状态更改时,需要进行逻辑处理应该怎么做呢?其实 this.setState() 的第一个参数是可以接收一个函数处理的,需要注意的是,函数的运行必须返回一个 state 对象,具体代码如下:

  fun = ()=>{
    this.setState((state)=>{
      // 函数逻辑代码 ……
      let returnData
      returnData = state.age+1
      // 最终返回一个 state 对象
      return {
        age:returnData
      }
    })
  }

也就是说 this.setState() 既可以接收对象参数,也可以接收一个处理函数,那么,这两者又有什么区别呢?

在时间处理函数中,我们分别使用两次 this.setState() 对 state 进行修改操作。

两次函数执行操作:

  fun = ()=>{
    this.setState((state)=>{
      return {
        age:state.age+2
      }
    })

    this.setState((state)=>{
      return {
        age:state.age+3
      }
    })
  }

运行代码可以发现,两次 this.setState() 中的函数都得到了执行,修改了 state 的值,我们在将同样的代码逻辑,使用对象数据的方式进行修改操作:

  fun = ()=>{
    this.setState({
      age:this.state.age +2
    })
    this.setState({
      age:this.state.age +3
    })
  }

结果显示,只有最后一次 this.setState() 得到了执行。

没错,this.setState() 如果是函数,那么函数会依次从上往下执行,而如果是一个对象, React 会将多次 this.setState() 的调用合并为一次执行,如果修改的了相同的值,则会将前面的修改替换成最后一次的修改数据。

这一点在项目编码中一定要牢记,避免出现意外的逻辑 Bug 。

当然,你可能也发现了一个问题,我们说了那么多关于组件状态的问题,各种实验编码都是在 class 类组件中完成的,那函数组件中是不是和类组件一样呢?

嗯~ o( ̄▽ ̄)o……,因为 state 是类组件中的特性,而函数组件中的 state,需要具备 Hook 特性的知识作为铺垫,所以,暂时先留个坑,等着后面来填就行了。