深入浅出 React -- 组件数据流

890 阅读4分钟

React 核心:数据驱动视图

UI = f(data)

UI = render(props + state)

React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。

在 React 组件中,props(入参)或者 state (状态)发生改变,UI 也会相应的更新。

组件更新不止来自自身状态的改变,而两个组件之间建立数据上的连接,实现组件间的通信,它的背后是 React 数据流解决方案。下面将会从各方面说明当前实践中 React 数据通信的方式。

基于 props 的单向数据流

组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

React 非常灵活,但它也有一个严格的规则:

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

而通过 props 通信的的组件也需遵循单向数据流(单向绑定)的原则

所谓单向数据流,是指 props 只能流向组件树中比自己层级更低的组件。React 单向数据流的思想使得组件模块化,易于快速开发。基于 props 这种传参方式,可以轻松实现“父子组件通信”、“子父组件通信”、“兄弟组件通信”。

父子组件通信

父组件直接将 props 传入子组件

🌰:

function Child(props) {
  return <div>{`接收来自父组件的内容:${props.text}`}</div>
}

class Father extends React.Component {
  state = {
    text: '初始化父组件文本'
  }

  changeText = () => {
    this.setState({
      text: '改变后的父组件文本'
    })
  }

  render() {
    return (
      <div>
        <button onClick={this.changeText}>
          修改文本
        </button>
        <Child text={this.state.text} />
      </div>
    )
  }
}

子组件读取了来自父组件 props 的内容,并且在父组件 state.text 更改后保持一致

子父组件通信

由于单向数据流,子组件不能直接将自身数据塞给父组件;但是父组件可以通过 props 传递给子组件一个绑定自身上下文的函数,那么子组件就可以将想要传递给父组件的数据以函数参数的形式交予父组件,从而间接实现子组件向父组件的数据通信

🌰:

class Child extends React.Component {
  state = {
    text: '子组件'
  }

  changeText = () => {
    this.props.changeText(this.state.text)
  }

  render() {
    return (
      <div>
        <button onClick={this.changeText}>点击更新父组件文本</button>
      </div>
    )
  }
}

class Father extends React.Component {
  state = {
    text: '初始化父组件文本'
  }

  changeText = (newText) => {
    this.setState({
      text: newText
    })
  }

  render() {
    return (
      <div>
        <p>{`父组件文本:${this.state.text}`}</p>
        <Child changeText={this.changeText} />
      </div>
    )
  }
}

子组件通过 props.changeText 传入自身的 state.text 调用,修改了父组件的 state.text ,从而实现了子父组件通信

兄弟组件通信

兄弟组件共有同一个父组件,可以通过父子组件通信和子父组件通信结合实现兄弟组件通信

🌰:

function Child1(props) {
  return <div>{`接收来自父组件的内容:${props.text}`}</div>
}

class Child2 extends React.Component {
  state = {
    text: '来自 Child2 的文本'
  }

  changeText = () => {
    this.props.changeText(this.state.text)
  }

  render() {
    return (
      <div>
        <button onClick={this.changeText}>点击更新 Child1 组件文本</button>
      </div>
    )
  }
}

class Father extends React.Component {
  state = {
    text: '初始化父组件文本'
  }

  changeText = (newText) => {
    this.setState({
      text: newText
    })
  }

  render() {
    return (
      <div>
        { /** 父子组件通信 */}
        <Child1 text={this.state.text} />
        {/** 子父组件通信 */}
        <Child2 changeText={this.changeText} />
      </div>
    )
  }
}

子组件 Child2 通过 props.changeText 调用更新了父组件的 state.text (子父通信),子组件 Child1 通过 props.text 获取了父组件的 state.text (父子通信),从而实现了兄弟组件之间的通信。

不推荐其他场景使用 props

比如多层组件通信,通过 props 层层传递非常简单,但是会编写非常繁琐冗余的代码,并且中间层的组件会引入很多不必要的属性。编写代码的程序员会非常痛苦,而且会是整个项目的维护成本变高。

针对其他场景的组件通信可以通过其他方式实现。

使用 Context API

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

在 React 16.3 之前,Context API 存在种种局限性,不被 React 官方推荐使用,只作为一个概念探讨。从 React 16.3 之后,对 Context API 进行了改进,使其具备更高的可用性。

Context API 工作流

Context API 有 3 个关键 API:React.createContextContext.ProviderContext.Consumer

React.createContext

const MyContext = React.createContext(defaultValue)

当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。

Context.Provider

<MyContext.Provider value={}></MyContext.Provider>

每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

Context.Consumer

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

一个 React 组件可以订阅 context 的变更,这让你在函数式组件中可以订阅 context。

这种方法需要一个函数作为子元素(function as a child)。这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext()defaultValue

新的 Context API 解决了什么问题

如果组件提供的一个Context发生了变化,而中间父组件的 shouldComponentUpdate 返回 false,那么使用到该值的后代组件不会进行更新。使用了 Context 的组件则完全失控,所以基本上没有办法能够可靠的更新 Context。这篇博客文章很好地解释了为何会出现此类问题,以及你该如何规避它。

新的 Context API 改进了这一点:即便组件的 shouldComponentUpdate 返回 false,它仍然可以“穿透”组件继续向后代组件进行传播,进而确保了数据生产者和数据消费者之间数据的一致性。

"发布-订阅"模式

"发布-订阅"模式是解决通信类问题的"万能钥匙",有着广泛的应用,比如:

  • ajax 的 successerror 等事件

  • Node.js 中的 EventEmitter

  • Vue.js 的事件总线(EventBus

理解"发布-订阅"

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

发布-订阅模式的优点是监听事件和触发事件的地方是不受限制的,只要它们在同一个上下文,就可以触发监听。这非常适合任意组件的通信

设计发布-订阅模式 API

针对事件的监听(订阅)事件的触发(发布)

  • on :注册事件的监听,指定事件触发时的回调
  • emit :触发事件,可以通过参数在触发时携带数据
  • off :删除监听
  • once :和 on 一样,不过在触发事件后删除监听

实现发布-订阅

class EventEmitter {
  constructor() {
    this.eventMap = Object.create(null)
  }

  on(type, handler) {
    if(!handler instanceof Function) {
      throw new Error('handler 必须是一个函数')
    }

    this.eventMap[type] ? this.eventMap[type].push(handler) : this.eventMap[type] = [handler]

    return this
  }

  emit(type, params) {
    this.eventMap[type] && this.eventMap[type].forEach((handler) => handler(params))

    return this
  }

  off(type, handler) {
    if(this.eventMap[type]) {
      if(!handler) {
        delete this.eventMap[type]
      } else {
        this.eventMap[type] = this.eventMap[type].filter(c => c !== handler)
      }
    }

    return this
  }

  once(type, handler) {
    this.on(type, (...args) => {
      handler(...args)
      this.off(type, handler)
    })

    return this
  }
}
const myEvent = new EventEmitter()

const testHandler = (params) => {
  console.log(`test 事件被触发了,testHandler 接收的参数:${params}`)
}

myEvent.on('test', testHandler)
myEvent.emit('test', 'emit') // test 事件被触发了,testHandler 接收的参数:emit

myEvent.off('test', testHandler)
myEvent.emit('test', 'emit')

组件通信应用

对于任意两个组件 A 和 B,可以通过 EventEmitter 将数据从 A 传入 B,从而实现组件之间通信

window.myEvent = new EventEmitter()

class A extends React.Component {
  state = {
    info: '来自A'
  }

  toB = () => {
    window.myEvent.emit('eventKey', this.state.info)
  }

  render() {
    return <button onClick={this.toB}>点击传递 info 到B</button>
  }
}

class B extends React.Component {
  state = {
    params: ''
  }

  handler = (params) => {
    this.setState({
      params
    })
  }

  bindHandler = () => {
    window.myEvent.on('eventKey', this.handler)
  }

  render() {
    return (
      <div>
        <button onClick={this.bindHandler}>点我监听 A 的动作</button>
        <div>A 传入的内容是:{this.state.params}</div>
      </div>
    )
  }
}

function App() {
  return (
    <div>
      <B />
      <A />
    </div>
  )
}

上面代码的关系图:

第三方数据流框架 -- Redux

对于简单的跨层级组件通信,可以通过 Context API 或者发布-订阅模式解决。但是随着应用复杂度提升,需要维护的状态增多,我们就需要 Redux 来处理。

什么是 Redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers

Redux 是如何帮助 React 管理数据的

Redux 主要由三个部分组成:storereduceraction

  • action :把数据从应用传到 store 的有效载荷(对变化的描述)

  • reducer :一个纯函数,负责对变化的分发和处理,并将新的数据返回给 store

  • store :唯一数据源,而且只读

Redux 工作流:

在 Redux 的整个工作流程中,数据流是严格单向的

如何理解上图工作流:

  • 视图(View)的所有数据 state 都来自 store

  • 对数据修改的唯一途径:派发 action

  • action 会通过 reducer,根据 action 的内容对数据进行处理,生产新的 state ,最后更新到 store

对于组件来说,可以通过约定的方式获取 store 的状态,也可以通过派发 action 修改 store 的状态。

工作流编码

1. 创建 store 对象

import { createStore } from 'redux'

const store = createStore(
	reducer,
  initialState,
  applyMiddleware(middleware1, middleware2, ...)
)

参数:

  • reducer
  • 初始状态
  • 中间件

2. 通过 reducer 将新的 state 更新到 store

const reducer = (state, action) => {
  // 逻辑处理
  return newState
}

3. action 对象通知 reducer 做什么更新

const action = {
  type: 'ADD',
  payload: 3
}

action 对象允许多个属性,只有 type 是必填属性

typeaction 的唯一标识,reducer 通过不同的 type 来识别更新不同的 state,从而能够实现精准的“定向更新”

4. 通过 dispatch 派发 action

import { createStore } from 'redux'

const reducer = (state, action) => {
  // 逻辑处理
  return newState
}

const store = createStore(reducer)

const action = {
  type: 'ADD',
  payload: 3
}

store.dispatch(action)

总结

Redux & React 工作流:

现在我们大致了解 Redux 是如何帮助 React 做状态管理,实现灵活的组件间通信