常见数据流对比

目前主流方案

V = f(M)

f : Vue\React\Angular\...

数据管理

前端越来越大

数据越来越多

对数据的修改越来越多

数据存储结构

数据规范修改

Redux

核心思想:

1. web应用本质是一个状态机,视图和状态一一对应

2. 所有状态存在一个对象中

Redux

store:

      存储全局数据

      整个应用只能有一个store

      通过redux提供的createStore方法生成

State:

      store在某个时间点的快照

      store.getState()可以获取

      state与view必须一一对应

Action:

      state无法直接被改变,action表示state会发生的变化

      改变state的方法就是使用action

Redux

Action Creator:

      每一种变化都得有一个对应的action,因此可以通过creator生成action

store dispatch:

      action规定了会发生的变化或者动作,dispatch是触发action

reducer:

      store收到action时,需产生一个对应且不同的state对象,view会随之变化。

      reducer接收当前state和action,返回一个全新的state

      reducer就是createStore的参数,可以生成初始store

Redux

纯函数:

      reducer是纯函数,无法修改传入的state,每次会生成全新的state

subscribe:

      监听函数。当state变化时会自动触发subscribe。对于很多MVVM框架,可以将render或者react的setState放在subscribe函数中,实现自动刷新。

Demo

// index.js
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
import todoApp from './reducers'

let store = createStore(todoApp)

let rootElement = document.getElementById('root')
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)
// components
onClick: () => {
  dispatch(setVisibilityFilter(ownProps.filter))
 }
// actions.js
export const ADD_TODO = 'ADD_TODO';
export const COMPLETE_TODO = 'COMPLETE_TODO';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
};

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function completeTodo(index) {
  return { type: COMPLETE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

Demo

// reducer.js
import { combineReducers } from 'redux'
import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return ***
    case COMPLETE_TODO:
      return ***
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

Demo

// containers/App.js  容器js
import ****

class App extends Component {
  render() {
    const { dispatch, visibleTodos, visibilityFilter } = this.props
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            dispatch(addTodo(text))
          } />
        <TodoList
          todos={visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          } />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          } />
      </div>
    )
  }
}

App.propTypes = {}

function selectTodos(todos, filter) {
  ****
}

// Which props do we want to inject, given the global state?
// Note: use https://github.com/faassen/reselect for better performance.
function select(state) {
  return {
    visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    visibilityFilter: state.visibilityFilter
  }
}

// 注入 dispatch 和 state
export default connect(select)(App)

基本原理

用户通过dispatch发出action

dispatch(action)

store自动调用reducer,传入action和当前state,生成全新state

let nextState = reducer(state, action)

state一旦变化,调用监听函数

store.subscribe(fn)

redux-thunk

加入了async、await

onSubmit = username => {
  this.props.dispatch(searchUsers(username, 1))
}
export const searchUsers = (username, page) => {
  return async dispatch => {
    dispatch({
      type: CONST.FETCH_GITHUB_SEARCH_USER_LOADING
    })
    try {
      const response = await fetch(`https://api.github.com/search/users?q=${username}&page=${page}`)
      let data = await response.json()

      dispatch({
        type: CONST.FETCH_GITHUB_SEARCH_USER_SUCCESS,
        payload: data
      })
    }catch(e) {
      dispatch({
        type: CONST.FETCH_GITHUB_SEARCH_USER_FAILURE,
        error: "No This User"
      })
    }
  }
}

redux-thunk

view

dispatch

function

action object

reducer

dispatch

dispatch

action object

update

store

state

dispatch传参:

Action Object,reducer会更新数据

function,函数内进行各种操作(可以用await实现异步),最后dispatch一个Action,进入上一情况

redux-saga

简化了 action creator

onSubmit = username => {
  this.props.dispatch(searchUsers(username, 1))
}
export const searchUsers = function*(action) {
  let {username,page} = action.payload

  yield put({
    type: CONST.FETCH_GITHUB_SEARCH_USER_LOADING
  })

  try {
    let response = yield call(axios.get, `https://api.github.com/search/users?q=${username}&page=${page}`)
    let data = response.data
    yield put({
      type: CONST.FETCH_GITHUB_SEARCH_USER_SUCCESS,
      payload: data
    })
  }catch(e) {
    yield put({
      type: CONST.FETCH_GITHUB_SEARCH_USER_FAILURE,
      error: "No This User"
    })
  }
}

redux-saga

view

Action Object

dispatch

reducer

not match

sagas

match

Action Object

dispatch

store

state

update

update

dispatch一个Action Object:

Action被sagas监听,进入sagas中处理,在sagas中dispatch一个Action Object,reducer更新数据

Action没有被sagas监听,reducer更新数据

二者区别

1. 原理不同

thunks在dispatch时才会创建。sagas是generator,应用启动时便被创建

2. 操作

sagas是generator,因此可以cancel等操作,async是无法做到的

3. 测试

sagas的可中断性,使其相对于其他函数更容易测试

4. action语义

业务逻辑放在sagas相比于业务逻辑放在thunks(async function action),action简洁且无副作用,保持了原意

redux-observable

observable:

在rxJS中,observable => asynchronous immutable array

通俗点 时间轴 + 数组 = observable

emit event sometime

push to array

notify subscriber

redux-observable

const search$ = Observable.fromEvent(input, 'input')  
  .map(e => e.target.value)
  .filter(value => value.length >= 1)
  .throttleTime(100)
  .distinctUntilChanged()
  .switchMap(term => Observable.fromPromise(wikiIt(term)))
  .subscribe(
    x => renderSearchResult(x),
    err => console.error(err)
  )

假设输入了 5 次

每次输入的值一次为:a, ab, c, d, c

并且第 3 次输入的 c 和第 4 次的 d 的时间间隔少于100ms

---a--ab---c-d-----c---|-->

---i--i---i-i-----i---|--> (input)

---a--ab---c-d-----c---|-->

---a--ab---c-------c---|-->

---a--ab---c----------|-->

---x--y---z----------|-->

map

filter

throttleTime

change

map

Marble diagrams

在redux中使用observable

onSubmit = username => {
  this.props.dispatch(searchUsers(username, 1))
}

redux-observable

// actions.js
export const searchUsers = (username, page) => {
  return {
    type: CONST.FETCH_GITHUB_SEARCH_USER_LOADING,
    payload: {
      username,
      page
    }
  }
}
export const searchUsersSuccess = list => {
  return {
    type: CONST.FETCH_GITHUB_SEARCH_USER_SUCCESS,
    payload: {
      total_count: list.total_count,
      items: list.items
    }
  }
}

export const searchUsersFailure = () => {
  return {
    type: CONST.FETCH_GITHUB_SEARCH_USER_FAILURE,
    error: "No This User"
  }
}

在redux中使用observable

redux-observable

// epics.js
const searchUsers$ = (username, page) => Observable.fromPromise(request(`https://api.github.com/search/users?q=${username}&page=${page}`)).delay(2000)
export const searchUsersEpic = action$ => {
  return action$
    .ofType(CONST.FETCH_GITHUB_SEARCH_USER_LOADING)
    .mergeMap(e => {
      return searchUsers$(e.payload.username, e.payload.page)
        .map(list => actions.searchUsersSuccess(list))
        .catch(error => actions.searchUsersFailure())
    })
}
// reducers
export const usersReducer = (state={
  loading: false,
  total: 0,
  data: [],
  error: ''
}, action) => {
  switch(action.type) {
    case CONST.FETCH_GITHUB_SEARCH_USER_LOADING:
      return {
        ...state,
        loading: true
      }
    case CONST.FETCH_GITHUB_SEARCH_USER_FAILURE:
      return {
        loading: false,
        error: action.error
      }
    case CONST.FETCH_GITHUB_SEARCH_USER_SUCCESS:
      return {
        loading: false,
        total: action.payload.total_count,
        data: action.payload.items
      }
    default:
      return state
  }
}

redux-observable

An Epic is the core primitive of redux-observable

Epic is a function which takes a stream of actions and returns a stream of actions.

Actions in, actions out.

View

Action Object

Epics

Action Object

Reducer

Store

State

dispatch

match

dispatch

not match

update

update

redux-saga VS redux-observable

saga:

     Apply a process manager for async, timed, or other non-pure effects so that you can keep application flow logic clean.

redux-observable:

      Applying RxJS observable flow to Redux, and therefore handling async actions in middleware. Actions in -> Actions out styled workflow.

mobx

transparently applying functional reactive programming

Core: observable

mobx

Observable state

class Todo {
    id = Math.random();
    @observable title = "";
    @observable finished = false;
}

Computed values

class TodoList {
    @observable todos = [];
    @computed get unfinishedTodoCount() {
        return this.todos.filter(todo => !todo.finished).length;
    }
}

mobx

Reactions

产生副作用

响应式与命令式之间的桥梁

原理

observable 包装原生值,get 钩子收集依赖,set 钩子触发改变。

autorun 包装观察者函数,在包装时会提前执行一次,并且触发其中涉及到的 observable 值的 get 钩子收集依赖,将当前的观察者函数对应到每个 observable 值上。(同步)

mobx

// index.jsx
@inject('usersStore', 'followersStore', 'followingsStore')
@observer
export default
class App extends React.Component {
  constructor(props) {
    this.state = {
      username: '',
    }
  }
  onSubmit = username => {
    this.props.usersStore.searchUsers(username, 1)
  }
  render() {
    return(
      /*   */
      <input  value={this.props.usersStore.value}/>
      <button onClick={()=>{this.onSubmit(this.state.username)}}>search</button>
    )
  }
}

mobx

// store.js
class UsersStore {
  @observable data = []
  @observable loading = false
  @observable total = 0
  @observable error = ''
  @action
  async searchUsers(username, page) {
    this.loading = true
    await delay(2000)
    try {
      const response = await fetch(`https://api.github.com/search/users?q=${username}&page=${page}`)
      let resData = await response.json()
      // 异步 action 之后,再次修改状态需要动作:
      runInAction(() => {
        this.loading = false
        this.data = resData.items
        this.total = resData.total_count
      })
    }catch(e) {
      runInAction(() => {
        this.loading = false
        this.error = 'No This User'
      })
    }
  }
}
export const usersStore = new UsersStore

mobx

Events

Actions

@action

State

@observable

Computed values

@computed

Reactions

@observer

Modify

update

trigger

redux VS mobx

redux VS mobx

学习成本

MobX 是 magic 的,通过 computed value、observer class、autorun function 对于 View 层的输入逻辑进行了抽象,我们只需要在 View 层对相应的 observale 数据进行访问即可完成自动变更。

Redux 需要考虑如何规范化你的 state,如何使用可记忆的 selector 函数(可记忆的selector来缓存变化),如何更好地处理异步等等。

redux VS mobx

代码量

MobX 内置抽象,代码简洁

Redux 对于每一个变更状态的操作,需要些 action,然后 View 调用 action , 根据 action,写出对应的 reducer。

redux VS mobx

可调试性、可预测性和可测试性

MobX 内部复杂,可以直接在 View 层对 observable 对象进行操作。 useStrict 限制下,只能 action 来修改 state

Redux 是简洁、纯净的,能够进行时间回溯,从 state -> view -> action -> reducer 形成单向闭环,使可调试性和可预测性较好,而对于 action 和 reducer 均可单独测试。

redux VS mobx

模块化

MobX 的 store 是分散的,不同的 store 可以封装对应的业务逻辑。但应用复杂之后,不同 store 之间的协调、分享、相互引用难以维护

Redux 是单一 store,全局共享,无模块化

redux VS mobx

可扩展性

MobX 的 store 分散,改变可以发生在各个地方,无法保证严格顺序执行。

Redux 中所有的修改都被集中处理,严格按照顺序执行

谢谢

数据流对比

By Joson Chen

数据流对比

  • 517