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

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

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


在文章开始之前,我想先向大家介绍一本书。

从去年起,我和知名技术大佬颜海镜开始了合著之旅,今年我们共同打磨的书籍《React 状态管理与同构实战》终于正式出版了!这本书以 React 技术栈为核心,在介绍 React 用法的基础上,从源码层面分析了 Redux 思想,同时着重介绍了服务端渲染和同构应用的架构模式。书中包含许多项目实例,不仅为用户打开了 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 => dispatch({ ...action, payload: result }))
.catch(error => {
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 抽象和状态抽象中分别例举分析;控制权与码农息息相关,它直接决定了我们的编程体验和开发效率。

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

最后,前端学习永无止境,希望和每一位技术爱好者共同进步,大家可以在知乎找到我!

Happy coding!

Happy coding!


《React 状态管理与同构实战》这本书由我和前端知名技术大佬颜海镜合力打磨,凝结了我们在学习、实践 React 框架过程中的积累和心得。除了 React 框架使用介绍以外,着重剖析了状态管理以及服务端渲染同构应用方面的内容。同时吸取了社区大量优秀思想,进行归纳比对。

本书受到百度公司副总裁沈抖、百度资深前端工程师董睿,以及知名 JavaScript 语言专家阮一峰、Node.js 布道者狼叔、Flarum 中文社区创始人 justjavac、新浪移动前端技术专家小爝、百度资深前端工程师顾轶灵等前端圈众多专家大咖的联合力荐。

有兴趣的读者可以点击这里,了解详情。也可以扫描下面的二维码购买。再次感谢各位的支持与鼓励!恳请各位批评指正!

React 进阶设计与控制权问题的更多相关文章

  1. React组件设计

    React组件设计 组件分类 展示组件和容器组件 展示组件 容器组件 关注事物的展示 关注事物如何工作 可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有D ...

  2. React进阶之高阶组件

    前言 本文代码浅显易懂,思想深入实用.此属于react进阶用法,如果你还不了解react,建议从文档开始看起. 我们都知道高阶函数是什么, 高阶组件其实是差不多的用法,只不过传入的参数变成了react ...

  3. React组件设计(转)

    React组件设计 组件分类 展示组件和容器组件 展示组件 容器组件 关注事物的展示 关注事物如何工作 可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有D ...

  4. React的设计哲学 - 简单之美

    React最初来自Facebook内部的广告系统项目,项目实施过程中前端开发遇到了巨大挑战,代码变得越来越臃肿且混乱不堪,难以维护.于是痛定思痛,他们决定抛开很多所谓的“最佳实践”,重新思考前端界面的 ...

  5. React组件设计技巧

    React组件设计 组件分类 展示组件和容器组件 展示组件 容器组件 关注事物的展示 关注事物如何工作 可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有D ...

  6. React进阶之路书籍笔记

    React进阶之路: "于复合类型的变量,变量名不指向数据,而是指向数据所在的地址.const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心 ...

  7. React进阶篇学习

    继续上一次基础篇, 分享一些关于React的进阶技术 React 进阶部分 ** context ** ** setState vs forceUpdate ** ** Mixins ** ** HO ...

  8. React 应用设计之道 - curry 化妙用

    使用 React 开发应用,给予了前端工程师无限"组合拼装"快感.但在此基础上,组件如何划分,数据如何流转等应用设计都决定了代码层面的美感和强健性. 同时,在 React 世界里提 ...

  9. 蒲公英 &#183; JELLY技术周刊 Vol.18 关于 React 那些设计

    蒲公英 · JELLY技术周刊 Vol.18 自 2011 年,Facebook 第一次在 News Feed 上采用了 React 框架,十年来 React 生态中很多好用的功能和工具在诸多设计思想 ...

随机推荐

  1. [19/05/27-星期一] JavaScript_ 条件语句(if语句)和循环语句(while 、for、do-while)

    一.条件语句 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <ti ...

  2. 在git bash 中配置git用户名和邮箱及查看配置信息

    Administrator@LuoTong- MINGW32 ~ $ git config --global user.name "mrluotong" Administrator ...

  3. java8----Predicate接口的使用

    //5.lambda表达式中加入Predicate // 甚至可以用and().or()和xor()逻辑函数来合并Predicate, // 例如要找到所有以J开始,长度为四个字母的名字,你可以合并两 ...

  4. Cyclic Nacklace HDU 3746 KMP 循环节

    Cyclic Nacklace HDU 3746 KMP 循环节 题意 给你一个字符串,然后在字符串的末尾添加最少的字符,使这个字符串经过首尾链接后是一个由循环节构成的环. 解题思路 next[len ...

  5. django项目学习之异步框架celery

    最近用django一个网上商城项目的时候用两个扩展,感觉还不错,所以在此记录一下. 首先来说下celery,celery是一个处理异步任务的框架,需要下载celery包,一般在项目需要进行耗时操作的时 ...

  6. 工作笔记20170315-------关于FAQ(Frequently Asked Questions)列表的代码

    源自于:http://www.17sucai.com/pins/3288.html (1)FAQ问答列表点击展开收缩文字列表 <ul>   <li class="clear ...

  7. 使用 ELK 来分析你的支付宝账单

    ELK 即 elasticsearch, logstash 以及 kibana.Elasticsearch 是一个基于 lucene 的分布式搜索引擎,logstash 是一种日志传输工具,也可以对日 ...

  8. vue(js)点击目标div以外区域将目标div隐藏

    今天开发新项目的时候,有个需求,点击一个div 展示出个弹出窗 弹出窗标题的颜色要跟点击的div图标颜色一致, 所以这就需要我遍历一下多个数据, 然后需要点击弹出div的外部其他区域,隐藏这个div. ...

  9. CollectionView刷新问题,以及定时器与控制器的销毁问题

    1.CollectionView的刷新必须首先保证CollectionView有高度 注意事项:在cell中嵌套CollectionView,如果使用的是AutoLayout的话,一定要注意保证Col ...

  10. 免费申请 QQ 免费靓号

    打开网址:https://ssl.zc.qq.com/v3/index-chs.html?type=3 说明:靓号一般分为 9位靓号 or 带有寓意的号码 激活规则:获取的"靓号" ...