React 进阶设计与控制权问题

发布于 2022-11-30 12:51:09 字数 9161 浏览 95 评论 0

控制权——这个概念在编程中至关重要。比如 轮子 封装层与业务消费层对于控制权的 争夺,就是一个很有意思的话题。这在 React 世界里也不例外。表面上看,我们当然希望 轮子 掌控的事情越多越好:因为抽象层处理的逻辑越多,业务调用时关心的事情就越少,使用就越方便。可是有些设计却 不敢越雷池一步。轮子 与业务在控制权上的拉锯,就非常有意思了。

同时,控制能力与组件设计也息息相关:Atomic components 这样的原子组件设计被受推崇;在原子组件这个概念之上,还有分子组件:Molecules components。不管是分子还是原子,在解决业务问题上都有存在的理由。

这篇文章将以 React 框架为背景,谈谈我在开发当中对于控制权的一些想法和总结。如果你并不使用 React,原则上仍不妨碍阅读。

从受控与非受控组件说起

初入 React 大门,关于控制权概念,我们最先接触到的就是受控组件与非受控组件。这两个概念往往与表单关联在一起。在大部分情况下,推荐使用受控组件来实现表单、输入框等状态控制。在受控组件中,表单等数据都由 React 组件自己处理。而非受控组件,是指表单的数据由 Dom 自己控制。下面就是一个典型的非受控组件:

<form>
  <label>
  Name:
  <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>
复制代码

对于 React 来说,非受控组件的状态和用户输入都无法直接掌控,只能依赖 form 标签的原生能力进行交互。如果使上例非受控组件变为一个受控组件,代码也很简单:

class NameForm extends React.Component {
state= {value: ''}
handleChange = event => {
this.setState({value: event.target.value});
}
handleSubmit = event => {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
)
}
}

这时候表单值和行为都由 React 组件控制,使得开发更加便利。这当然是很基础的概念,借此抛出控制权的话题,请读者继续阅读。

UI 轮子 与 Control Props 模式

前文介绍的样例,我称之为 狭义受控和非受控 组件。广义来说,我认为完全的非受控组件是指:不含有内部 states,只接受 props 的函数式组件或无状态组件。它的渲染行为完全由外部传入的 props 控制,没有自身的“自治权”。这样的组件在很好地实现了复用性,且具有良好的测试性。

但在 UI 轮子 设计当中,半自治 或者 不完全受控 组件,有时也会是一个更好的选择。我们将此称之为 control props 模式。简单来说就是:组件具有自身 state,当没有相关 porps 传入时,使用自身状态 statea 完成渲染和交互逻辑;当该组件被调用时,如果有相关 props 传入,那么将会交出控制权,由业务消费层面控制其行为。

在研究大量社区 UI 轮子 之后,我发现由 Kent C. Dodds 编写的,在 paypal 使用的组件库 downshift 便广泛采用了这样的模式。

简单用一个 Toogle 组件举例,这个组件由业务方调用时:

class Example extends React.Component {
  state = {on: false, inputValue: 'off'}
  handleToggle = on => {
  this.setState({on, inputValue: on ? 'on' : 'off'})
  }
  handleChange = ({target: {value}}) => {
  if (value === 'on') {
    this.setState({on: true})
  } else if (value === 'off') {
    this.setState({on: false})
  }
  this.setState({inputValue: value})
  }
  render() {
  const {on} = this.state
  return (
    <div>
    <input
      value={this.state.inputValue}
      onChange={this.handleChange}
    />
    <Toggle on={on} onToggle={this.handleToggle} />
    </div>
  )
  }
}

我们可以通过输入框来控制 Toggle 组件状态切换(输入 on 激活状态,输入 off 状态置灰),同时也可以通过鼠标来点击切换,此时输入框内容也会相应变化。

请思考:对于 UI 组件 Toggle 来说,它的状态可以由业务调用方来控制其状态,这就赋予了使用层面上的消费便利。在业务代码中,不管是 Input 还是其他任何组件都可以控制其状态,调用时我们具有完全的控制权掌控能力。

同时,如果在调用 Toggle 组件时,不去传 props 值,该组件仍然可以正常发挥。如下:

  <Toggle>
  {({on, getTogglerProps}) => (
    <div>
    <button {...getTogglerProps()}>Toggle me</button>
    <div>{on ? 'Toggled On' : 'Toggled Off'}</div>
    </div>
  )}
  </Toggle>

Toggle 组件在状态切换时,自己维护内部状态,实现切换效果,同时通过 render prop 模式,对外输出本组件的状态信息。

我们看 Toggle 源码(部分环节已删减):

const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))
class Toggle extends Component {
static defaultProps = {
defaultOn: false,
onToggle: () => {},
}
state = {
on: this.getOn({on: this.props.defaultOn}),
}
getOn(state = this.state) {
return this.isOnControlled() ? this.props.on : state.on
}
isOnControlled() {
return this.props.on !== undefined
}
getTogglerStateAndHelpers() {
return {
on: this.getOn(),
setOn: this.setOn,
setOff: this.setOff,
toggle: this.toggle,
}
}
setOnState = (state = !this.getOn()) => {
if (this.isOnControlled()) {
this.props.onToggle(state, this.getTogglerStateAndHelpers())
} else {
this.setState({on: state}, () => {
this.props.onToggle(
this.getOn(),
this.getTogglerStateAndHelpers()
)
})
}
}
setOn = this.setOnState.bind(this, true)
setOff = this.setOnState.bind(this, false)
toggle = this.setOnState.bind(this, undefined)
render() {
const renderProp = unwrapArray(this.props.children)
return renderProp(this.getTogglerStateAndHelpers())
}
}
function unwrapArray(arg) {
return Array.isArray(arg) ? arg[0] : arg
}
export default Toggle

关键的地方在于组件内 isOnControlled 方法判断是否有命名为 on 的属性传入:如果有,则使用 this.props.on 作为本组件状态,反之用自身 this.state.on 来管理状态。同时在 render 方法中,使用了 render prop 模式,关于这个模式本文不再探讨,感兴趣的读者可以在社区中找到很多资料,同时也可以在我新书中找到相关内容。

盘点一下,control props 模式反应了典型的控制权问题。这样的**“半自治”**能够完美适应业务需求,在组件设计上也更加灵活有效。

Redux 异步状态管理与控制权

提到控制权话题,怎能少得了 Redux 这样的状态管理工具。Redux 的设计在方方面面都体现出来良好的控制权处理,这里我们把注意力集中在异步状态上,更多的内容还请读者关注我的新书。

Redux 处理异步,最为人熟知的就是 Redux-thunk 这样的中间件,它由 Dan 亲自编写,并在 Redux 官方文档上被安利。它与其他所有中间件一样,将 action 到 reducer 中间的过程进行掌控,使得业务使用时可以直接 dispatch 一个函数类型的 action,实现代码也很简单:

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
export default thunk;

但是很快就有人认为,这样的方案因为在中间件实现中的控制不足,导致了业务代码不够精简。我们还是需要遵循传统的 Redux 步骤:八股文似的编写 action,action creactor,reducer......于是,控制粒度更大的中间件方案应运而生

Redux-promise 中间件控制了 action type,它限制业务方在 dispatch 异步 action 时,action的 payload 属性需要是一个 Promise 对象时,执行 resolve,该中间件触发一个类型相同的 action,并将 payload 设置为 promise 的 value,并设 action.status 属性为 "success"。

export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action);
}
return isPromise(action.payload)
? action.payload
.then(result =&gt; dispatch({ ...action, payload: result }))
.catch(error =&gt; {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
})
: next(action);
};
}

这样的设计与 Redux-thunk 完全不同,它将 thunk 过程控制在中间件自身中,这样一来,第三方轮子做的事情更多,因此在业务调用时更加简练方便。我们只需要正常编写 action 即可:

dispatch({
  type: GET_USER,
  payload: http.getUser(userId) // payload 为 promise 对象
})

我们对比一下 Redux-thunk,相对于“轮子”控制权较弱,业务方控制权更多的 Redux-thunk,实现上述三行代码,就得不得不需要:

dispatch(
	function(dispatch, getState) {
    dispatch({
      type: GET_USERE, 
      payload: userId
    })
    http.getUser(id)
      .then(response => {
        dispatch({
          type: GET_USER_SUCCESS,
          payload: response
        })
      })
      .catch(error => {
        dispatch({
          type: GET_DATA_FAILED,
          payload: error
        })
      }) 
  }
)

当然,Redux-promise 控制权越多,一方面带来了简练,但是另一方面,业务控制权越弱,也丧失了一定的自主性。比如如果想实现乐观更新(Optimistic updates),那就很难做了。具体详见 Issue #7

为了平衡这个矛盾,在 Redux-thunk 和 Redux-promise 这两个极端控制权理念的中间件之间,于是便存在了中间状态的中间件:Redux-promise-middleware,它与 Redux-thunk 类似,掌控粒度也类似,但是在 action 处理上更加温和和渐进,它会在适当的时机 dispatch XXX_PENDING、XXX_FULFILLED 、XXX_REJECTED 三种类型的 action,也就是说这个中间件在掌控更多逻辑的基础上,增加了和外界第三方的通信程度,不再是直接高冷地触发 XXX_FULFILLED 、XXX_REJECTED,请读者仔细体会其中不同

状态管理中的控制主义和极简主义

了解了异步状态中的控制权问题,我们再从 Redux 全局角度进行分析。在内部分享时,我将基于 Redux 封装的状态管理类库共同特性总结为这一页 slide。

以上四点都是相关类库基于 Redux 所进行的简化,其中非常有意思的就是后面三点,它们无一例外地与控制权相关。以 Rematch 为代表,它不再是处理 action 到 reducer 的中间件,而是完全控制了 action creator,reducer 以及联通过程。

具体来看

  • 业务方不再需要显示申明 action type,它由类库直接函数名直接生成,如果 reducer 命名为 increment,那么 action.type 就是 increment;
  • 同时控制 reducer 和 action creator 合二为一,态管理从未变得如此简单、高效。

我把这样的实践称为控制主义或者极简主义,相比 Redux-actions 这样的状态管理类库,这样的做法更加彻底、完善。具体思想可参考 Shawn McKay 的文章,介绍的比较充分,这里我不再赘述。

总结:码农和控制权

控制权说到底是一种设计思想,是第三方类库和业务消费的交锋和碰撞。它与语言和框架无关,本文只是以 React 举例,实际上在编程领域控制权的争夺随处可见;他与抽象类别无关,本文已经在 UI 抽象和状态抽象中分别例举分析;控制权与码农息息相关,它直接决定了我们的编程体验和开发效率。

可是在编程的初期阶段,优秀的控制权设计难以一蹴而就。只有投身到一线开发当中,真正了解自身业务需求,进而总结大量最佳实践,同时参考社区精华,分析优秀开源作品,相信我们都会得到成长。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

风渺

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

eins

文章 0 评论 0

世界等同你

文章 0 评论 0

毒初莱肆砂笔

文章 0 评论 0

初雪

文章 0 评论 0

miao

文章 0 评论 0

qq_zQQHIW

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文