前端数据流的设计

2022-2-21 14:35:35

#前端

21

前言

最近由于工作的需要,在看《React设计模式和最佳实践》这一本书,看到 第5章:恰当地获取数据 这一章节,觉得很有意思,特地在此作一摘记和总结。

正文

  • 目前很多前端项目都采取了组件式编程,其结构都可以理解为一棵组件树,每个页面有一个父节点的组件,并且在其之下会衍生出很多的子组件,由这些子组件共同构成了页面。
  • 而涉及到这些组件时,自然而然的会扯出一个问题,如何组织协调这些组件之间的通信,尽可能的降低整个系统的复杂度,从而使得代码更好维护与升级。

单向数据流

  • 像Vue和React这些前端框架都不约而同的采取了单向数据流这一种设计模式,顾名思义,该数据模式的数据流是单向的,即从组件树的顶部流向底部。
  • 组织形式
    • 每个组件都以prop的形式从父组件接受数据,并且prop无法修改。
    • 组件获取数据后,可以将其转化为新的形式或者继续往下传给其它子组件。
    • 组件可以保存内部状态,并且将其传入下一个子组件的prop中。
  • 在这种模式下,大大简化了组件行为以及组件间的关系,增强的代码的可预测性和可维护性。
  • 上面讲的都是父组件传值给子组件,但是当我们需要子组件将数据上传给父组件。该怎么办?当子组件的状态改变时,如何更新父组件?兄弟组件需要共享数据时又该怎么办?
  • 首先我们创建一个简单的Counter父组件,该组件是一个拥有加减两个按钮的计数器。
class Counter extends React.Component{
  constructor(props) {
    super(props) 

    this.state = {
      counter: 0, 
    }

    this.handleDecrement = this.handleDecrement.bind(this) 
    this.handleIncrement = this.handleIncrement.bind(this) 
  }

  handleDecrement() { 
    this.setState({ 
    counter: this.state.counter - 1, 
    }) 
  }

  handleIncrement() { 
    this.setState({ 
    counter: this.state.counter + 1, 
    }) 
  }

  render() { 
      return ( 
      <div> 
      <h1>{this.state.counter}</h1> 
      <button onClick={this.handleDecrement}>-</button> 
      <button onClick={this.handleIncrement}>+</button> 
      </div> 
      ) 
    }

}

子组件与父组件的通信

  • 首先简单讲一下Vue框架是如何实现子组件改变父组件状态,Vue使用$emit来使父组件传递一个事件给子组件,子组件只要触发这个事件便可以将参数传递给父组件的自定义事件来改变父组件的状态。
  • 而React采用回调函数的形式来实现。
  • 我们这次创建一个简单的无状态函数式Buttons组件,来替换上面Counter组件的button
const Buttons = ({ onDecrement, onIncrement }) => ( 
  <div> 
    <button onClick={onDecrement}>-</button> 
    <button onClick={onIncrement}>+</button> 
  </div> 
) 

Buttons.propTypes = { 
  onDecrement: React.PropTypes.func, 
  onIncrement: React.PropTypes.func, 
}
  • 可以看到该组件内部的onClick事件处理器会触发props上的函数,逻辑仍然只存在于父组件,使得按钮组件变得纯粹,被点击时只会通知自身的拥有者。
  • 结论
    • 每当子组件需要向父组件推送数据或者通知父组件发生了某个事件时,可以传递回调函数,同时将其余逻辑放在父组件中

公有父组件

  • 在写代码的时候,我们应该尽可能让组件与数据源无关,这样就能在应用各部分的不同数据源下复用组件。
  • 因此对于上面的计数器,我们可以再抽离出显示逻辑,创建一份单纯显示的组件
const Display = ({ counter }) => <h1>{counter}</h1> 
Display.propTypes = { 
  counter: React.PropTypes.number, 
}
  • 该组件依旧是一个无状态函数式组件,目前这是一个非常简单的组件,看起来没什么拆分的必要,但是能提高你之后拓展该组件的便捷性,比如说之后在应用内添加CSS类,显示逻辑根据值改变计数器的颜色等等内容与功能。
  • 该显示(Display)组件与上文的按钮(Button)组件成为了兄弟组件,这两个兄弟组件通过公有的父组件(Counter)进行了通信
  • 通信过程
    • 当被点击时,Button组件会通知父组件,然后父组件会将更新后的值发送给Display组件
    • 该过程仍旧依照单向数据流的原则,数据始终从父组件流向子组件,但是子组件也可以发送通知给父组件,以便组件数按照新的数据重新渲染。
    • 因此当我们碰到需要两个没有关联的组件互相通信的情况时,我们都可以找到它们的公有父组件来保存状态。这样一来,当状态更新时,两个子组件都能从props接收新数据。

数据的获取

  • 用于获取的代码可以放在componentWillMount(组件首次渲染前)和componentDidMount(组件挂载完毕后)
  • 而由于在服务端渲染组件时会触发componentWillMount,服务端渲染时触发异步API会带来意料之外的结果,所以一般都使用componentDidMount生命函数钩子来确保只在浏览器端调用API请求。

对API数据获取的数据逻辑抽离

  • 很多时候,我们前端都要调用各种API来获取数据,这种时候,我们就有可能遇到这样的一个需求:多处代码都要从API获取数据。
  • 为了简化代码,减少重复,我们可以试着从组件中移除数据逻辑并在整个应用中复用。
  • 这时候我们可以使用高阶函数来试着抽离这一数据逻辑
  • 高阶组件
    • 本质上其实就是函数,它接收组件和一些其它参数,然后返回添加了某些特殊行为的新组件
    • 其命名通常会带有with前缀
  • 采用偏函数写法接收其它参数,实际组件为第二个参数
const withData = url => Component => (withDataClass)

class withDataClass extends React.Component{
    constructor(props){
        super(props)
        this.state={data:[]}
    }
    componentDidMount(){
        fetch(url)
            .then(Response=>Response.json())
            .then(data=>this.setState({data}))
    }
    render(){
        return <Component {...this.props}{...this.state}></Component>
    }
}
  • 使用示例
// 示例展示组件
const List = ({ data: gists }) => (
    <ul>
        {gists.map(gist => (
            <li key={gist.id}>{gist.description}</li>
        ))}
    </ul>
)
List.propTypes = {
    data: React.PropTypes.array,
}
// 定制高阶组件
// 使用新的withGists函数封装组件,可以不用重复制定URL了
const withGists = withData(
    'https://api.github.com/users/gaearon/gists'
)
// 使用高阶组件增强原组件获得新组件ListWithGists
const ListWithGists = withGists(List)