react,redux和dva的源码解析系列之二

544 阅读9分钟

前言

本章介绍react-redux的状态管理、redux-saga的异步流程管理、react-router的路由管理,这些都被Dva框架集成进去,并加以封装。有了这些基础知识,理解Dva就会轻松很多。

react-redux

react-redux是redux基于react框架做的封装。主要工作就是将redux中的state注入到组件中(mapStateToProps),将组件中的用户行为dispatch到redux中(mapDispatchToProps),然后使用connect这个高阶函数包裹组件,自动注入上述逻辑,Provider作为最外层的高阶组件,给组件树中注入store。 下面介绍这几个API的核心原理:

  • mapStateToProps
const mapStateToProps = (state) => {
  return {
    color: state.color,
    name: state.name,
    ...
  }
}

mapStateToProps就是一个函数,接受store中的state,然后返回一个对象(包含部分state属性)

  • mapDispatchToProps
const mapDispatchToProps = (dispatch) => {
  return {
    onSwitchColor: (color) => {
      dispatch({ type: 'CHANGE_COLOR', color: color })
    }
  }
}

mapDispatchToProps也是一个函数,接受store中的dispatch,然后返回一个对象(组件和store中行为的映射关系)

  • connect
const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = {
        allProps: {}
      }
    }

    componentWillMount () {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props)
        : {} // 防止 mapStateToProps 没有传入
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props)
        : {} // 防止 mapDispatchToProps 没有传入
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render () {
      return <WrappedComponent {...this.state.allProps} />
    }
  }
  return Connect
}

//例子
ThemeSwitch = connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch)

connect会将mapStateToProps和mapDispatchToProps执行的结果合并为props,注入到WrappedComponent中,在WrappedComponent内部就可以通过this.props.xxx来使用state和dispatch用户行为了。

  • Provider
class Provider extends Component {
  static propTypes = {
    store: PropTypes.object,
    children: PropTypes.any
  }

  static childContextTypes = {
    store: PropTypes.object
  }

  getChildContext () {
    return {
      store: this.props.store
    }
  }

  render () {
    return (
      <div>{this.props.children}</div>
    )
  }
}

//使用例子,把 Provider 作为组件树的根节点
ReactDOM.render(
  <Provider store={store}>
    <Index />
  </Provider>,
  document.getElementById('root')
)

Provider就是将传入的store注入到context中,然后组件内都可以从context中取到store了。最新版本的react-redux使用了新的React.createContext()来完成context的注入,原理是一样的。

小结

有了这些API,我们就可以在react自由地使用redux管理状态了,核心就是组件的props上被注入了很多属性和方法,方便和store进行交互

redux-saga

一个高效管理副作用,让你的应用方便测试的redux异步流程管理库

saga基本流程

saga本身就是redux的一个中间件,用来处理异步逻辑。 所以第一步就是引入'redux-saga'库的createSagaMiddleware方法,用来生成saga实例。

  • 创建一个 Saga middleware 和要运行的 Saga
  • 将这个 Saga middleware 连接至 Redux store
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

//...
import { incrementAsync } from './sagas'

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
    reducer,
    applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(incrementAsync);

saga作为中间件注入完成后,开始做异步调用了。

这里我们用延迟1秒来模拟api调用。在组件里,dispatch对应的action来触发saga的逻辑

function render() {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={() => action('INCREMENT')}
      onDecrement={() => action('DECREMENT')}
      onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
    document.getElementById('root')
  )
}

与 redux-thunk 不同,上面组件 dispatch 的是一个 plain Object 的 action。现在react的组件dispatch了action,接下来我们需要在saga内部监听这个action,进行响应逻辑。

// Our worker Saga: 将执行异步的 increment 任务
export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}

// Our watcher Saga: 在每个 INCREMENT_ASYNC action spawn 一个新的 incrementAsync 任务
export function* watchIncrementAsync() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

上面的逻辑很简单,saga会监听INCREMENT_ASYNC,并在接收到INCREMENT_ASYNC时,触发incrementAsync函数,执行异步逻辑。watchIncrementAsync会export到启动文件,项目初始化时注入到saga中间件,这样一个简单的saga流程就搭建好了。

saga核心API

接下来介绍,saga框架内部的一些基础知识,主要是一些核心API,这些API都被Dva框架集成进去并加以改造,使用更加方便

  • takeEvery

这是一个最常见的api,上文提到过,用来监听主流程的action。

function* watchFetchData() {
  yield* takeEvery('FETCH_REQUESTED', fetchData)
}

注:takeEvery 允许多个 fetchData 实例同时启动。在某个特定时刻,尽管之前还有一个或多个 fetchData 尚未结束,我们还是可以启动一个新的 fetchData 任务。

function* watchFetchData() {
  yield* takeEvery('FETCH_REQUESTED', fetchData)
  yield* takeEvery('DELETE_REQUESTED', fetchData)
}
  • takeLatest

如果我们只想得到最新那个请求的响应(例如,始终显示最新版本的数据)。我们可以使用 takeLatest 辅助函数。

import { takeLatest } from 'redux-saga'

function* watchFetchData() {
  yield* takeLatest('FETCH_REQUESTED', fetchData)
}
  • call

call仅仅只是创建一条描述函数调用的信息。

// Effect -> 调用 Api.fetch 函数并传递 `./products` 作为参数
{
  CALL: {
    fn: Api.fetch,
    args: ['./products']
  }
}

语义和js里面绑定函数this的call类似,用指定的参数去执行当前的函数

import { call } from 'redux-saga/effects'

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // ...
}

call 同样支持调用对象方法,你可以使用以下形式,为调用的函数提供一个 this 上下文:

yield call([obj, obj.method], arg1, arg2, ...) // 如同 obj.method(arg1, arg2 ...)

这主要是为了方便测试generator的流程,其实功能实现上,完全可以这么写:

function* fetchProducts() {
  const products = yield Api.fetch('/products')
  console.log(products)
}

但是在做单元测试的时候,没法比较返回值。关于单元测试的细节,后面会讲解,这里了解下即可。

  • put

put用来发起action到store中,上面我们通过call调用了API接口,获取了数据,需要把数据存储到store中。其实功能上完全可以这么写:

function* fetchProducts(dispatch)
  const products = yield call(Api.fetch, '/products')
  dispatch({ type: 'PRODUCTS_RECEIVED', products })
}

但是,问题还是不方便测试,测试时无法真实模拟一个dispatch函数,所以引入了put。

import { call, put } from 'redux-saga/effects'
//...

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // 创建并 yield 一个 dispatch Effect
  yield put({ type: 'PRODUCTS_RECEIVED', products })
}

总结 : call用来处理异步逻辑,比如调用接口,put用于数据获取,更新后,发起action到store中,更新state。call和put都是为了测试generator逻辑而引入的,模拟真实的函数调用,返回一个plain javascript对象,方便测试。


以上就是构建基本saga工作流需要用的概念,下面是一些复杂逻辑所涉及的api


  • take

take 就像我们更早之前看到的 call 和 put。它创建另一个命令对象,告诉 middleware 等待一个特定的 action

function* watchAndLog() {
  while (true) {
    const action = yield take('*')
    const state = yield select()

    console.log('action', action)
    console.log('state after', state)
  }
}

与call一样,take也会暂停generator,直到监听的action被发起,这里我们监听任何action。

  • fork

当我们 fork 一个 任务,任务会在后台启动,调用者也可以继续它自己的流程,而不用等待被 fork 的任务结束。是无阻塞版的call函数。

举个简单的实例,我们模拟下用户登陆的行为:

import { take, call, put } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    return token
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  }
}

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const token = yield call(authorize, user, password)
    if(token) {
      yield call(Api.storeItem({token}))
      yield take('LOGOUT')
      yield call(Api.clearItem('token'))
    }
  }
}

上面的逻辑是用户登陆,调用接口进行身份验证获取用户token,验证通过后,我们监听LOGOUT行为,暂停登陆流程,等待登出行为,匹配时则进行清理逻辑。 好像很正常,没什么问题。但是我们没有考虑请求失败和异步调用阻塞的问题。所以,我们必须完善下上面的流程代码。

  1. 首先是阻塞问题,generator在yield call(authorize, user, password)此处等待,如果用户此时点击登出,代码还没有执行到yield take('LOGOUT')无法响应登出行为,就出错了。所以,我们不能让流程在验证接口时阻塞,所以需要引进一个新的函数fork来替代阻塞的call。如果在接口调用过程中,用户登出,我们就取消验证任务。
  2. 现在加上接口容错处理,只需要加上错误处理逻辑就行,监听一个请求失败action。yield take(['LOGOUT', 'LOGIN_ERROR'])

进行补充后的完整业务逻辑就完成了:

import { take, put, call, fork, cancel } from 'redux-saga/effects'

// ...

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    // fork return a Task object
    const task = yield fork(authorize, user, password)
    const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
    if(action.type === 'LOGOUT')
      yield cancel(task)
    yield call(Api.clearItem('token'))
  }
}

注:上面api函数的使用的返回值,不用太在意,都是plain javascript对象,方便进行流程测试。

小结

本节主要介绍了saga的几个核心API,通过它们组建完整的异步流程。

  • call-发起异步请求,处理副作用
  • put-获取数据后,发起一个行为到store中,修改state
  • take-暂停generator,告诉 middleware 等待一个特定的 action
  • fork-任务会在后台启动,是无阻塞版的call函数

react-router介绍

React Router的作用就是保持URL与组件的映射关系。根据当前页面路径,渲染不同的页面的组件

前端路由主要分为hash模式和history模式。hash的原理主要是监听hashchange事件,处理相应的逻辑。history主要是利用H5的pushState和replaceState实现页面的跳转。其中hash和history的区别和取舍就不详述了,相关的文档资料很多

react-router还有一个重要的知识点就是V4版本之后加入了动态路由,官方推荐大家把Route当作组件来使用,并且不要在Route中嵌套Route,多路由匹配时使用switch包裹,只渲染匹配到的第一个路由

V3中使用路由例子:

import { Router, Route, IndexRoute } from 'react-router'

const PrimaryLayout = props => (
  <div className="primary-layout">
    <header>
      Our React Router 3 App
    </header>
    <main>
      {props.children}
    </main>
  </div>
)

const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>

const App = () => (
  <Router history={browserHistory}>
    <Route path="/" component={PrimaryLayout}>
      <IndexRoute component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </Route>
  </Router>
)

render(<App />, document.getElementById('root'))

所有的路由都陈列在入口文件,非常直观,但是不够灵活,与react组件化思想相违背

V4中使用路由例子:

import { BrowserRouter, Route } from 'react-router-dom'

const PrimaryLayout = () => (
  <div className="primary-layout">
    <header>
      Our React Router 4 App
    </header>
    <main>
      <Route path="/" exact component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </main>
  </div>
)

const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>

const App = () => (
  <BrowserRouter>
    <PrimaryLayout />
  </BrowserRouter>
)

render(<App />, document.getElementById('root'))

v4 中 react-router 仓库拆分成了多个包进行发布,

react-router 路由基础库

react-router-dom 浏览器中使用的封装

react-router-native React native 封装

示例中 BrowserRouter 组件便来自 react-router-dom 这个包

动态路由散落项目各个地方,Route会当作组件,按需使用

总结

本节介绍了react-redux、redux-saga、react-router的基本知识和使用技巧,这么都是组成react整个生态的核心,掌握了这些,会对react框架的使用和Dva的学习更加得心应手。