写在前面
react自从2013年推出到如今已经走过了很多个年头,react生态redux、react-redux、redux-saga也已经成为react开发者的配套标准,使用起来已经相当熟悉,本文简单聊聊redux、react-redux和redux-saga的实现原理。
redux
这张经典的图描述了redux的工作流程,简单来说就是view层触发一个action到dispatcher,dispatcher将aciton传入reducer进行计算,得到新的state后传入view层进行状态更新。
下边是一个简易版的createStore.js代码
export function createStore(reducer, enhandler) {
if (enhandler) {
return enhandler(createStore)(reducer)
}
let state = {}; //全局state存放的地方
let observers = []; // 观察者队列
// getter
function getState() {
return state;
}
// setter
function dispatch(action) {
state = reducer(state, action);
observers.forEach(fn => fn());
}
function subscribe(fn) {
observers.push(fn);
}
dispatch({ type: '@@REDUX_INIT' }) // 初始化state数据
return {getState, dispatch, subscribe}
}
可以看到,createStore是一个高阶函数,接收reducer函数和enhandler函数作为参数,并返回getState, dispatch, subscribe三个函数,其中
reducer用来根据当前的state和传入的action计算出新的state,redux规定了reducer必须为纯函数;
enhandler是执行applyMiddleware后的中间件扩展函数,用来对dispatch进行增强;
getState单纯地返回state,相当于getter;
dispatch是唯一修改state的入口,相当于setter;
subscribe是对state的订阅,当state产生变化时,会触发对应的回调函数。
简单来说,createStore将全局state作为一个闭包变量保存,保证了外界无法直接读取修改,同时返回了操作state的句柄,以及对于state变化提供了监听。
reducer部分,一个例子根据不同的action类型分别对state进行计算,返回新的state
import { ADD, SUB } from '../action/app';
const initialState = {
count: 0
}
export const appReducer = function(state, action) {
switch (action.type) {
case ADD:
return {
...state,
count: state.count + action.text
}
case SUB:
return {
...state,
count: state.count - action.text
}
default:
return state || initialState;
}
}
通常我们会将reducer合并后再传入createStore
import {combineReducers} from '../myRedux';
import {appReducer} from './app';
import {compReducer} from './comp';
const rootReducer = combineReducers({
app: appReducer,
comp: compReducer
})
export default rootReducer;
combineReducers的代码
export const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState;
},
{}
);
};
}
可以看到,在createStore传入rootReducer后,得到的state结构为
{
app: {},
comp: {}
}
combineReducers将state进行分发, 例如appReducer只传入app key对应的数据,达到了拆分数据,单独进行处理的效果。
redux同时也提供了扩展的功能,我们知道,这一步扩展在dispatch到reducer之间实现,即通过对dispatch的增强,来达到扩展其他功能的效果,下边是简易版applyMiddleware.js的实现
function compose(...fns) {
if (fns.length === 0) return arg => arg
if (fns.length === 1) return fns[0]
return fns.reduce((res, cur) =>(...args) => res(cur(...args)))
}
export const applyMiddleware = (...middlewares) => {
return (createStore) => {
return (reducer) => {
const store = createStore(reducer)
let { getState, dispatch } = store
const params = {
getState,
dispatch: (action) => dispatch(action)
}
const middlewareArr = middlewares.map(middleware => middleware(params))
dispatch = compose(...middlewareArr)(dispatch)
return { ...store, dispatch }
}
}
}
可以看到,applyMiddleware是一个柯里化函数,刚好对应了enhandler(createStore)(reducer)的调用方式,其中最关键的两行代码
const middlewareArr = middlewares.map(middleware => middleware(params))
dispatch = compose(...middlewareArr)(dispatch)
此处对传入的中间件依次进行初始化,将getState, dispatch塞入中间件中,使得中间件有访问store的能力,在完成中间件的第一步调用后,再利用compose函数将多个中间件串联起来,传入旧的dispatch进行第二次调用,最终返回增强后的dispatch。
看看一个中间件的例子,同样地中间件也是一个柯里化函数
export default function logger({ getState }) {
return (next) => (action) => {
let returnValue = next(action)
console.log('state after dispatch', getState())
return returnValue
}
}
也就是说,applyMiddleware、中间件logger都是柯里化后的函数,利用了其延迟执行的特点,分步进行调用,最终
applyMiddleware(middleware1, middleware2, middleware3)经过处理后,会得到
dispatch = middleware1(middleware2(middleware3(action)))的执行方式,即洋葱模型
总之,redux的实现,总体思路是将state提取到统一的地方进行管理,state设置为只读,并只能通过reducer进行更改。约定action为对象,reducer为纯函数,并且不干涉异步场景的处理,只提供middler机制开放出扩展的功能。 实现原理上看,代码体现了函数式编程的思想,多次运用高阶函数,函数柯里化等技巧,代码设计得相当简洁和巧妙。
react-redux
redux与react一起使用时,我们需要手动在组件里通过subscribe监听state的变化并更新组件,为了解决这样的问题,redux官方提供了react-redux的库,通过connect的方式连接state和react组件,达到自动监听的效果,使用方式如下
index.js
<Provider store={store}>
<React.StrictMode>
<App />
<Comp />
</React.StrictMode>
</Provider>
app.js
class APP extends React.Component {
constructor(props) {
super(props)
}
handleAddItem = () => {
const {dispatch} = this.props;
dispatch({
type: `${namespace}/ADD_ITEM`,
text: 2
})
}
handleDelItem = () => {
const {dispatch} = this.props;
dispatch({
type: `${namespace}/DEL_ITEM`,
text: 2
})
}
render() {
const {comp} = this.props;
const {list} = comp;
return (
<div>
<ul>
{
list.map(i => {
return <li>{i}</li>
})
}
</ul>
<button onClick={this.handleAddItem}>add li</button>
<button onClick={this.handleDelItem}>del li</button>
</div>
)
}
}
function mapStateToProps(state){
return {
comp: state.comp
}
}
function mapDispatchToProps(dispatch){
return {
dispatch
}
}
export default connect(mapStateToProps, mapDispatchToProps)(APP);
主要看看provider.js和connect.js的实现
provider.js简单版
import React from 'react'
import PropTypes from 'prop-types'
export default class Provider extends React.Component {
// 需要声明静态属性childContextTypes来指定context对象的属性,是context的固定写法
static childContextTypes = {
store: PropTypes.object
}
constructor(props, context) {
super(props, context)
this.store = props.store
}
// 实现getChildContext方法,返回context对象,也是固定写法
getChildContext() {
return { store: this.store }
}
// 渲染被Provider包裹的组件
render() {
return this.props.children
}
}
可以看到,利用了react的conetext功能,只需要在最顶级套上Provider组件,其他所有组件均可从conetext获取到state,避免了props的层层传递
再看看connect的简单实现
import React from 'react';
import PropTypes from 'prop-types';
export function connect(mapStateToProps, mapDispatchToProps) {
return function(Component) {
class Connect extends React.Component {
componentDidMount() {
//从context获取store并订阅更新
this.context.store.subscribe(this.handleStoreChange.bind(this));
}
handleStoreChange() {
// 触发更新
// 触发的方法有多种,这里为了简洁起见,直接forceUpdate强制更新,读者也可以通过setState来触发子组件更新
this.forceUpdate()
}
render() {
const {store} = this.context;
const {getState, dispatch} = store;
return (
<Component
// 传入该组件的props,需要由connect这个高阶组件原样传回原组件
{ ...this.props }
// 根据mapStateToProps把state挂到this.props上
{ ...mapStateToProps(getState()) }
// 根据mapDispatchToProps把dispatch(action)挂到this.props上
{ ...mapDispatchToProps(dispatch) }
/>
)
}
}
//接收context的固定写法
Connect.contextTypes = {
store: PropTypes.object
}
return Connect
}
}
connect是一个高阶组件,接收mapStateToProps和mapDispatchToProps参数,其中mapStateToProps的作用是将特定的state映射到组件的props上,mapDispatchToProps将dispatch(action)映射到props上,并在componentDidMount统一进行store的subscribe监听,当state变化时,被connect的所有组件都会进行一次render。
总结:Provider的本质是利用context统一传递,connect本质是将监听和获取state的逻辑进行统一抽取复用,这也是高阶组件的常用功能,被connect的组件变成了UI型组件,只需要从props中获取到状态进行渲染即可。
redux-saga
提到redux-saga,通常会提到redux-thunk,两者都是redux的中间件,都是对异步场景的处理。redux-thunk非常简短,只有十几行的代码,简单实现如下
export default function thunk({ dispatch, getState }) {
return (next) => (action) => {
if (typeof action === 'function') {
action(dispatch, getState())
}
next(action)
}
}
可以看到,thunk支持了function形式的action,将dispatch句柄交由function去处理,我们可以在action进行异步调用,等到结果返回时再进行dispatch达到支持异步的效果,一个简单的使用例子
const addCountAction = (text) => {
return {
type: ADD,
text
}
}
const fetchData = (text) => (dispatch) => {
new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 2000)
}).then(() => {
dispatch(addCountAction(text))
})
}
这里假定异步结果2s后返回,返回后再进行dispatch的调用。
redux-thunk的大概流程:
redux-thunk虽然支持了异步场景,但其存在的缺点也很明显:
1、使用回调的方式来实现异步,容易形成层层回调的面条代码
2、异步逻辑散落在各个action中,难以进行统一管理
因此,出现了redux-saga更强大的异步管理方案,可以代替redux-thunk使用。
redux-saga的大概流程:
其主要特点
1、使用generator的方式实现,更加符合同步代码的风格;
2、统一监听action,当命中action时,执行对应的saga任务,并且支持各个saga之间的互相调用,使得异步代码更方便统一管理。
在saga中,出现了新的概念,其中effect指一个普通的js对象,描述一个指定的动作,saga指一个generator函数。
首先看看saga的接入:
//index.js
const sagaMiddleware = createSagaMiddleware();
store = createStore(rootReducer, applyMiddleware(sagaMiddleware, logger));
sagaMiddleware.run(rootSaga)
//rootSaga.js
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
// Our worker Saga: 将异步执行 increment 任务
function* addAsync(action) {
yield fork(delay, 1000)
yield put(formatAction(action, namespace))
}
export default function* rootSaga() {
yield takeEvery(`${namespace}/ADD_ASYNC`, addAsync)
}
先通过createSagaMiddleware生成sagaMiddleware,注册成为redux的中间件,再调用中间件暴露的run方法,run的作用是统一初始化rootSaga,开启对action的监听。其中createSagaMiddleware的简单版如下:
export default function sagaMiddlewareFactory() {
let _store; //闭包store,后续sagaMiddleware可以访问到
function sagaMiddleware(store) {
_store = store;
return (next) => (action) => {
next(action);
channel.put(action)
}
}
// 启动rootSaga,即进行入口saga的自执行
sagaMiddleware.run = function(rootSaga) {
let iterator = rootSaga();
proc(iterator, _store);
}
return sagaMiddleware;
}
sagaMiddleware是一个符合redux标准的中间件,并在sagaMiddleware挂载了run方法,run方法中调用了proc,看看proc的实现
export default function proc(iterator, store) {
next();
function next(err, preValue) {
let result;
if (err) {
result = iterator.throw(err);
} else {
result = iterator.next(preValue)
}
if (result.done) return result;
if (isPromise(result.value)) { //yield promise
let promise = result.value;
promise.then((success) => next(null, success)).catch((err) => next(err, null))
} else if (isEffect(result.value)) { //yield effect
let effect = result.value;
runEffect[effect.type](effect, next, store)
} else { //yield others
next(null, result.value)
}
}
}
可以看到,proc是一个generator的自执行器,通过递归的方式实现,在result.done为true时表示完成generator的执行。我们假定只yield三种类型,promise、effect和普通语法,分别进行对应处理。例如effect,当在saga中 yield put(action)时,只是调用了put普通函数,返回的了一个put类型的effect,effect是一个普通的js对象,看看effect的定义:
export function take(signal) {
return {
isEffect: true,
type: 'take',
signal
}
}
export function put(action) {
return {
isEffect: true,
type: 'put',
action
}
}
export function takeEvery(signal, saga) {
return {
isEffect: true,
type: 'takeEvery',
signal,
saga
}
}
所以,effect描述了该任务的类型和相关参数,effect的执行是在runEffect环节,即runEffect.js:
function runTake({signal}, next, store) {
channel.take({
signal,
callback: (args) => {next(null, args)}
})
}
function runPut({action}, next, store) {
const {dispatch} = store;
dispatch(action);
next();
}
function runTakeEvery({signal, saga, ...args}, next, store) {
function *takeEveryGenerator() {
while(true) {
let action = yield take(signal);
yield fork(saga, action);
}
}
runFork({saga: takeEveryGenerator}, next, store);
}
runEffect执行对应的effect,比如put,可以看到本质是对dispatch的封装。saga提供的其他辅助函数takeEvery等,是对低级effect的封装。
那么当我们使用takeEvery监听到了action,调用take进行监听,函数中调用的channel.take是什么意思呢?看看channel实现例子:
function channel() {
let _task = null;
function take(task) {
_task = task;
}
function put(action) {
const {type, ...args} = action;
if (!_task) {
return;
}
_task.signal === type && _task.callback(action);
}
return {
take, put
}
}
export default channel();
可以看到,channel的实现是一个简单生产消费者的模式,take生成任务,put消费任务。这里就可以看到take为什么是阻塞的了,当take一个action类型时,实际上是往channel中put了一个任务,只有在该action被dispatch时,调用put消费,此处take effect所在的saga的迭代器才会被继续执行下去,所以执行到take时,实际上是迭代器next没有进行下一步的迭代,导致saga 阻塞。
总结:从这个简易的模型可以看出,redux-saga其实是在dispatch和reducer之间架设了一层异步处理层,专门来处理异步任务。 在sagaMiddleware初始化run时,对入口的saga进行了自执行,开始了对action的监听。遇到yield的effect时交由对应的runEffect执行,命中action时则派生对应的saga任务,这就是redux-saga大概的原理。
至此,完成了对于redux、react-redux、redux-saga的原理的简单分析,不仅可以从中学习优秀的设计思路,也能在业务使用中做到知其所以然。
demo地址:github.com/lianxc/lear…
参考资料:
written by:先崇@ppmoney