3

Redux 主要知识学习总结

 2 years ago
source link: https://knightyun.github.io/2020/11/29/js-redux-summary
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Redux 作为一个状态管理器,可以应用于多种 web 技术或框架中,React 只是其中之一;Redux 的特点在于,多个页面或组件使用同一个状态(store,用于管理应用的 state),可以实现各模块或组件之间的数据共享,应用的任何部分都能进行状态修改,避免了传统的组件间深层次传值问题;

创建状态(store)

Redux.createStore() 方法用于创建一个 store,其接收 reducer 作为第一个参数;

reducer 为一个自定义函数,接收 state 作为第一个参数,同时返回一个值作为新的 state

reduce 有缩减,减少的意思,可以理解为一个缩减器,不断将新得到的状态覆盖原状态,以实现 store 的单一状态更新,其名字也是根据 Array.reduce() 方法而来的;

import Redux from 'redux';

// 为 state 设置默认值
const reducer = (state = 1, action) => {
    return state;
}
const store = Redux.createStore(reducer);

createStore() 方法还接收第二个参数 initialState,作为 state 的初始化值,即下面两种写法效果相同:

import Redux from 'redux';

const store1 = Redux.createStore((state = 1) => {
    return state;
});
const store2 = Redux.createStore((state) => {
    return state;
}, 1);

const state1 = store1.getState();
const state2 = store2.getState();

console.log(state1); // 1
console.log(state2); // 1

getState(),是所创建的 store 对象的一个方法,用于获取创建的状态;

const store = Redux.createStore(
    (state = 1) => state;
);
const state = store.getState();

console.log(state); // 1

改变状态(action)

state 的更新需要通过触发 action 来实现,actoin 是前面的 reducer 函数接收的第二个参数,一个 action 是一个包含操作信息的对象,同时也可以携带要传递的额外数据;

触发 action 使用 dispatch 实现,dispatchstore 对象的一个方法,其接收参数为 action 对象,是更新状态的唯一途径;

这里之所以多加一层 action,而不直接修改状态,是为了追踪某一状态为何更新,或者调试时进行操作复现等目的,而 action 中的 type 就相当于为了被追踪而留下的痕迹;

const store = Redux.createStore(
    (state=1, action) => {
        if (action.type === 'myAction') {
            return action.myData;
        } else {
            return state;
        }
    }
);

const action = {
    type: 'myAction', // type 属性为必填项
    myData: 'myContent.', // 自定义携带数据
}

store.dispatch(action);
console.log(store.getState()); // "myContent."

在模块较多的复杂应用中,为了辨识操作,方便理解,通常 type 的格式会定义为 模块/操作 的形式,模块一般和对应的 reducer 相关,例如:

const todoReducer = (state, action) => state;
const userReducer = (state, action) => state;

const addTodo = { type: 'todo/add' };
const renameUser = { type: 'user/rename' };

更新 stateaction 被触发了,还需要定义一些操作对其进行响应,在 action 触发时执行,即指定如何更新 state

这里更新 state 的逻辑写在之前创建 store 时传入的 reducer 函数中,由于 Redux 中的 state 是只读的(并未强制,但需自行在代码中遵守),所以 reducer 每次返回的 state 都是新的;

const myState = {
    num: 0
}
const myReducer = (state=myState, action) => {
    if (action.type === 'add') {
        return {
            num: state.num + 1
        };
    } else {
        return state; // 非指定状态需要考虑返回原状态
    }
}
const store = Redux.createStore(myReducer);
const myAction = {
    type: 'add'
}

store.dispatch(myAction);

console.log(store.getState().num); // 1

Redux 并未强制 reducer 中的 state 为只读的,其实是可以对其进行修改,例如:

const defaultState = { num: 0 };
const store = Redux.createStore(
    (state=defaultState, action) => {
        if (action.type === 'add') {
            state.num += 1;
            return state;
        } else {
            return state;
        }
    }
);

console.log(store.getState()); // { num: 0 }

store.dispatch({ type: 'add'});

console.log(store.getState()); // { num: 1 }

但官方并不建议这么做,这有可能导致页面数据得不到及时更新的 bug,所以需要开发者考虑自行维护其不可变性(Immutability),这也能实现更好的状态追踪,问题追溯等开发体验,如官网提到的一项叫 time traveling debugging 技术;并且,Redux 官网对该框架的介绍也是 Redux is a predictable state container,即具有预见性的状态管理器;

subscribe() 是 store 对象的方法之一,它接收一个函数作为参数,用于设置监听器以订阅状态的更新,即指定 state 更新时应该做什么;

const store = Redux.createStore((state=0, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else {
        return state;
    }
});

store.subscribe(() => {
    // 指定每次更新状态就打印当前值
    console.log('dispatch', store.getState());
})
store.dispatch({ type: 'add' }); // 'dispatch' 1
store.dispatch({ type: 'add' }); // 'dispatch' 2

虽然 Redux 为了管理方便而设置单一的 store 对所有 state 进行统一管理,但是状态量的增长会使得书写变得复杂,所以 Redux 对象提供了一个 combineReducers() 方法,将所有声明的分工不同(不同组件、页面或子应用)的 reducer 合并为一个总的 reducer

该方法接收一个对象作为参数,不同的属性名用于标识不同作用的 reducer,以及状态更新后从 store 中取回状态值,属性值为声明的 reducer 函数;

const calcReducer = (state=1, action) => {
    switch (action.type) {
        case 'add':
            return state + 1;
        case 'minus':
            return state - 1;
        default:
            return state;
    }
}
const countReducer = (state=0, action) => {
    if (action.type === 'add') return state + 1;
    else return state;
}
const rootReducer = Redux.combineReducers({
    calc: calcReducer,
    count: countReducer,
});
const store = Redux.createStore(rootReducer);


console.log(store.getState()); // { calc: 1, count: 0, }

store.dispatch({ type: 'add' });

console.log(store.getState()); // { calc: 2, count: 1 }

combineReducers() 参数对象中指定的属性名用于存储该 reducer 的所有状态值;

Enhancer

Redux.createStore() 方法还可以接收第三个参数 enhancer,用于自定义 store 的功能或强化其能力(例如魔改),比如改变 dispatch(), getState(), subscribe() 等方法的默认行为;

enhancer 参数为一个自定义函数,其接收 Redux.createStore 这个方法作为参数,并返回一个新的 createStore 方法;

下面是一个为 dispatch 添加功能的简单示例:

const myReducer = (state=1, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else {
        return state;
    }
}
// enhancer 接收一个参数,即 Redux.createStore 这个方法,
// 用于执行创建 store 的默认行为;
const myEnhancer = (createStore) => {
    // enhancer 需要返回一个函数,其参数与 Redux.createStore 的相同,
    // 可以理解为返回另一个新的 createStore 函数;
    return (reducer, initialState, enhancer) => {
        // 需要执行一次 Redux.createStore 的默认行为,并获取 store
        const store = createStore(reducer, initialState, enhancer);

        // 修改 store 中的默认 dispatch 方法
        store.dispatch = (action) => {
            // 新加的功能
            console.log('dispatched.');
            // 最后仍需执行一次 dispatch 的默认行为
            return store.dispatch(action);
        }
        
        // 修改默认的 getState 方法
        store.getState = () => {
            return store.getState() + 1;
        }

        // 返回的新 createStore 方法还需要返回一个对象,即整个 store 对象;
        return store; 
        }
    }
}

const store = Redux.createStore(myReducer, undefined, myEnhancer);

store.dispatch({ type: 'add' }); // "dispatched."

console.log(store.getState()); // 3

需要同时使用多个 enhancer 时,需要进行合成,可以使用 Redux.compose() 方法:

const enhancers = Redux.compose(enhancer1, enhancer2); // 可以传入多个参数作 enhancer
const store = Redux.createStore(myReducer, undefined, enhancers);

Middleware

大多数时候,我们只希望自定义 dispatcch 方法的逻辑,所以官方专门提供了一个叫 middleware 的特性,翻译过来就是中间件,即在触发 action 和调用 reducer 执行响应之间,给用户提供一个可操作空间,如用于日志记录,问题报告,或者处理异步操作等;

middleware 是一个自定义函数,其接收一个对象作为参数,该参数对象有两个方法,分别是 dispatchgetState,逻辑都与 store 对象中的两个同名方法相同;

middleware 函数还需要返回另一个函数作为包装(自定义)后的 dispatch 方法,由于逻辑层次较多,下面会通过代码说明;

Redux 中内置了一个叫做 applyMiddlewareenhancer 方法,用于添加 middleware,它可以接收多个参数以传入多个 middleware

具体实现通过举例说明:

const myReducer = (state, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else {
        return state;
    }
}

// 自定义的中间件函数
const myMiddleware1 = ({ dispatch, getState }) => {
    // 中间件需要返回一个函数,即新的 dispatch 逻辑,
    // 该函数又接收一个参数 next,用于执行下一个 middleware,
    // 当然如果有下一个中间件就执行,没有了就执行原始的 dispatch,
    // 其实这个参数 next 也就是原始的那个 store.dispatch 方法;
    return (next) => {
        // 该函数也需要返回一个函数,用于处理 action,
        // 接收一个 acction 作参数,即 store 触发的 action;
        return (action) => {
            // 自定义逻辑
            console.log('mid 1', getState());

            // 这个函数还需要返回一个函数,即用之前的 next 方法
            // 将 action 传递给下一个 middleware 继续处理;
            return next(action);
        }
    }
}

// 也可以使用简写方式
const myMiddleware2 = ({ getState }) => next => action => {
    console.log('mid 2', getState());
    const result = next(action);
    console.log('mid 2 new', getState());

    return result;
}

// 使用中间件
const myEnhancer = Redux.applyMiddleware(myMiddleware1, myMiddleware2);

const store = Redux.createStore(myReducer, 1, myEnhancer);

store.dispatch({ type: 'add' });
// "mid 1" 1
// "mid 2" 1
// "mid 2 new" 2

console.log(store.getState());
// 2

总结一下整个执行过程就是:

  • 用户调用了 store.dispatch() 触发 action
  • Redux 按 applyMiddleware() 方法中参数的传入顺序,挨个执行自定义的 middleware 逻辑;
  • 然后再调用原始的 store.dispatch() 方法触发 action
  • 最终执行 reducer 中的逻辑;

整个过程有些类似函数的链式调用:

dispatch -> middleware1 -> middleware2 ... -> dispatch -> reducer

此外,由于 middleware 的执行逻辑,其特性还包括对 action 中数据的修改、中断甚至彻底停止 action,的触发,例如上例中最后不返回 next(action),那么整个过程执行完第一个 middleware 就结束了,state 也不会发生预期的改变;

处理异步逻辑

Redux 内部并不知道如何处理异步逻辑,只会同步的触发 action,然后调用 reducer 更新 state,所以任何异步逻辑需要我们在外部自己实现;而 Redux 的宗旨是 recuder 不要有任何副作用,最好是一个纯函数,即不要有多余的外部联系,如控制台打印,异步请求等;

middleware 就是 Redux 专为副作用逻辑需求而设计的,这里以异步操作为例用代码进行简单实现:

const reducer = (state, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else if (action.type === 'asyncAdd') {
        return action.data;
    } else {
        return state;
    }
}
const asyncMiddleware = _store => next => action => {
    if (action.type === 'asyncAdd') {
        setTimeout(() => {
            action.data = 'some data.';
            next(action);
        }, 2000);
    } else {
        next(action);
    }
}
const enhancer = Redux.applyMiddleware(asyncMiddleware);
const store = Redux.createStore(reducer, 0, enhancer);


store.dispatch({ type: 'add' });
console.log(store.getState()); // 1

store.dispatch({ type: 'asyncAdd' });
console.log(store.getState()); // 1

setTimeout(() => {
    console.log(store.getState());
}, 2000);
// 2 秒后输出:
// some data.

结果显示异步操作获取的数据,可以成功被 reducer 拿到并实现相应的逻辑,所以把 setTimeout 换成 Ajax 请求也同样可以从服务器获取到数据,然后传递给 Redux 进行下一步处理;

由于上面的异步逻辑的原生写法不太方便,Redux 官方就提供了一款 redux-thunk 工具,封装好了一个 middleware,应用之后就可以将 action 声明为一个函数(以前是一个对象),其接收 dispatchgetState 两个参数;具体用法如下:

import Redux from 'redux';
import ReduxThunk from 'redux-thunk';

const reducer = (state, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else if (action.type === 'asyncAdd') {
        return action.data;
    } else {
        return state;
    }
}

// 直接应用该工具
const middlewareEnhancer = Redux.applyMiddleware(ReduxThunk.default);

const store = Redux.createStore(reducer, 1, middlewareEnhancer);

// 这里 action 声明为函数,处理异步逻辑
const asyncAction = (dispatch, getState) => {
    console.log('old state:', getState());
    setTimeout(() => {
        dispatch({ type: 'asyncAdd', data: 'some data.' });
    }, 2000);
}

store.dispatch(asyncAction);
// "old state:" 1

setTimeout(() => {
    console.log(store.getState());
}, 2000);
// 2 秒后输出:
// "some data."

需要注意的是,一些教程上(包括 Redux 官网)介绍 Redux Thunk 的用法时,仍然使用的 Redux.applyMiddleware(ReduxThunk) 写法,这是该工具 1.x 版本的写法,现在 2.x 版本需要加上 .default,即 Redux.applyMiddleware(ReduxThunk.default),不然程序会出现问题;


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK