理解 Redux

272 阅读19分钟
原文链接: github.com

MV* 模式

在前端开发领域,MV* 模式是被广泛使用的。比如 MVVM 就是基于 MVC 演变出来的,它提供了 View-Model 模块,自动地将 Model 模块中的数据与 View 模块中视图进行绑定,省却了很多手动更新视图等的操作。

但是 MV* 模式也存在很多问题,MVC 模式本身,由于它的实现太过自由,导致很多人在使用的时候,稍微不注意就会出现一些误用。这些误用可能在初期看不出问题,但是到了后期维护会难以拓展并且导致一些问题。比如一个常见的误用就是在 Controller 中同时实现了对视图的更新与数据层 Model 的数据同步两个操作,这样不仅违背了单一职责原则,还违背了数据流单一方向的原则。模块职责不单一,后期在拓展性与维护性就比较差。如果数据流不是单一流向就比较难做事件回溯,而且修改数据的“入口” 比较多,导致数据层比较混乱,而且在面对复杂业务的时候,有时候我们不仅需要关心数据本身,而且需要关心这个数据是从前一个状态经历了怎么样的变化变成了现在这样的状态。

Flux 与 Redux

面对上面的问题,Facebook 提出一个新的应用架构——Flux。在我看来,Flux 是一个更规范化的 MVC 架构,它规范了如何去修改数据层的数据,然后数据层的数据发生变化之后,统一通过事件分发来更新视图层,这样就消除了上面所说的数据流的单一流向问题,在解决这个问题后,引入 Event Souring 即事件回溯,记录数据变化的事件而不仅是数据本身,来达到更好地复现应用的问题的目的。而 Redux 是基于 Flux 架构又做了一点优化的架构实现。至于他们的具体区别这里不累述,推荐一篇文章 zhuanlan.zhihu.com/p/20263396,有兴趣的读者可以阅读一下。

Redux 是一个纯粹的状态管理库,它与 react 没有关系,可以与任何框架或者库进行配合使用。当你的组件间数据传递的方向与方式变得复杂的时候,redux 或许就是你的解决方案。

再造 Redux

"What I Cannot Create, I Do Not Understand"。

情景再现

假设我们现在有一个 ToDoList 的应用:

const doc = document;
const listEl = doc.getElementById('todolist');
const submit = doc.getElementById('submit');
const input = doc.getElementsByTagName('input')[0];

const state = {
	list: []
};

function renderList (list = []) {
	let content = '';
	for (let item of list) {
		content += `
		<li class="todolist-item">
			${item.text}
		</li>`;
	}
	listEl.innerHTML = content;
}

function addItem () {
	if (input.value) {
		state.list.push({ text: input.value });
		renderList(state.list);
	}
}

submit.addEventListener('click', () => { addItem(); });

我们会看到代码中会存在很多问题,比如 addItem 函数不应该掺杂了像 renderList 这样的代码,这样代码的职责不单一,代码耦合度比较高。按照逻辑,addItem 只需要关心修改的数据,而不需要关心这个数据修改之后 UI 层面会做怎么样的变化。

一开始可能只有 renderList 这一个函数,但是随着应用的复杂度上升,数据修改之后需要重新渲染的地方可能会变得更多,所以需要将数据的变化与 UI 层变化做一个自动的关联,因此观察者模式是一个很自然而然的引入:

// 代码只做简单的示意,不做值类型校验等
const state = {
    list: []
};

const subscribes = [];
function subscribe (listener) {
    subscribes.push(listener);
    return function () {
        let index = subscribes.indexOf(listener);
        subscribes.splice(index, 1);
    }
}

function changeState (nextState) {
    state = nextState;
    for (let listener of subscribes) {
        listener();
    }
}

从上面的代码可以看到,当整个 state 的值改变的时候,就会触发监听者回调函数。有了这个机制之后,原来的 addItem 函数就可以变成:

function addItem () {
    if (input.value) {
    	/**
    	* 有些同学可能会对这里为什么要执行一次 slice 对数组进行复制有疑问
    	* 作为应用的前后两个状态,前后状态值各为独立的值更有利于我们在开发中
    	* 打断点看到状态间哪些值被修改了,而且也有利于开发周边更好用的调试工具
    	* 但是这里抛出一个问题带大家思考,最好的情况就是开发人员保持了两个状态
    	* 的独立,但总会有人有意无意地直接修改原状态的值,这不符合初衷的行为是怎么在
    	* 代码层面去解决的?
    	*/
    	let list = state.list.slice();
    	list.push({ text: input.value });
    	changeState({
            list,
            ...state
    	});
	}
}

看起来与原来变化不大甚至更复杂了,但是拓展性好了很多,比如 state 中的 list 属性不仅被 renderList 需要,而且被 renderListFirstItem 需要, 那么这个时候只需要:

subscribe (() => {
    if (state.list) {
        renderList(state.list);
        renderListFirstItem(state.list);
    }
});

此时可以调用多个渲染函数,addItem 函数对此无感知。但 addItem 函数现在看起来还是很累赘,还能再度简化吗?答案当然是可以的。

既然上面说到两状态值之间需要保持独立,那么是不是让 addItem 函数的返回值变成状态值,然后将这个返回值覆盖原 state 就可以了?代码如下:

function addItem () {
	if (input.value) {
     	let list = state.list.slice();
    	list.push({ text: value });
    	return { ...state, list };
	}
}

经过修改之后,addItem 函数与 changeState 函数解耦了,然后我们只需在调用方式上做一些修改:

// 修改前
submit.addEventListener('click', () => { addItem() });
// 修改后
submit.addEventListener('click', () => { changeState(addItem()) });

接着我们需要将 addItem 函数变得更通用,其实它就是对 state 中的 list 属性数据进行操作,那么其实可以改为:

function handleList(params) {
    let { type, payload } = params;
    swtich (type) {
        case 'ADD_ITEM':
        	let list = state.list.slice();
        	list.push(payload);
        	return {
                ...state,
                list
        	};
        case 'UPDATE_LIST':
        	....
    }
}

函数调用就会变为:

submit.addEventListener('click', () => { 
	changeState(handleList({
        type: 'ADD_ITEM',
        data: {
            text: input.value
        }
	}));
});

到这一步,状态管理已经初具雏形了,但还是存在代码通用性以及 state 仍是一个全局变量,容易被其他代码进行修改等问题,我们将上面代码封装成一个库。

createStore

像刚刚所说,我们需要一个用于管理 state 的对象,那么就取名为 store。我们来实现创建 store 的函数:

function createStore(initState) {
	// 将 state 放在函数内形成闭包
    let currentState = initState || void 0;
    
    return {
        getState: () => currentState, // 暴露一个查询 state 的 API
    };
}

const store = createStore();

由于现在 state 是一个局部变量,如果函数需要修改 state,那么就需要将这个函数传入到 createStore 里面,那么函数就变成了:

/**
* handler 函数就相当于上面的 handleList 函数的高级版,这个函数不再局限于修改 state 的某个单一属性
* initState 参数是可选参数,根据 API 设计哲学,可选参数放在后面
*/
function createStore(handler, initState) { // initState 为可选
	let currentState = initState;
	
	return {
        getState: () => currentState
	}
}

/**
* 这里将 handler 的参数改变了,直接将 state 传入到函数中
* 注:这里有一个个人思考点在附录中
*/
const store = createStore(function (state, params) {
    let { type, payload } = params;
    switch (type) {
        case 'ADD_ITEM':
        	let list = state.list.slice();
        	list.push(payload);
        	return {
                ...state,
                list
        	}
        ....
    }
});

根据前面的章节,我们触发 state 某些属性值的修改是通过有个固定属性 type 来说明操作类型加上 payload 对象装载其他参数的对象的形式的:

{
    type: 'ADD_ITEM',
    payload: {}
}

而这种形式我们称之为 Command 即命令,上面这个对象我们称之为 Action。根据前面的代码,对于原本接收这个对象的函数----changeState 函数,我们称之为 dispatch 函数(分发器)。(注:概念来自 CQRS 架构)

那么我们将一开始所说的事件监听者模式与 dispatch(changeState) 函数封装到 createStore 里面:

function createStore(handler, initState) {
	let currentState = initState;
	
	const subscribes = [];
    function subscribe (listener) {
        subscribes.push(listener);
        return function () {
            let index = subscribes.indexOf(listener);
            subscribes.splice(index, 1);
        }
    }
	
	function dispatch (action) {
        currentState = handler(currentState, action);
        for (let listener of subscribes) {
        	listener();
    	}
	}
	
	return {
        getState: () => currentState,
        dispatch,
        subscribe
	}
}

至此,我们已经实现了一个极简版本的 redux。

....
const store = createStore(function (state, action) {
 	   let { type, payload } = action;
 	   swtich (type) {
           case 'ADD_ITEM':
           	  let list = state.list;
           	  list.push(payload);
           	  return {
                ...state,
                list
           	  };
 	   }
}, {
    list: []
});

store.subscribe(() => renderList(store.getState()));

submit.addEventListener('click', () => { 
	store.dispatch({ type: 'ADD_ITEM', payload: { text: input.value } });
});

通过这样,拓展性与数据的单向流动性都得到了实现。像前面章节说的,拓展性是通过事件监听者模式来实现的,现在当数据发生变化时,触发其他模块函数的执行只需要通过 subscribe 来达到,而修改数据的函数本身对此无感知。数据单向流动性,是通过在所有需要触发修改数据的地方传入一个说明操作类型与 payload 的命令对象,通过 dispatch 这个统一的函数将操作进行分发,所有修改数据的操作只能通过 dispatch 函数来触发,完成了数据修改动作之后,dispatch 会通过事件监听触发对应的回调,这也完成了数据从 disaptch 出发,到 state,再到视图层或其他地方的单向流动性。

state

管理 State 对象

接下来我们继续将上面的代码进行优化。

当应用变得庞大,state 对象也会相应地变得很大。而对应 state 对象的操作也会随之增多,换言之,就是调用 createStore 函数时传入的参数处理 (switch) 分支会变得很大,这样不利于代码维护。所以我们会提出将这些处理分支分开到一个个函数中,最后在传入 createStore 的时候再统一合并,这样代码的维护性就提高了。

我们来实现一个 combine 函数:

function combine (handlers) {
	// 接受一个存放各种处理函数的对象
    let handlerKeys = Object.keys(handlers);
    let finalHandlers = {};
    
    // 检测 handler 是否是函数类型,并 map 进局部变量的对象中
    for (let key of handlerKeys) {
        let handler = handlers[key];
        if (typeof handler === 'function') {
            finalHandlers[key] = handler;
        }
    }
    
    let finalHandlerKeys = Object.keys(finalHandlers);
    
    // 为什么要返回一个函数的形式?
    // 因为 createStore 函数接受第一个参数就是 (state, action) => {} 这样形式的函数
    return function (state, action) {
        let nextState = {};
        for (let key of finalHandlerKeys) {
        	// 按照传入时的 key 来对 state 作划分
        	// 对应的 key 处理函数处理 state 对应 key 的数据
            let handler = finalHandlers[key];
            let prevStateForKey = state[key];
            // 将处理函数的返回结果作为新 state 的对应 key 的结果
            let nextStateForKey = handler(prevStateForKey, action);
            nextState[key] = nextStateForKey;
        }
        return nextState;
    }
}

这样就可以将庞大的 state 对象通过多个方法进行整合,分而治之。举个例子:

combine({
    list: listHandler,
    count: countHandler
});

像上面的代码,我们传入了两个对 state 中的数据作处理的函数。那么 state 的结构的类似:

{
    list: Any, // 对象或其他任意类型
    count: Any // 对象或其他类型
}

至于 list 属性或者是 count 属性内部的数据结构是怎么样的,就由 listHandler 或 countHandler 来决定。我们回头看一下 combine 函数最后返回的函数的实现,实际上可以改写为这样:

return function (state, action) {
    return finalHandlerKeys.reduce((nextState, key) => {
        let handler = finalHandlers[key];
        let pervStateForKey = state[key];
        let nextStateForKey = handler(pervStateForKey, action);
        nextState[key] = nextStateForKey;
        return nextState;
    }, {});
}

我们都知道在 javascript 中 reduce 函数的用法,它相当于把一个给定的初始值(这里相当于第二个参数传入的空对象),通过一系列的函数执行,将初始值进行一个"折叠",得到一个最终的结果。而我们这里的 handler 处理函数就相当于在 reduce 函数中的一个个逻辑处理部分,称为 "reducer"。其实这个命名的来源,在 Redux 的中文文档中也有提及。所以此后我们将 handler 函数称为 "reducer"。

使用中间件增强功能

在日常开发中,我们总是会遇到像打印一个变量在变化前的值与在变化后的值是怎么样的,还有对于全局的错误进行处理的需求的时候。像打印日志,如果使用前面代码,如下:

console.log('before change', store.getState());
store.dispatch({ type: 'UPDATE_VAL', val: 1 });
console.log('after change', store.getState());

但是这样的代码是属于手动添加的,这种通用功能,是否可以通过开放一个接口,将其集成到状态处理库中,而对于编写处理函数 reducer 的人员来说,他无需编写额外的代码就可以拥有这种通用的能力呢?

首先,我们从最基本的代码开始,要实现上面的功能,那么我们可以抽象成一个函数,然后重写 dispatch 函数:

const dispatch = store.dispatch;

store.dispatch = (action) => {
  console.log('before change', store.getState());
  dispatch(action);
  console.log('after change', store.getState());
};

上面的代码通过全局劫持重写了 store 的 dispatch 函数,实现了当触发 dispatch 函数的之前和之后都会打印该时刻的 state 的值,这样其他开发人员在使用 dispatch 的时候都会拥有这项能力。但是这里实现还是存在问题,它在函数内部依赖了一些外部全局变量,所以为了通用与调用方便,我们将其参数化:

function logger({ getState, dispatch }) { // 使用对象解构进行传参不用理会参数顺序而且可读性也很好
    return function(action) {
        console.log('before change', getState());
        diapatch(action);
        console.log('after change', getState());
    }
}

store.dispatch = logger({
	getState: store.getState,
	dispatch: store.dispatch
});

按照上面的模式,我们应该能很快地写出处理全局错误的函数:

function errorHandle({ getState, dispatch }) {
    return function(action) {
        try {
            dispatch(action);
        } catch (err) {
            console.log('global error:', err);
        }
    }
}

store.dispatch = errorHandle({
    getState: store.getState,
    dispatch: store.dispatch
});

但是问题来了,我们怎么将这两个函数进行整合?强行结合也是可以的:

store.dispatch = errorHandler({
    getState: store.getState,
    dispatch: logger({
        getState: store.getState,
        dispatch: store.dispatch
    })
})

问题是这样看起来非常别扭,而且当类似的全局需求多了,嵌套会越来越深,我们能否编写一个函数将这些函数能够统一串联在一起?

我们回过头来审视刚才实现的函数,其实 errorHandler 函数执行的并不是真正的 dispatch,而是下一个函数(logger)返回的一个函数,只不过这个函数内部(可能)执行了 dispatch 函数。也即是说 errorHandler 可以改写为下面这种形式:

function errorHandler({ getState, dispatch }) {
    return function(next) {
        return fucntion(action) {
            try {
                next(action);
            } catch(err) {
                console.log('global error:', err);
            }
        }
    }
}

store.dispatch = errorHandler({
    getState: store.getState,
    dispatch: store.disaptch // 保留,万一内部调用需要也可以取到
})(logger({
    getState: store.getState,
    dispatch: store.disaptch
}));

可以看到,我们在原来的模式上面添加了一层 next 参数的柯里化,这样的话,能够更加灵活,这里的 next 函数你可以理解为是由另外一个函数经过增强之后的 dispatch 函数。

看上面的例子,可以看到有一些重复的代码,比如 getState 和 dispatch 的传值,我们将全局 logger 也改写为像上面全局错误处理的函数一样:

function logger({ getState, dispatch }) {
    return function(next) {
        return function(action) {
            console.log('before change', getState());
            next(action);
            console.log('after change', getState());
        }
    }
}

假设这两个函数放在一个数组里,那么 getState 和 dispatch 的传值就可以:

let chain = [errorHandler, logger].map(func => func({ getState, dispatch }));

然后对于第二层 next 参数的固定,其实就是决定各个函数(errorHandler 和 logger)的执行顺序,我们使用一个函数来达到决定各个函数的执行顺序的目的。

compose

这里的 compose 可能和 Koa 框架的 compose 有些许不同,它的原理就是:

function compose(...arr) {
	if (arr.length == 0) return args => args;
	if (arr.length == 1) return arr[0];
	let last = arr[arr.length - 1];
	let rest = arr.slice(0, -1);
    return (...args) => rest.reduceRight((compose, func) => func(compose), last(...args));
}

compose 同样适用了柯里化,固定了函数数组,返回了一个函数,所以使用到我们的代码中就是:

store.dispatch = compose(errorHandler, logger)(store.dispatch);

结合上面我们所说的 logger,errorHandler 函数来看,reduceRight 在执行的时候,其实是在固定 logger,errorHandler 函数的 next 参数,按照顺序,其实 logger 函数的 next 函数是 store.dispatch 函数,而 errorHandler 函数的 next 参数是 logger 函数经过 next 参数固定之后得到的函数。

最终得到的函数依然是形参为 "action, payload" 的函数,符合 store.dispatch 原来的形式。这样就可以增强原来 dispatch 的功能了。

总结

将上面的代码进行总结,我们会得到下面这个函数:

function applyMiddleware(...middlewares) {
    return function(createStore) {
    	return (...args) => {
            const store = createStore(...args);
            
            const getState = store.getState;
            let dispatch = store.dispatch;
            const middleContext = { getState, dispatch: (action) => dispatch(action) }; // 这里可以看到其实传入的 diaptch 并不是 store.dispatch, 而还是经过 compose 编排的 dispatch
            
            middlewares = middlewares.map(m => m(middleContext)); // 固定 getState, diaptch
            dispatch = compose(middlewares)(dispatch); // 使用 compose 进行编排中间件
            
            return {
                ...store,
                dispatch
            };
    	}
    }
}

有些同学可能会对这里为什么是传入一个 createStore 函数而不是一个 store 对象有疑问,这样是为了保持灵活性的。你可以将 applyMiddleware 函数看作是另外一种中间件,只不过它不是针对 dispatch 的,而是针对 createStore 函数的,通过上面这种函数形式,它可以增强 createStore 函数:

createStore = applyMiddleware(errorHandler, logger)(createStore);
const store = createStore(reducer, null);

如果有其他想要增强 createStore 函数的中间件,也可以写成这样:

function enhancer() {
 	return function(createStore) {
        return function(...args) {
            // do something before createStore
            return createStore;
        }
	}   
}

这样代码就可以变成:

const store = createStore(reducer, null,
	compose(
		applyMiddleware(
			errorHandler,
			logger
		),
		enhancer()
	)
);

这样 createStore 函数也要做出相对应的改变:

function createStore(reducer, initState, enhancer) {
	....
    if (enhancer !== void 0 && typeof enhancer === 'function') {
        return enhancer(createStore)(reducer, initState);
    }
    ....
}

根据上面的代码,其实 createStore 内部的 enhancer 函数是由 compose 返回的,由前面的部分我们知道,enhancer 类型的函数第二层需要固定的参数是 createStore 函数,然后返回一个形参和 createStore 函数一样,返回值也是 store 的函数,也即是所谓增强后的 createStore 函数。

换言之,compose 的时候,applyMiddleware 函数得到的 createStore 函数其实是 enhancer 函数在传入 createStore 函数后执行得到的那个函数,其实 applyMiddleware 在这里得到的 createStore 是一个增强后的 createStore 函数,这一点和中间件中的 compose 是类似的。

至此,我们基本实现了 Redux 的几个 API:createStore, combineReducer, compose, applyMiddleware,与 store 本身的几个 API:dispatch,getState,subscribe。完整代码如下:

function combineReducer(reducers) {
    let keys = Object.keys(reducers);
    let finalReducers = {};
    for (let key of keys) {
        let reducer = reducers[key];
        if (typeof reducer === 'function') {
            finalReducers[key] = reducer;
        }
    }
    let finalKeys = Object.keys(finalReducers);
    return (state, action) => {
        return finalKeys.reduce((nextState, key) => {
            let recuer = finalReducers[key];
            let stateForKey = state[key];
            let nextStateForKey = reducer(stateForKey, action);
            nextState[key] = nextStateForKey;
            return nextState;
        }, {});
    }
}

function createStore(reducer, initState, enhancer) {
    if (enhancer !== void 0 && typeof enhancer === 'function') {
        return enhancer(createStore)(reducer, initState);
    }
    const state = initState || {};
    const subcrribes = [];
    function subcribe(listener) {
    	subscribe.push(listener);
        return function() {
            let idx = subscirbes.indexOf(listener);
            if (idx > -1) subscirbes.splice(idx, 1);
        }
    }
    function dispatch (action) {
        state = reducer(state, action);
        subscirbe.forEach(func => func());
    }
    return {
      getState: () => state,
      dispatch,
      subscribe
    };
}

fucntion compose(...funcs) {
    if (funcs.length === 0) reutrn args => args;
    if (funcs.length === 1) return funcs[0];
    let last = funcs[funcs.length - 1];
    let rest = funcs.slice(0, -1);
    return (...args) => rest.reduceRight((compose, func) => func(compose), last(...args));
}

function applyMiddleware(...middlewares) {
    return function(createStore) {
        return (...args) {
            const store = createStore(...args);
            
            let getState = store.getState;
            let dispatch = store.disaptch;
            middlewares = middlewares.map(func => func({ getState, dispatch: (action) => dispatch(action) }));
            dispatch = compose(middlewares)(dispatch);
            return {
              ...store,
              dispatch
            };
        }
    }
}

React-Redux

就像前面所讲,redux 是一个状态管理工具,能与任何框架配合使用,但是由于前端的 DSL 方案很多,想要做到最小成本地使用 Redux 就需要一些工具来帮助接入。

比如在 React 中,如果你想和 react 配合使用,那么就需要在多个组件中都能取到这个全局的 store 管理对象。可能我们会考虑在组件的 props 属性里面传进去,但是一旦组件的层级比较多,这个方案就比较麻烦了,而且有可能会遇到这个组件根本不需要使用这个 store 对象,但是还是需要从 props 传进去,因为它的子组件需要使用。

另一方面,当我们通过 dispatch 修改了 state 数据之后,store 的 event emitter 需要触发组件渲染(setState),这些工作,我们需要有一个工具来帮我们完成——React-Redux。

首先 React-Redux 提供了一个 Provider 组件:

class Provider extends React.Component {
    getChildContext() {
        return {
            store: this.props.store
        };
    }
    render() {
        return this.props.children;
    }
}

Provider.childrenContextTypes = {
    store: PropTypes.object
};

可以看到 Provider 其实是一个高阶组件,它使用了 React 的 context api 来达到多层级传参的目的。通过包裹一层,即使这个 API 不稳定,那么后期只需要更换实现,上层逻辑代码是不需要变化的。通过指定 childContext,可以让后代(不仅是直接子级)可以通过 context 属性就可以取到 store。

此外,React-Redux 提供 connect 函数,减少很多使用 Redux 的重复代码,能够帮助组件自动实现当 store 变化触发渲染(setState)等操作。

const connect = (
	mapStateToProps => () => ({}),
	mapDispatchToProps => () => {{}}
) => Component => { // 接受一个 React 组件
    class Connected extends React.Component { // 返回一个容器型组件,包裹刚传入的组件
    	onStoreOrPropsChange(props) { // 处理 Store 或者 Props 数据发生变化的具体逻辑
            const { store } = this.context;
            const state = store.getState(); // 获取最新的 state 值
            const stateProps = mapStateToProps(state, props);
            const dispatchProps = mapDispatchToProps(store.disaptch, props);
            // 触发组件更新
            this.setState({
                ...stateProps,
                ...dispatchProps
            });
    	}
    	
    	componentWillMount() {
            const { store } = this.context; // 由 context 获取 Provider 组件提供的 store
            this.unsubscribe = store.subscribe(() => this.onStoreOrPropsChange(this.props)); // 当 store 被触发 dispatch 就执行上面的函数
    	}
    	
    	componentWillReceiveProps(nextProps) {
            this.onStoreOrPropsChange(nextProps); // props 数据发生变化时执行
    	}
    	
    	componentWillUnmount() {
         	this.unsubscribe(); // 组件卸载时取消监听 Redux
    	}
    	
        render() {
            return <Component {...this.props} {...this.state} />
        }
    }
};

上面的 mapStateToProps 函数是用来解决对于某 container 类的组件面对庞大的 store 树的时候,通过 mapStateToProps 的返回结果来减少 state 对象的层级的。

通过 mapDispatchToProps 我们可以在函数中取到 store 的 dispatch 函数,并将封装了 dispatch 的函数在 props 参数中传给组件,使得组件能够触发 dispatch 函数。

附录

State 为什么是全状态替换?而不是针对字段作属性依赖?

redux 只是一个纯粹的状态管理库,它好就好在够纯粹,只关注于状态的前后变化,而无需关心状态值内部值变化的细节,也无需关心变化之后到底是怎么 diff 出到底哪里变化了做对应变化。

但也由于太纯粹,所以导致出现各种 xxx-redux, 但是个人觉得这个不能怪 redux,只能说前端领域 View 层 DSL 方案确实比较多。

Reducer 一定要写成纯函数吗?

我们在上面设计的时候,可以看到 reducer 函数中要用到的 state 对象其实是通过参数传入的,但是为什么不能这样设计呢:

function (params) {
	let { type, payload } = params;
    let state = store.getState(); // 这样动态去取 state 的值
    switch (type) {
        case 'ADD_ITEM':
        	.....
    }
}

如果在函数中插入 store.getState 函数,可能因为调用次序等问题,并不能保证每次获取到的 state 都是相同的,这样就没有办法保证相同的输入会有相同的输出,这也就违背了 Redux 中设计思想。让 reducer 接收旧的 state 和 action,返回一个新的 state,只有让 reducer 保持纯函数使得这个行为可预测,是它的设计原则。

其实在实际运用中,你是可以在 reducer 中修改 state 的值的,因为 state 一般来说是一个对象:

const { createStore } = require('redux');

const reducer = function (state, action) {
    console.log('reducer', state);
    const { type } = action;
    switch (type) {
        case 'add':
            state.test = 1; // [ATTENACTION]! 这里直接修改了 state 中其他部分的值甚至添加了一些数据
            Object.assign(state, { data: 2 });
            return state;
    }
    return state;
}

const store = createStore(reducer, {});

store.subscribe(() => {
    console.log('subscribe', store.getState());
});

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

上面这些代码就非常邪恶,在编写中要杜绝这样的代码,如果存在这样的代码,reducer 就存在了副作用,此时对于开发维护就非常不友好了。

如果你想要使用修改的方式而不是 Object.assign 的方式来修改,那你可以借助 immer 这种库创建不可变对象来辅助 Redux 的使用。