浅谈前端状态管理(上)

浅谈前端状态管理(上)

吴双 Clarence吴双 Clarence

近两年前端技术的发展如火如荼,大量的前端项目都在使用或转向 Vue 和 React 的阵营,由前端渲染页面的单页应用占比也越来越高,这就代表前端工作的复杂度也在直线上升,前端页面上展示的信息越来越多也越来越复杂。我们知道,任何状态都需要进行管理,那么今天我们来聊聊前端状态管理。

Virtual DOM 及 React 诞生

在 Web 应用开发中,AngularJS 扮演了重要角色。然而 AngularJS 数据和视图的双向绑定基于脏检测的机制,在性能上存在短板,任何数据的变更都会重绘整个视图。但是,由状态反应视图、自动更新页面的思想是先进的,为了解决性能上的问题,Facebook 的工程师们提出了 Virtual DOM 的思想。将 DOM 放到内存中,state 发生变化的时候,根据 state 生成新的 Virtual DOM,再将它和之前的 Virtual DOM 通过一个 diff 算法进行对比,将被改变的内容在浏览器中渲染,避免了 JS 引擎频繁调用渲染引擎的 DOM 操作接口,充分利用了 JS 引擎的性能。有了 Virtual DOM 的支持,React 也诞生了。

有了 React,「state => view」的思想也就有了很好的实践,但反过来呢,怎么在 view 中合理地修改 state 成为了一个新的问题,为此,Facebook 提出了 Flux 思想。

Flux 思想

是的,Flux 不是某一个 JS 库的名称,而是一种架构思想,很多 JS 库则是这种思想的实现,例如 AltFluxible 等,它用于构建客户端 Web 应用,规范数据在 Web 应用中的流动方式。

那么这个和状态管理有什么关系呢?我们知道,React 只是一个视图层的库,并没有对数据层有任何的限制,换言之任何视图组件中都可能存在改变数据层的代码,而过度放权对于数据层的管理是不利的,另外一旦数据层出现问题将会很难追溯,因为不知道变更是从哪些组件发起的。另外,如果数据是由父组件通过 props 的方式传给子组件的话,组件之间会产生耦合,违背了模块化的原则。

我们以 AngularJS 应用为例,在 AngularJS 中,controller 是一个包含于作用域 $scope 的闭包,而这个闭包对应了一个视图模板,$scope 中的数据将会被渲染到模板中。但是一个模板可能会对应到多个 model(当前 controller 的 $scope,父级 $scope,指令的 isolated scope 等),同样,一个 model 也可能影响到多个模板的渲染。应用规模一旦变大,数据和视图的关系很容易混乱,由于这个过程中数据和视图会互相影响,思维的负担也会增加。

而 Flux 的思维方式是单向的,将之前放权到各个组件的修改数据层的 controller 代码收归一处,统一管理,组件需要修改数据层的话需要去触发特定的预先定义好的 dispatcher,然后 dispatcher 将 action 应用到 model 上,实现数据层的修改。然后数据层的修改会应用到视图上,形成一个单向的数据流。打个比方,这就像是图书馆的管理,原来是开放式的,所有人可以随意进出书库借书还书,如果人数不多,这种方式可以减少流程,增加效率,一旦人数变多就势必造成混乱。Flux 就像是给这个图书馆加上了一个管理员,所有借书还书的行为都需要委托管理员去做,管理员会规范对书库的操作行为,也会记录每个人的操作,减少混乱的现象。

主要 Flux 实现

Flux 的实现有很多,不同的实现也各有亮点,下面介绍一些比较流行的 Flux 的实现。

Flux

这应该是 Flux 的一个比较官方”的实现,显得中规中矩,实现了 Flux 架构文档里的基本概念。它的核心是 Dispatcher,通过 Dispatcher,用户可以注册需要相应的 action 类型,对不同的 action 注册对应的回调,以及触发 action 并传递 payload 数据。

下面是一个简单示例:

const dispatcher = new Dispatcher()
const store = {books: []}

dispatcher.register((payload) => {
  if (payload.actionType === 'add-book') {
    store.books.push(payload.newBook)
  }
})
dispatcher.dispatch({
  actionType: 'add-book',
  newBook: {
    name: 'cookbook'
  }
})

可以看到,只使用 Flux 提供的 Dispatcher 也是可以的,不过推荐使用 Flux 提供的一些基础类来构建 store,这些基础类提供了一些方法可供调用,能更好的扩展数据层的功能,具体使用方法可以参考 Flux 文档

Reflux

Reflux 是在 Flux 的基础上编写的一个 Flux 实现,从形式上看,去掉了显式的 Dispatcher,将 action 表现为函数的形式,构建一个 action 的方式为:

const addBook = Reflux.createAction({
  actionName: 'add-book',
  sync: false,
  preEmit: function() {/*...*/},
  // ...
})
addBook({/*...*/})

另外,Reflux 相比 Flux 有一些区别,例如:

依赖

首先 Flux 不是一个库,而是一种架构思想,不过要使用 Flux 还是要引入一个 Dispatcher,而 Reflux 则提供了一整套库供你使用,可以方便地通过 npm 来安装。

组件监听事件

在组件内监听事件的写法上,Flux 和 Reflux 也有一些区别,在 Flux 中:

const _books = {}
const BookStore = assign({}, EventEmitter.prototype, {
  emitChange () {
    this.emit(CHANGE_EVENT)
  },
  addChangeListener (callback) {
    this.on(CHANGE_EVENT, callback)
  },
  removeChangeListener (callback) {
    this.removeListener(CHANGE_EVENT, callback)
  }
})
const Book = React.createClass({
  componentDidMount:function(){
    bookStore.addChangeListener(this.onAddBook)
  }
})

而在 Reflux 中,写法有些不同,它通过在组件中引入 Mixin 的方式使得在组件中可调用 listenTo 这个方法:

var BookStore = React.createClass({
  mixins: [Reflux.ListenerMixin],
  componentDidMount: function() {
    this.listenTo(bookStore, this.onAddBook)
  }
})

Store 和 Action 的写法

在 Flux 中,初始化一个 Store 以及编写 Action 都是比较麻烦的,这导致了代码量的增加,可维护性也会降低,例如我们仍然要写一个 Store 和对应的 Action,创建 Store 的写法在上面的示例中已经有了,而创建 Action 在两者之间区别也很大,首先是 Flux:

const fluxActions = {
  addBook: function(book) {
    Dispatcher.handleViewAction({
      actionType: 'ADD_BOOK',
      book
    })
  },
  // more actions
}

Reflux 和 Flux 相比就简单很多:

const refluxActions = Reflux.createActions([
  'addBook',
  // more actions
])

之所以 Reflux 会简单这么多,是因为它可以在 Store 中直接注册事件的回调函数,而去掉了 Dispatcher 这一中间层,或者说将 Dispatcher 的功能整合进了 Store 中。

总的来看,Reflux 相当于是 Flux 的改进版,补全了 Flux 在 Store 上缺少的功能,并去掉了 Dispatcher(实际上并不是去掉,而是和 Store 合并),减少了冗余的代码。

Redux

Redux 实际上相当于 Reduce + Flux,和 Flux 相同,Redux 也需要你维护一个数据层来表现应用的状态,而不同点在于 Redux 不允许对数据层进行修改,只允许你通过一个 Action 对象来描述需要做的变更。在 Redux 中,去掉了 Dispatcher,转而使用一个纯函数来代替,这个纯函数接收原 state tree 和 action 作为参数,并生成一个新的 state tree 代替原来的。而这个所谓的纯函数,就是 Redux 中的重要概念 —— Reducer。

在函数式编程中,Reduce 操作的意思是通过遍历一个集合中的元素并依次将前一次的运算结果代入下一次运算,并得到最终的产物,在 Redux 中,reducer 通过合并计算旧 state 和 action 并得到一个新 state 则反映了这样的过程。

因此,Redux 和 Flux 的第二个区别则是 Redux 不会修改任何一个 state,而是用新生成的 state 去代替旧的。这实际上是应用了不可变数据(Immutable Data),在 reducer 中直接修改原 state 是被禁止的,Facebook 的 Immutable 库可以帮助你使用不可变数据,例如构建一个可以在 Redux 中使用的 Store。

下面是一个用 Redux 构建应用的状态管理的示例:

const { List } = require('immutable')
const initialState = {
  books: List([])
}
import { createStore } from 'redux'

// action
const addBook = (book) => {
  return {
    type: ADD_BOOK,
    book
  }
}

// reducer
const books = (state = initialState, action) => {
  switch (action.type) {
    case ADD_BOOK:
      return Object.assign({}, state, {
        books: state.books.push(action.book)
      })
  }
  return state
}

// store
const bookStore = createStore(books, initialState)

// dispatching action
store.dispatch(addBook({/* new book */}))

Redux 的工作方式遵循了严格的单向数据流原则,从上面的代码示例中可以看出,整个生命周期分为:

  1. 在 store 中调用 dispatch,并传入 action 对象。action 对象是一个描述变化的普通对象,在示例中,它由一个 creator 函数生成。

  2. 接下来,store 会调用注册 store 时传入的 reducer 函数,并将当前的 state 和 action 作为参数传入,在 reducer 中,通过计算得到新的 state 并返回。

  3. store 将 reducer 生成的新 state 树保存下来,然后就可以用新的 state 去生成新的视图,这一步可以借助一些库的帮助,例如官方推荐的 React Redux

如果一个应用规模比较大的话,可能会面临 reducer 过大的问题。这时候我们可以对 reducer 进行拆分,例如使用 combineReducers,将多个 reducer 作为参数传入,生成新的 reducer。当触发一个 action 的时候,新 reducer 会触发原有的多个 reducer:

const book(state = [], action) => {
  // ...
  return newState
}
const author(state = {}, action) => {
  // ...
  return newState
}
const reducer = combineReducers({ book, author })

关于 Redux 的更多用法,可以仔细阅读文档,这里就不多介绍了。

React 技术栈中可用的状态管理库还有更多,例如 Relay,不过它需要配合 GraphQL,在没有 GraphQL 的支持下不好引入,这里就不多赘述了(其实是我没有研究过😝)。

次回予告

关于 React 中类 Flux 架构的状态管理工具我们就先聊到这里,在下篇中我们会聊到其他框架技术栈中的状态管理工具,看看它们会有什么特点,敬请期待~

「真诚赞赏,手留余香」
5 人赞赏
微信用户
袁斌
微信用户
塞翁
微信用户
文章被以下专栏收录
25 条评论
推荐阅读