Open yinguangyao opened 4 years ago
呜呜呜
dispatch 加锁不是因为多个 action 同时发送吧,单线程哪来的同时发送,还不是按顺序执行吗 搜了一下是为了避免在 reducer 里调用 dispatch https://github.com/reduxjs/redux/issues/1668#issuecomment-214672679
大佬写的不错,文中有些图片我这里打不开,不知道是链接给错了,还是我本地网络问题,大佬检查下?
麻烦贴一下文中图片,链接已经失效
@Vinson-Ben @samwangdd 可以看这篇吧:https://zhuanlan.zhihu.com/p/114791371
1. 前言
在前端圈子有这样一种说法,Vue 入门最简单,React 学习曲线太陡,Angular...我还是选择狗带吧。 在 React 诞生之初,Facebook 宣传这是一个用于前端开发的界面库,仅仅是一个 View 层。前面我们也介绍过 React 的组件通信,在大型应用中,处理好 React 组件通信和状态管理就显得非常重要。 为了解决这一问题,Facebook 最先提出了单向数据流的 Flux 架构,弥补了使用 React 开发大型网站的不足。
Flux:
![image_1dvghf5eo1b121baocp1dbek119.png-10.5kB][1]
随后,Dan Abramov 受到 Flux 和函数式编程语言 Elm 启发,开发了 Redux 这个状态管理库。 Redux 源码非常精简,实现也很巧妙,这篇文章将带你从零手写一个 Redux 和 react-redux 库,以及告诉你该如何设计 Redux 中的 store。 在开始前,我已经将这篇文章的完整代码都整理到 GitHub 上,大家可以参考一下。 Redux:[simple-redux][2] React-redux:[simple-react-redux][3]
2. 状态管理
2.1 理解数据驱动
在开始讲解状态管理前,我们先来了解一下现代前端框架都做了些什么。 以 Vue 为例子,在刚开始的时候,Vue 官网首页写的卖点是数据驱动、组件化、MVVM 等等(现在首页已经改版了)。 那么数据驱动的意思是什么呢?不管是原生 JS 还是 jQuery,他们都是通过直接修改 DOM 的形式来实现页面刷新的。 而 Vue/React 之类的框架不是粗暴地直接修改 DOM,而是通过修改 data/state 中的数据,实现了组件的重新渲染。也就是说,他们封装了从数据变化到组件渲染这一个过程。
![image_1e3c2ig3oj2m1d9d10m31ugf1tmo9.png-32.2kB][4]
原本我们用 jQuery 开发应用,除了要实现业务逻辑,还要操作 DOM 来手动实现页面的更新。尤其是涉及到渲染列表的时候,更新起来非常麻烦。
所以后来出现了 jQuery.tpl 和 Underscore.template 之类的模板,这些让操作 DOM 变得容易起来,有了数据驱动和组件化的雏形,可惜我们还是要手动去渲染一遍。
如果说用纯原生 JS 或者 jQuery 开发页面是原始农耕时代,那么 React/Vue 等现代化框架则是自动化的时代。 有了前端框架之后,我们不需要再去关注怎么生成和修改 DOM,只需要关心页面上的这些数据以及流动。 所以如何管理好这些数据流动就成了重中之重,这也是我们常说的“状态管理”。
2.2 什么状态需要管理?
前面讲了很多例子,可状态管理到底要管理什么呢?在我看来,状态管理的一般就是这两种数据。
2.3 全局状态管理
我们用 React 写组件的时候,如果需要涉及到兄弟组件通信,经常需要将状态提升到两者父组件里面。一旦这种组件通信多了起来,数据管理就是个问题。 结合上面的例子,如果想要对应用的数据流进行管理,那是不是可以将所有的状态放到顶层组件中呢? 将数据按照功能或者组件来划分,将多个组件共享的数据单独放置,这样就形成了一个大的树形 store。这里更建议按照功能来划分。
![image_1e3c34ubd19i0edh191pdit1tp3m.png-34.9kB][5]
这个大的 store 可以放到顶层组件中维护,也可以放到顶层组件之外来维护,这个顶层组件我们一般称之为“容器组件”。 容器组件可以将组件依赖的数据以及修改数据的方法一层层传给子组件。 我们可以将容器组件的 state 按照组件来划分,现在这个 state 就是整个应用的 store。将修改 state 的方法放到 actions 里面,按照和 state 一样的结构来组织,最后将其传入各自对应的子组件中。
我们可以看到,这种方式可以很完美地解决子组件之间的通信问题。只需要修改对应的 state 就行了,App 组件会在 state 变化后重新渲染,子组件接收新的 props 后也跟着渲染。
![image_1e3c3kn7n17tg1q54g6j4op74k13.png-70.2kB][6]
这种模式还可以继续做一些优化,比如结合 Context 来实现向深层子组件传递数据。
如果你已经接触过 Redux 这个状态管理库,你会惊奇地发现,如果我们把 App 组件中的 state 移到外面,这不就是 Redux 了吗? 没错,Redux 的核心原理也是这样,在组件外部维护一个 store,在 store 修改的时候会通知所有被 connect 包裹的组件进行更新。这个例子可以看做 Redux 的一个雏形。
3. 实现一个 Redux
根据前面的介绍我们已经知道了,Redux 是一个状态管理库,它并非绑定于 React 使用,你还可以将其和其他框架甚至原生 JS 一起使用,比如这篇文章:如何在非 React 项目中使用 Redux
Redux 工作原理:
![image_1e3bj67tdkstv46np1bgfcibm.png-40.8kB][7]
在学习 Redux 之前需要先理解其工作原理,一般来说流程是这样的:
从这个流程中不难看出,Redux 的核心就是一个 发布-订阅 模式。一旦 store 发生了变化就会通知所有的订阅者,view 接收到通知之后会进行重新渲染。
Redux 有三大原则:
单一数据源
前面的那个例子,最终将所有的状态放到了顶层组件的 state 中,这个 state 形成了一棵状态树。在 Redux 中,这个 state 则是 store,一个应用中一般只有一个 store。
State 是只读的
在 Redux 中,唯一改变 state 的方法是触发 action,action 描述了这次修改行为的相关信息。只允许通过 action 修改可以使应用中的每个状态修改都很清晰,便于后期的调试和回放。
通过纯函数来修改
为了描述 action 使状态如何修改,需要你编写 reducer 函数来修改状态。reducer 函数接收前一次的 state 和 action,返回新的 state。无论被调用多少次,只要传入相同的 state 和 action,那么就一定返回同样的结果。
关于 Redux 的用法,这里不做详细讲解,建议参考阮一峰老师的《Redux 入门》系列的教程:[Redux 入门教程][8]
3.1 实现 store
在 Redux 中,store 一般通过 createStore 来创建。
先看一下 Redux 中暴露出来的几个方法。
![image_1e3bjfhpfplfnea198bj7lf5e13.png-40.9kB][9]
其中 createStore 返回的方法主要有
subscribe
、dispatch
、replaceReducer
、getState
。createStore
接收三个参数,分别是 reducers 函数、初始值 initalStore、中间件 middleware。store
上挂载了getState
、dispatch
、subscribe
三个方法。getState
是获取到 store 的方法,可以通过store.getState()
获取到store
。dispatch
是发送 action 的方法,它接收一个 action 对象,通知store
去执行 reducer 函数。subscribe
则是一个监听方法,它可以监听到store
的变化,所以可以通过subscribe
将 Redux 和其他框架结合起来。replaceReducer
用来异步注入 reducer 的方法,可以传入新的 reducer 来代替当前的 reducer。3.2 实现 getState
store 的实现原理比较简单,就是根据传入的初始值来创建一个对象。利用闭包的特性来保留这个 store,允许通过 getState 来获取到 store。 之所以通过 getState 来获取 store 是为了获取到当前 store 的快照,这样便于打印日志以对比前后两次 store 变化,方便调试。
当然,现在这个 store 实现的比较简单,毕竟 createStore 还有两个参数没用到呢。 先别急,这俩参数后面会用到的。
3.3 实现 subscribe && unsubscribe
既然 Redux 本质上是一个 发布-订阅 模式,那么就一定会有一个监听方法,类似 jQuery 中的
$.on
,在 Redux 中提供了监听和解除监听的两个方法。 实现方式也比较简单,使用一个数组来保存所有监听的方法。3.4 实现 dispatch
dispatch 和 action 是息息相关的,只有通过 dispatch 才能发送 action。而发送 action 之后才会执行 subscribe 监听到的那些方法。 所以 dispatch 做的事情就是将 action 传给 reducer 函数,将执行后的结果设置为新的 store,然后执行 listeners 中的方法。
这样就行了吗?当然还不够。如果有多个 action 同时发送,这样很难说清楚最后的 store 到底是什么样的,所以需要加锁。在 Redux 中 dispatch 执行后的返回值也是当前的 action。
至此为止,Redux 工作流程的原理就已经实现了。但你可能还会有很多疑问,如果没有传 initialState,那么 store 的默认值是什么呢?如果传入了中间件,那么又是什么工作原理呢?
3.5 实现 combineReducers
在刚开始接触 Redux 的 store 的时候,我们都会有一种疑问,store 的结构究竟是怎么定的?combineReducers 会揭开这个谜底。 现在来分析 createStore 接收的第一个参数,这个参数有两种形式,一种直接是一个 reducer 函数,另一个是用 combineReducers 把多个 reducer 函数合并到一起。
![image_1e3c3pvnlknf7f5139n1fuqu7j1g.png-48.3kB][10]
可以猜测 combineReducers 是一个高阶函数,接收一个对象作为参数,返回了一个新的函数。这个新的函数应当和普通的 reducer 函数传参保持一致。
那么 combineReducers 做了什么工作呢?主要是下面几步:
细心的童鞋一定会发现,每次调用 dispatch 都会执行这个 combination 的话,那岂不是不管我发送什么类型的 action,所有的 reducer 函数都会被执行一遍? 如果 reducer 函数很多,那这个执行效率不会很低吗?但不执行貌似又无法完全匹配到
switch...case
中的action.type
。 如果能通过键值对的形式来匹配action.type
和 reducer 是不是效率更高一些?类似这样:这样每次发送新的 action 的时候,可以直接用
reducers
下面的 key 值来匹配了,无需进行暴力的遍历。 天啊,你实在太聪明了。小声告诉你,社区中一些类 Redux 的方案就是这样做的。以 rematch 和 relite 为例: rematch:const count = { state: 0, reducers: { increment: (state, payload) => state + payload, decrement: (state, payload) => state - payload }, effects: { async incrementAsync(payload) { await delay(); this.increment(payload); } } };
const store = init({ models: { count } });
dispatch.count.incrementAsync(1);
const increment = (state, payload) => { state.count = state.count + payload; return state; } const decrement = (state, payload) => { state.count = state.count - payload; return state; }
const enhancer = () => { return (createStore) => (reducer, initState, enhancer) => { ... } }
const logger = () => { return (createStore) => (reducer, initState, enhancer) => { const store = createStore(reducer, initState, enhancer); const dispatch = (action) => { console.log(
action=${JSON.stringify(action)}
); const result = store.dispatch(action); const state = store.getState(); console.log(state=${JSON.stringify(state)}
); return result; } return { ...state, dispatch } } }const createStore = (reducer, initialState, enhancer) => { if (enhancer && typeof enhancer === "function") { return enhancer(createStore)(reducer, initialState) } }
const store = createStore(reducers, initialStore, applyMiddleware(thunk, logger, reselect));
const applyMiddleware = (...middlewares) => { return (createStore) => (reducer, initState, enhancer) => { const store = createStore(reducer, initState, enhancer) const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } let chain = middlewares.map(middleware => middleware(middlewareAPI)) store.dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }
const compose = (...funcs) => { if (!funcs) { return args => args } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((f1, f2) => (...args) => f1(f2(...args))) }
function logger(middlewareAPI) { return function (next) { // next 即 dispatch return function (action) { console.log('dispatch 前:', middlewareAPI.getState()); var returnValue = next(action); console.log('dispatch 后:', middlewareAPI.getState(), '\n'); return returnValue; }; }; }
// 这里需要对参数为0或1的情况进行判断 const compose = (...funcs) => { if (!funcs) { return args => args } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((f1, f2) => (...args) => f1(f2(...args))) }
const bindActionCreator = (action, dispatch) => { return (...args) => dispatch(action(...args)) }
const createStore = (reducer, initState, enhancer) => { if (!enhancer && typeof initState === "function") { enhancer = initState initState = null } if (enhancer && typeof enhancer === "function") { return enhancer(createStore)(reducer, initState) } let store = initState, listeners = [], isDispatch = false; const getState = () => store const dispatch = (action) => { if (isDispatch) return action // dispatch必须一个个来 isDispatch = true store = reducer(store, action) isDispatch = false listeners.forEach(listener => listener()) return action } const subscribe = (listener) => { if (typeof listener === "function") { listeners.push(listener) } return () => unsubscribe(listener) } const unsubscribe = (listener) => { const index = listeners.indexOf(listener) listeners.splice(index, 1) } return { getState, dispatch, subscribe, unsubscribe } }
const applyMiddleware = (...middlewares) => { return (createStore) => (reducer, initState, enhancer) => { const store = createStore(reducer, initState, enhancer); const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } let chain = middlewares.map(middleware => middleware(middlewareAPI)) store.dispatch = compose(...chain)(store.dispatch) return { ...store } } }
const combineReducers = reducers => { const finalReducers = {}, nativeKeys = Object.keys nativeKeys(reducers).forEach(reducerKey => { if(typeof reducers[reducerKey] === "function") { finalReducers[reducerKey] = reducers[reducerKey] } }) return (state, action) => { const store = {} nativeKeys(finalReducers).forEach(key => { const reducer = finalReducers[key] const nextState = reducer(state[key], action) store[key] = nextState }) return store } }
// Provider ReactDOM.render({