06月22, 2018

redux深入笔记

N久之前,写了一篇redux笔记。然后,现在又有了一些心得,在此记录一下,算是对整个redux的回顾。

渲染状态

let appState={
    title: {color: 'red',text: '标题'},
    content:{color:'green',text:'内容'}
}
function renderTitle(title) {
    let titleEle=document.querySelector('#title');
    titleEle.innerHTML=title.text;
    titleEle.style.color=title.color;
}
function renderContent(content) {
    let contentEle=document.querySelector('#content');
    contentEle.innerHTML=content.text;
    contentEle.style.color=content.color;
}
function renderApp(appState) {
    renderTitle(appState.title);
    renderContent(appState.content);
}
renderApp(appState);

提高数据修改的门槛

  • 一旦数据可以任意修改,所有对共享状态的操作都是不可预料的
  • 模块之间需要共享数据和数据可能被任意修改导致不可预料的结果之间有矛盾
  • 所有对数据的操作必须通过 dispatch 函数
let appState={
    title: {color: 'red',text: '标题'},
    content:{color:'green',text:'内容'}
}
function renderTitle(title) {
    let titleEle=document.querySelector('#title');
    titleEle.innerHTML=title.text;
    titleEle.style.color=title.color;
}
function renderContent(content) {
    let contentEle=document.querySelector('#content');
    contentEle.innerHTML=content.text;
    contentEle.style.color=content.color;
}
function renderApp(appState) {
    renderTitle(appState.title);
    renderContent(appState.content);
}
function dispatch(action) {
    switch (action.type) {
        case 'UPDATE_TITLE_COLOR':
            appState.title.color=action.color;    
            break;    
        case 'UPDATE_CONTENT_CONTENT':
            appState.content.text=action.text;
            break;
        default:
            break;    
    }
}
dispatch({type:'UPDATE_TITLE_COLOR',color:'purple'});
dispatch({type:'UPDATE_CONTENT_CONTENT',text:'新标题'});

renderApp(appState);

封装仓库

显然上面appState和dispatch全局暴露是不合理的,所以我们可以封装一个方法,然后返回出来。

function createStore(reducer) {
    let state;
    let listeners = [];
    let subscribe = (listener) => { // 订阅
        listeners.push(listener);
        return () => {
            // 再次调用时 移除监听函数
            listeners = listeners.filter(fn => fn !== listener);
        }
    }
    let getState = () => JSON.parse(JSON.stringify(state));
    function dispatch(action) { // 发布
        state = reducer(state, action);
        listeners.forEach(listener => listener());
    }
    dispatch({});
    // 将方法暴露给外面使用,将状态放到了容器中外部无法在进行更改了
    return { dispatch, getState, subscribe }
}

createStore方法接收一个reducer的函数。那么问题来了,reducer是什么?

其实就是一个纯函数,接收stateaction两个参数,根据老的状态和新传递的动作算出新的状态。类似:

let initState = {
    title: {
        color: 'red',
        text: '标题'
    },
    content: {
        color: 'blue',
        text: '内容'
    }
}
const CHANGE_TITLE_COLOR = 'CHANGE_TITLE_COLOR';
const CHANGE_CONTENT_TEXT = 'CHANGE_CONTENT_TEXT';
function reducer(state = initState, action) {
    switch (action.type) {
        case CHANGE_TITLE_COLOR:
            return { ...state, title: { ...state.title, color: action.color } }
        case CHANGE_CONTENT_TEXT:
            return { ...state, content: { ...state.content, text: action.text } };
        default:
            return state;
    }
}

在react中的使用

根据之前写的createStore,我们就可以写一个简单的加减计数器的功能。

import React, { Component } from 'react';
import ReactDOM from "react-dom";
import { createStore } from './redux';
let initState = { number: 0 };
// 创建需要的方法 ation-type
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
// 创建规则
function reducer(state = initState, action) { //{type:'IN...',count:2}
    switch (action.type) {
        case INCREMENT:
            return { number: state.number + action.count };
        case DECREMENT:
            return { number: state.number - action.count }
    }
    return state;
}
// 创建容器
let store = createStore(reducer);
export default class Counter extends Component {
    constructor() {
        super();
        this.state = { number: store.getState().number }
    }
    componentDidMount() {
        // 组件挂载完成后 希望订阅一个更新状态的方法,只要状态发生变化,就setState更新视图
        this.unsub = store.subscribe(() => {
            this.setState({ number: store.getState().number })
        });
    }
    componentWillUnmount() {
        // 移除订阅
        this.unsub();
    }
    render() {
        return (
            <div>
                计数器 {this.state.number}
                <button onClick={() => {
                    store.dispatch({ type: INCREMENT, count: 2 })
                }}>+</button>
                <button onClick={() => {
                    store.dispatch({ type: DECREMENT, count: 1 })
                }}>-</button>
            </div>
        )
    }
}

ReactDOM.render(<Counter />, window.root);

拆分及combineReducers的实现

我们可以有这样的一个结构:

alt

当然有些人可能喜欢将action和reducer写在一起。目录中的action-types文件用来存放常量。

我们都知道redux的store只能是一根树,所以当我们拆分成多个文件时,需要有一个方法将多个reducer合并成一个reducer。

// => {c:{number:0},t:{todos:[]}}
// 合并reducer 把他们合并成一个
// key是新状态的命名空间 值是reducer,执行后会返回一个新的reducer
function combineReducers(reducers) {
    // 第二次调用reducer ,内部会自动的把第一次的状态传递给reducer
    return (state = {}, action) => {
        let newState = {}
        // reducer默认要返回一个状态,要获取counter的初始值和todo的初始值
        for (let key in reducers) {
            // 默认reducer俩参数 一个叫state,一个叫action
            let s = reducers[key](state[key], action);
            newState[key] = s;
        }
        return newState;
    }
}

react-redux

假设没有这货,那么我们都需要在每个组件中的componentDidMount,去手动store.subscribe,这样会造成相似的代码比较多。

因此就有了这个模块,它提供了两个东西:Providerconnect

它的实现思路就是context传递数据。

Provider实现特别简单:

let Context = React.createContext();
class Provider extends React.Component {
    render() {
        return <Context.Provider value={this.props.store}>
            {this.props.children}
        </Context.Provider>
    }
}

connect实现略微复杂一些,我们平常使用的方式是这样的:

export default connect(mapStateToProps, mapDispatchToProps)(component)

也就是说它会返回一个高阶组件。

mapStateToProps是一个函数,它接收两个参数,stateownProps

mapDispatchToProps可以是一个函数,参数为dispatch,也可以是一个对象。

let connect = (mapStateToProps, mapDispatchToProps) => (Component) => {
    return function (ownProps) {
        // 可以算出一些属性 传递给Component
        return <Context.Consumer>
            {(store) => {
                class High extends React.Component {
                    constructor(props) {
                        super(props);
                        this.state = mapStateToProps(store.getState(), ownProps);
                        this.old = store.getState();
                    }
                    componentDidMount() {
                        // 每次派发dispatch都会调用subscibe中的方法
                        this.unsub = store.subscribe(() => {
                            // 如果改的是同一个对象 不去更新视图
                            if (this.old == store.getState()) return; // 当然这个写法,并不严谨
                            this.old = store.getState();
                            // let ownProps = c.props;
                            this.setState(mapStateToProps(store.getState(), ownProps));
                        });
                    }
                    componentWillUnmount() {
                        this.unsub();
                    }
                    render() {
                        let actions;
                        // 判断mapDispatchToProps是不是函数 不是函数就用bindActionCreators转化成对象
                        if (typeof mapDispatchToProps == 'function') {
                            actions = mapDispatchToProps(store.dispatch);
                        } else {
                            actions = bindActionCreators(mapDispatchToProps, store.dispatch);
                        }
                        return (
                            <Component
                                {...this.state}
                                {...actions}
                            ></Component>
                        )
                    }
                }
                return <High></High>
                // let state = mapStateToProps(store.getState());
                // let actions = mapDispatchToProps(store.dispatch);
                // // 状态变化后 需要更新视图  this.setState()
                // return <Component {...state} {...actions}></Component>
            }}
        </Context.Consumer>
    }
}

在代码中有一个bindActionCreators的函数(它是redux库里面的),这个需要简单说一下,它的作用就是将:

const mapDispatchToProps = {
  onClick: (filter) => {
    type: 'SET_VISIBILITY_FILTER',
    filter: filter
  };
}

转成下面的代码

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

因此bindActionCreators的实现如下:

export default function bindActionCreators(actions,dispatch){
    let newActions = {};
    for(let attr in actions){
     newActions[attr] = function(){
         dispatch(actions[attr].apply(null,arguments));
     }
    }
    return newActions;
 }

概念总结

看了上面的一些代码,我们基本上要明白以下几个概念:

  • store
  • state(通过store如何获取state)
  • action
  • action creator
  • store.dispatch
  • reducer

中间件

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

这是 redux 作者 Dan 对 middleware 的描述,middleware 提供了一个分类处理 action 的机会,在 middleware 中你可以检阅每一个流过的 action,挑选出特定类型的 action 进行相应操作,给你一次改变 action 的机会。

这里有几个概念:

  • 可以对每个流过的action,进行相应的操作,比如前后加一些东西,redux-logger就是这样的功能
  • 特定类型,我们都知道,默认的类型是一个对象(action),然后里面要有一个type的key。考虑到异步,所以就有了function的类型,如redux-thunk,或者是promise的类型,如redux-promise

在redux中有一个方法是:applyMiddleware

它的实现思路就是改变dispatch,比如我们写一个自己的中间件:

let store = createStore(reducer);
let dispatch = store.dispatch;
store.dispatch = function (action) {
  console.log(store.getState().number);
  dispatch(action);
  console.log(store.getState().number)
};
export default store;

虽然实现了,但是这种方案并不好。

我们先来考虑一个中间件的情况:

function applyMiddleware(middleware) {
    return function (createStore) {
        return function (reducer) {
            let store = createStore(reducer);//创建出原生的仓库 getState dispatch
            let dispatch;
            let middlewareAPI = {
                getState: store.getState,
                dispatch: action => dispatch(action)
            };
            middleware = middleware(middlewareAPI);
            dispatch = middleware(store.dispatch);
            return { ...store, dispatch }
        }
    }
}

applyMiddleware的用法:

let store = applyMiddleware(你指定的中间件)(createStore)(reducer);

中间件的写法如下:

let middleware = ({ dispatch, getState }) => next => action => {
    // 处理逻辑代码,如判断action是否是promise,是否是function
    return next(action);
};

compose

显然一个中间件,肯定满足不了我们的需求。所以上面的方法,肯定得改造。

在改造前,我们先来实现一个compose的函数功能。

function addA(str) {
    return str + 'A';
}
function addB(str) {
    return str + 'B';
}
function addC(str) {
    return str + 'C';
}
//let r = addC(addB(addA('hello')));
//console.log(r);//helloABC

function compose(...fns) {
    if (fns.length == 0) {
        return (args) => args;
    } else if (fns.length == 1) {
        return (...args) => fns[0](...args);
    } else {
        let last = fns.pop();
        return function (...args) {
            let result = last(...args);//helloA
            //1  val=helloA   item=addB =>helloAB
            //2  val=helloAB  item=addC =>helloABC
            return fns.reduceRight((val, item) => {
                return item(val);//helloAB
            }, result);//helloA
        }
    }
}

有一个更优雅的写法:

let compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)));
/**
 * 第一次执行 a=addC b= addB  =>  函数 参数先传给b,b执行结果再传给a,再把a的结果返回
 *   args=>addC(addB(args));
 * 第二次执行 a=args=>addC(addB(args));  b=addA =>  xx=>addC(addB(addA(xx))
 *
 */

有了这个函数,我们就可以将一个参数的applyMiddleware方法进行改造了:

let applyMiddleware1 = (...middlewares) => createStore => reducer => {
    let store = createStore(reducer);
    let dispatch;
    let middlewareAPI = {
        getState: store.getState,
        //此处不要简化为dispatch,因为我们希望让middlewareAPI.dispatch调用到增加后的dispatch
        dispatch: action => dispatch(action)
    }
    //[thunk,promise,logger]
    middlewares = middlewares.map(middleware => middleware(middlewareAPI));
    //把多个中间件组合成一个函数,接收一个参数,并获取一个返回值
    dispatch = compose(...middlewares)(store.dispatch);
    return { ...store, dispatch }
}

需要说明的是,这个中间件数组是有顺序的,比如像logger中间件,必须要放在最后,因为它是从最外层开始监控的。

创建store的两种写法

写法一:

let store = applyMiddleware(thunk,promise,logger)(createStore)(reducer);

写法二:

let store = createStore(reducer, applyMiddleware(thunk, promise, logger));

之所以会有写法2,是因为在源码中:

function createStore(reducer, enhancer) {
    if (typeof enhancer == 'function') {
        return enhancer(createStore)(reducer);
    }
    ....
}

redux-logger

function logger({ getState, dispatch }) {
    return function (next) {//如果想继续,则可以调用next store.dispatch
        return function (action) {
            console.log('before state', getState());
            console.log(action);
            next(action);
            console.log('after state', getState());
        }
    }
}

redux-thunk

用来处理异步请求的action

function thunk({ getState, dispatch }) {
    return function (next) {//next指向的是下一个中间件或者dispatch
        return function (action) {
            if (typeof action == 'function') {
                action(dispatch, getState);
            } else {
                next(action);
            }
        }
    }
}

redux-promise

let promise = ({ getState, dispatch }) => next => action => {
    if (action.then && typeof action.then == 'function') {
        action.then(dispatch);
    } else if (action.payload && action.payload.then && typeof action.payload.then == 'function') {
        action.payload.then(function (payload) {
            dispatch({ ...action, payload });
        }, function (payload) {
            dispatch({ ...action, payload });
        });
    } else {
        next(action);
    }
}

redux-saga

讲真,前三个中间件还是比较容易的,但这个saga略复杂一些。

工作原理

  • sagas 采用 Generator 函数来 yield Effects(包含指令的文本对象)
  • Generator 函数的作用是可以暂停执行,再次执行的时候从上次暂停的地方继续执行
  • Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。
  • 你可以通过使用 effects API 如 fork,call,take,put,cancel 等来创建 Effect。

具体用法

组件的代码:

class UserComponent extends React.Component {
  ...
  onSomeButtonClicked() {
    const { userId, dispatch } = this.props
    dispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})
  }
  ...
}

入口js:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

// then run the saga
sagaMiddleware.run(mySaga)

// render the application

sagas文件:

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

// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

/*
  Starts fetchUser on each dispatched `USER_FETCH_REQUESTED` action.
  Allows concurrent fetches of user.
*/
function* mySaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}

/*
  Alternatively you may use takeLatest.

  Does not allow concurrent fetches of user. If "USER_FETCH_REQUESTED" gets
  dispatched while a fetch is already pending, that pending fetch is cancelled
  and only the latest one will be run.
*/
function* mySaga() {
  yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}

export default mySaga;

redux-saga

alt

与redux-thunk的区别

用过redux-thunk的人会发现,redux-saga 其实和redux-thunk做的事情类似,都是可以处理异步操作和协调复杂的dispatch。不同点在于:

  • sagas 是通过 Generator 函数来创建的,意味着可以用同步的方式写异步的代码;
  • thunks 是在 action 被创建时才调用,sagas 在应用启动时就开始调用,监听action 并做相应处理; (通过创建 sagas 将所有的异步操作逻辑收集在一个地方集中处理)
  • 启动的任务可以在任何时候通过手动取消,也可以把任务和其他的 Effects 放到 race 方法里可以自动取消

常用api

  • call(fn, ...args)
    • 创建一条 Effect 描述信息,指示 middleware 调用 fn 函数并以 args 为参数。
    • fn: Function - 一个 Generator 函数, 或者返回 Promise 的普通函数
    • args: Array - 一个数组,作为 fn 的参数
  • takeEvery(pattern, saga, ...args)
    • 在发起的 action 与 pattern 匹配时派生指定的 saga。
    • 每次发起一个 action 到 Store,并且这个 action 与 pattern 相匹配,那么 takeEvery 将会在后台启动一个新的 saga 任务。
    • pattern: String | Array | Function
    • saga: Function - 一个 Generator 函数
    • args: Array - 将被传入启动的任务作为参数。takeEvery 会把当前的 action 放入参数列表(action 将作为 saga 的最后一个参数)
  • put(action)
    • 创建一条 Effect 描述信息,指示 middleware 发起一个 action 到 Store。
    • action: Object
  • take(pattern)
    • 创建一条 Effect 描述信息,指示 middleware 等待 Store 上指定的 action。 Generator 会暂停,直到一个与 pattern 匹配的 action 被发起。
    • 如果调用 take 时参数为空,或者传入 '*',那将会匹配所有发起的 action(例如,take() 会匹配所有的 action)。
    • 如果是一个函数,action 会在 pattern(action) 返回为 true 时被匹配(例如,take(action => action.entities) 会匹配那些 entities 字段为真的 action)。
    • 如果是一个字符串,action 会在 action.type === pattern 时被匹配(例如,take(INCREMENT_ASYNC))。
    • 如果参数是一个数组,会针对数组所有项,匹配与 action.type 相等的 action(例如,take([INCREMENT, DECREMENT]) 会匹配 INCREMENT 或 DECREMENT 类型的 action)。
  • all([...effects]) - parallel effects
    • Creates an Effect description that instructs the middleware to run multiple Effects in parallel and wait for all of them to complete. It's quite the corresponding API to standard Promise#all.
    • 当我们需要 yield 一个包含 effects 的数组, generator 会被阻塞直到所有的 effects 都执行完毕,或者当一个 effect 被拒绝 (就像 Promise.all 的行为)。

take与takeEvery

function * logger(getState,action){
    console.log('action',action);
    console.log('newState',getState());
}
function* watchAndLog(getState){
  yield takeEvery('*',logger,getState);
}
function* watchAndLog2(getState,action){
    while(true){
        // take 就像我们更早之前看到的 call 和 put。它创建另一个命令对象,告诉 middleware 等待一个特定的 action
        // 正如在 call Effect 的情况中,middleware 会暂停 Generator,直到返回的 Promise 被 resolve
        // 在 take 的情况中,它将会暂停 Generator 直到一个匹配的 action 被发起了
        // 在 takeEvery 的情况中,被调用的任务无法控制何时被调用, 它们将在每次 action 被匹配时一遍又一遍地被调用。并且它们也无法控制何时停止监听。
        // 而在 take 的情况中,控制恰恰相反。与 action 被 推向(pushed) 任务处理函数不同,Saga 是自己主动 拉取(pulling) action 的。
        const action = yield take('*');
        console.log('action',action);
        console.log('newState',getState());
    }
}
export function* watchIncrement(dispatch) {
  //用来监听特定的动作,
  for (let i = 0; i < 3; i++) {
    console.log("current i :", i);
    //take监听指定的动作 ,只不过他只监听一次
    let action = yield take(types.INCREMENT_ASYNC);
    yield increment();
  }
}

根据take的这个特性,我们可以做一个登录流程的saga功能,伪代码如下:

function* loginFlow() {
  while (true) {
    let { username, password } = yield take(types.LOGIN_REQUEST);
    let token = yield login(username, password);
    if (token) {
      yield take(types.LOGOUT_REQUEST);
      //跳回登录
      yield put(push('/login'));
    }
  }
}

学习资料

总结

单纯redux其实还是比较容易的,然而像redux-saga这种,确实不容易理解或者进行书写。

本文链接:www.my-fe.pub/post/redux-deep-note.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。