浅谈前端响应式设计(一)
现实世界有很多是以响应式的方式运作的,例如我们会在收到他人的提问,然后做出响应,给出相应的回答。在开发过程中我也应用了大量的响应式设计,积累了一些经验,希望能抛砖引玉。
响应式编程(Reactive Programming)和普通的编程思路的主要区别在于,响应式以推( push
)的方式运作,而非响应式的编程思路以拉( pull
)的方式运作。例如,事件就是一个很常见的响应式编程,我们通常会这么做:
button.on('click', () => {
// ...
})
而非响应式方式下,就会变成这样:
while (true) {
if (button.clicked) {
// ...
}
}
显然,无论在是代码的优雅度还是执行效率上,非响应式的方式都不如响应式的设计。
Event Emitter
EventEmitter
是大多数人都很熟悉的事件实现,它很简单也很实用,我们可以利用 EventEmitter
实现简单的响应式设计,例如下面这个异步搜索:
class Input extends Component {
state = {
value: ''
}
onChange = e => {
this.props.events.emit('onChange', e.target.value)
}
afterChange = value => {
this.setState({
value
})
}
componentDidMount() {
this.props.events.on('onChange', this.afterChange)
}
componentWillUnmount() {
this.props.events.off('onChange', this.afterChange)
}
render() {
const { value } = this.state
return (
<input value={value} onChange={this.onChange} />
)
}
}
class Search extends Component {
doSearch = (value) => {
ajax(/* ... */).then(list => this.setState({
list
}))
}
componentDidMount() {
this.props.events.on('onChange', this.doSearch)
}
componentWillUnmount() {
this.props.events.off('onChange', this.doSearch)
}
render() {
const { list } = this.state
return (
<ul>
{list.map(item => <li key={item.id}>{item.value}</li>)}
</ul>
)
}
}
这里我们会发现用 EventEmitter
的实现有很多缺点,需要我们手动在 componentWillUnmount
里进行资源的释放。它的表达能力不足,例如我们在搜索的时候需要聚合多个数据源的时候:
class Search extends Component {
foo = ''
bar = ''
doSearch = () => {
ajax({
foo,
bar
}).then(list => this.setState({
list
}))
}
fooChange = value => {
this.foo = value
this.doSearch()
}
barChange = value => {
this.bar = value
this.doSearch()
}
componentDidMount() {
this.props.events.on('fooChange', this.fooChange)
this.props.events.on('barChange', this.barChange)
}
componentWillUnmount() {
this.props.events.off('fooChange', this.fooChange)
this.props.events.off('barChange', this.barChange)
}
render() {
// ...
}
}
显然开发效率很低。
Redux
Redux
采用了一个事件流的方式实现响应式,在 Redux
中由于 reducer
必须是纯函数,因此要实现响应式的方式只有订阅中或者是在中间件中。
如果通过订阅 store
的方式,由于 Redux
不能准确拿到哪一个数据放生了变化,因此只能通过脏检查的方式。例如:
function createWatcher(mapState, callback) {
let previousValue = null
return (store) => {
store.subscribe(() => {
const value = mapState(store.getState())
if (value !== previousValue) {
callback(value)
}
previousValue = value
})
}
}
const watcher = createWatcher(state => {
// ...
}, () => {
// ...
})
watcher(store)
这个方法有两个缺点,一是在数据很复杂且数据量比较大的时候会有效率上的问题;二是,如果 mapState
函数依赖上下文的话,就很难办了。在 react-redux
中, connect
函数中 mapStateToProps
的第二个参数是 props
,可以通过上层组件传入 props
来获得需要的上下文,但是这样监听者就变成了 React
的组件,会随着组件的挂载和卸载被创建和销毁,如果我们希望这个响应式和组件无关的话就有问题了。
另一种方式就是在中间件中监听数据变化。得益于 Redux
的设计,我们通过监听特定的事件(Action)就可以得到对应的数据变化。
const search = () => (dispatch, getState) => {
// ...
}
const middleware = ({ dispatch }) => next => action => {
switch action.type {
case 'FOO_CHANGE':
case 'BAR_CHANGE': {
const nextState = next(action)
// 在本次dispatch完成以后再去进行新的dispatch
setTimeout(() => dispatch(search()), 0)
return nextState
}
default:
return next(action)
}
}
这个方法能解决大多数的问题,但是在 Redux
中,中间件和 reducer
实际上隐式订阅了所有的事件(Action),这显然是有些不合理的,虽然在没有性能问题的前提下是完全可以接受的。
面向对象的响应式
ECMASCRIPT5.1
引入了 getter
和 setter
,我们可以通过 getter
和 setter
实现一种响应式。
class Model {
_foo = ''
get foo() {
return this._foo
}
set foo(value) {
this._foo = value
this.search()
}
search() {
// ...
}
}
// 当然如果没有getter和setter的话也可以通过这种方式实现
class Model {
foo = ''
getFoo() {
return this.foo
}
setFoo(value) {
this.foo = value
this.search()
}
search() {
// ...
}
}
Mobx
和 Vue
就使用了这样的方式实现响应式。当然,如果不考虑兼容性的话我们还可以使用 Proxy
。
当我们需要响应若干个值然后得到一个新值的话,在 Mobx
中我们可以这么做:
class Model {
@observable hour = '00'
@observable minute = '00'
@computed get time() {
return `${this.hour}:${this.minute}`
}
}
Mobx
会在运行时收集 time
依赖了哪些值,并在这些值发生改变(触发 setter
)的时候重新计算 time
的值,显然要比 EventEmitter
的做法方便高效得多,相对 Redux
的 middleware
更直观。
但是这里也有一个缺点,基于 getter
的 computed
属性只能描述 y=f(x)
的情形,但是现实中很多情况 f
是一个异步函数,那么就会变成 y=awaitf(x)
,对于这种情形 getter
就无法描述了。
对于这种情形,我们可以通过 Mobx
提供的 autorun
来实现:
class Model {
@observable keyword = ''
@observable searchResult = []
constructor() {
autorun(() => {
// ajax ...
})
}
}
由于运行时的依赖收集过程完全是隐式的,这里经常会遇到一个问题就是收集到意外的依赖:
class Model {
@observable loading = false
@observable keyword = ''
@observable searchResult = []
constructor() {
autorun(() => {
if (this.loading) {
return
}
// ajax ...
})
}
}
显然这里 loading
不应该被搜索的 autorun
收集到,为了处理这个问题就会多出一些额外的代码,而多余的代码容易带来犯错的机会。
或者,我们也可以手动指定需要的字段,但是这种方式就不得不多出一些额外的操作:
class Model {
@observable loading = false
@observable keyword = ''
@observable searchResult = []
disposers = []
fetch = () => {
// ...
}
dispose() {
this.disposers.forEach(disposer => disposer())
}
constructor() {
this.disposers.push(
observe(this, 'loading', this.fetch),
observe(this, 'keyword', this.fetch)
)
}
}
class FooComponent extends Component {
this.mode = new Model()
componentWillUnmount() {
this.state.model.dispose()
}
// ...
}
而当我们需要对时间轴做一些描述时, Mobx
就有些力不从心了,例如需要延迟5秒再进行搜索。
在下一篇博客中,将介绍 Observable
处理异步事件的实践。
- HTML 教程
- HTML 简介
- html div 标签介绍
- html span 标签介绍
- html a 超链接标签
- HTML Br换行标签介绍
- HTML P段落标签介绍
- HTML br与p标签区别
- Html H 标题标签
- html px em pt长度单位
- HTML form 标签
- HTML radio 单选框
- HTML B 加粗标签
- HTML strong加粗粗体标签
- HTML em 强调标签
- HTML i 斜体标签
- HTML u下划线标签
- HTML s 删除线标签
- Html img 图片标签
- Html上标注sup与下标注sub标签
- HTML nobr 禁止换行标签
- HTML hr 水平线标签
- HTML label 标签
- HTML input 标签
- HTML textarea 标签
- HTML select下拉列表标签
- HTML checkbox 多选框
- HTML font color 标签
- HTML iframe 框架标签
- HTML Table 表格
- HTML dl dt dd 标签
- HTML ol li有序列表标签
- HTML ul li 无序列表标签
- HTML 注释
- CSS 教程
- CSS 简介
- CSS 语法
- CSS Id 和 Class选择器
- CSS 样式的创建
- CSS background 背景介绍
- CSS 文本样式
- CSS font 字体
- CSS A 链接
- CSS ul ol列表样式
- CSS TABLE 样式
- CSS 框模型
- CSS border 边框
- CSS Outlines 轮廓
- CSS 外边距 Margin
- CSS Padding 内边距
- CSS 分组和嵌套选择器
- CSS 尺寸 (Dimension)
- CSS Display 属性
- CSS Position 定位
- CSS Float 浮动
- CSS 水平对齐(Horizontal Align)
- CSS 组合选择符
- CSS 伪类
- CSS 伪元素
- CSS 导航栏
- CSS 下拉菜单
- CSS 图片廊
- CSS 图像透明/不透明
- CSS sprite 图像拼合技术
- CSS 媒体类型
- CSS 属性选择器
- CSS 实例