📢 博客首发 : 阿宽的博客javascript
在本文开始以前,唠叨几句话吧,那就是本文有点长,且有部分源码等;前几天有幸和寒雁老哥聊了一小会,他说我如今已经懂怎么写文章阶段,建议下一个阶段能稳下来,而后去写一些有深度的东西,而不是浮在表面上;上周六去听了同公司已出书的挖坑的张师傅的技术写做分享。vue
因而我沉默了一下,听了一些前辈的建议,我决定,奥力给,作一个技术深度专区的文章~,尽可能每个月一更,不过每更一次,篇幅都会较长,尽量的分享一个较为完整的主题。因此打个预防针吧,但愿各位小伙伴,能静下心来看,你们一同进步~java
🔥 为何这个专栏叫【KT】,我这人比较 low,专栏中文叫: 阿宽技术深文,K 取自阿宽中的宽,T,Technology,技术,有逼格。能够,感受本身吹牛逼的技术又进了一步。react
因为时间关系,而且在组里引出了 react 中状态管理的论战,围绕着 hox、mobx、redux 进行一波交流,因此第四步的动手实践,我会晚点再更,接下来这段时间打算研究一下 hox
、mobx
的一个内部实现原理,而后动手实践写下 demo,在组里评审一波,取其精华去其糟粕,说不定又是一个新的产物?想一想就很激动有意思呢~git
别喷,造轮子只是为了学习~程序员
函数式编程
、洋葱模型
相关知识博主在 18 年末面试的时候,面试官看我简历,问: “我看你简历,vue 和 react 都用过,你能说一下Vue 和 React 的区别嘛?”,当时逼逼赖赖说了一下,也不知道说的对不对,而后在说到 vuex 和 redux 的时候,血案发生了,面试官问了一句,为何 Redux 老是要返回一个新的 state ?返回旧的 state 为何不行 ?面试结果不用说,毕竟当时我也不是这么了解嘛~github
当时面试完了以后,抽空把 redux 的源码看了一遍,ojbk,确实看的比较晕,记得当时看的时候,redux 还没引入 TS,前段时间,想深刻去了解一下 redux
,谁知,一发不可收拾,鬼知道我在看的过程说了多少句 WC,牛逼...面试
虽然这篇文章,是针对 redux 入门选手写的,但因为我这该死的仪式感,说个东西以前,仍是得简单介绍一下~vuex
Redux 是 JavaScript 状态容器,提供可预测化的状态管理方案, 官网里是这么介绍的 :编程
✋ Redux is a predictable state container for JavaScript apps.
咩呀?听不懂啊?稍等稍等,在作解释以前,请容许我问你个问题,react 是单向数据流仍是双向数据流?,若是你回答的是双向数据流,ok,拜拜 👋,出门左转,若是你回答的是单向数据流,嗯,咱们仍是好兄弟~
要理解 redux 是啥子,先看我画的一个图 👇
咱们知道哈,react 中,有 props 和 state,当咱们想从父组件给子组件传递数据的时候,可经过 props 进行数据传递,若是咱们想在组件内部自行管理状态,那能够选择使用 state。可是呢,咱们忽略了 react 的自身感觉~
react 它是单向数据流的形式,它不存在数据向上回溯的技能,你要么就是向下分发,要么就是本身内部管理。(咋地,挑战权威呢?你觉得能够如下犯上吗?)
小彭一听,“ 哎不对啊,不是能够经过回调进行修改父组件的 state 吗?” 是的,确实能够。先说说咱们为啥使用 redux,通常来说,咱们在项目中能用到 redux 的,几乎都算一个完整的应用吧。这时候呢,若是你想两个兄弟组件之间进行交流,互相八卦,交换数据,你咋整?
咱们模拟一个场景,Peng 组件和 Kuan 组件想共享互相交换一些数据,按照 react 单向数据流的方式,该怎么解决?
这个图应该都看得懂哈,也就是说,咱们兄弟组件想互相交流,交换对方的数据,那么惟一的解决方案就是:提高 state,将本来 Peng、Kuan 组件的 state 提高到共有的父组件中管理,而后由父组件向下传递数据。子组件进行处理,而后回调函数回传修改 state,这样的 state 必定程度上是响应式的。
这会存在什么问题?你会发现若是你想共享数据,你得把全部须要共享的 state 集中放到全部组件顶层,而后分发给全部组件。
为此,须要一个库,来做为更加牛逼、专业的顶层 state 发给各组件,因而,咱们引入了 redux,这就是 redux 的简单理解。
这就是咱们总能看到,为啥在根App组件都有这么个玩意了。
function App() {
return (
<Provider store={store}> ... </Provider>
);
}
复制代码
阿宽这里就默认你们都会使用 redux 了,不会使用的你就去啃啃文档,写个 demo 你就会了嘛,不过呢,仍是要说一说 redux 的三大原则的~
state
都存储在一颗 state tree 中,而且只存在于惟一一个 store 中action
,而后经过 action 的 type 进而分发 dispatch 。不能直接改变应用的状态纯函数
完成 : 为了描述 action 如何改变 state tree,须要编写 reducers
store
是由 Redux 提供的 createStore(reducers, preloadedState, enhancer)
方法生成。从函数签名看出,要想生成 store,必需要传入 reducers,同时也能够传入第二个可选参数初始化状态(preloadedState)。第三个参数通常为中间件 applyMiddleware(thunkMiddleware)
,看看代码,比较直观
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk' // 这里用到了redux-thunk
const store = createStore(
reducerList,
(initialState = {}),
applyMiddleware(thunkMiddleware)
)
复制代码
redux 中最核心的 API 就是: createStore
, 经过 createStore 方法建立的 store 是一个对象,它自己包含 4 个方法 :
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的惟一来源。简单来讲,Action 就是一种消息类型,他告诉 Redux 是时候该作什么了,并带着相应的数据传到 Redux 内部。
Action 就是一个简单的对象,其中必需要有一个 type 属性,用来标志动做类型(reducer 以此判断要执行的逻辑),其余属性用户能够自定义。如:
const KUAN_NEED_GRID_FRIEND = 'KUAN_NEED_GRID_FRIEND'
复制代码
// 一个action对象
// 好比此action是告诉redux,阿宽想要一个女友
{
type: KUAN_NEED_GRID_FRIEND,
params: {
job: '程序员',
username: '阿宽'
}
}
复制代码
咱们来了解一个知识点: Action Creator
,看看官网中的介绍 : Redux 中的 Action Creator 只是简单的返回一个 Action,咱们通常都会这么写~
function fetchWishGridFriend(params, callback) {
return {
type: KUAN_NEED_GRID_FRIEND,
params,
callback,
}
}
复制代码
咱们知道哈,Redux 由 Flux 演变而来,在传统的 Flux 中, Action Creators 被调用以后常常会触发一个 dispatch。好比是这样的 👇
// 传统 Flux
function fetchFluxAction(params, callback) {
const action = {
type: KUAN_NEED_GRID_FRIEND,
params,
callback,
}
dispatch(action)
}
复制代码
可是在 redux 中,由于 store(上边说过了)中存在 dispatch 方法的,因此咱们只须要将 Action Creators
返回的结果传给 dispatch()
,就完成了发起一个 dispatch 的过程,甚至于建立一个被绑定的 Action Creators 来自动 dispatch ~
// 普通dispatch
store.dispatch(fetchWishGridFriend(params, () => {}))
// 绑定dispatch
const bindActionCreatorsDemo = (params, callback) => (store.dispatch) =>
store.dispatch(fetchWishGridFriend(params, callback))
bindActionCreatorsDemo() // 就能实现一个dispatch action
复制代码
👉 在你的代码中,必定能够找获得
bindActionCreators()
这玩意,由于通常状况下,咱们都会使用 react-redux 提供的 connect() 帮助器,bindActionCreators() 能够自动把多个 action 建立函数绑定到 dispatch() 方法上。
Reducers 必须是一个纯函数,它根据 action 处理 state 的更新,若是没有更新或遇到未知 action,则返回旧 state;不然返回一个新 state 对象。注意:不能修改旧 state,必须先拷贝一份 state,再进行修改,也可使用 Object.assign 函数生成新的 state,具体为何,咱们读源码的时候就知道啦~
举个例子 🌰
// 用户reducer
const initialUserState = {
userId: undefined
}
function userReducer = (state = initialUserState, action) {
switch(action.type) {
case KUAN_NEED_GRID_FRIEND:
return Object.assign({}, state, {
userId: action.payload.data
})
default:
return state;
}
}
复制代码
在看源码以前,我举个形象生动的 🌰 ,帮助你们理解理解。
小彭想请个假去旅游,按照原流程,必须得由从 小彭申请请假 -> 部门经理经过 -> 技术总监经过 -> HR 经过(单向流程),小彭的假条不能直接到 HR 那边。看下图 👇
阿宽看到小彭请假旅游,也想请一波,因而想 copy 一份小彭的请假事由(兄弟组件进行数据共享)那咋办,他不能直接从小彭那拿数据,因此他只能傻乎乎的经过部门经理、技术总监,一路“闯关”到 HR 那,指着 HR 说,你把小彭的请假表给我复印一份,我也要请假。
小彭和阿宽想进行数据之间共享,只能经过共有的 boss(HR)
当咱们用了 redux 以后呢,就变成这屌样了 👇 看懂扣 1,看不懂扣眼珠子
淦!!! 又到了我最讨厌的源码解读了,由于讲源码太难了,不是源码难,而是怎么去讲比较难,毕竟我自己理解的和认识的 redux,不必定是正确的,同时我也不想直接贴一大堆代码上去,你不就是不想看源码才看的这篇文章吗~
不过没办法,理解万岁。幸亏 redux 的源码文件相对较少,你们一块儿奥力给!
🎉 直接看源码,github 戳这里,咱们能够看到这样的文件架构
├── utils
│ ├── actionTypes
│ ├── isPlainObject
│ ├── warning
│ └─
│
├── applyMiddleware
├── bindActionCreatorts
├── combineReducers
├── compose
├── createStore
├── index.js
│
└─
复制代码
很少吧?说多的出门左转不送。看源码要从 index.js
开始入手,跟着镜头,咱们去看看这个文件有啥玩意。其实没啥重要玩意, 就是把文件引入而后 export
// index.js
import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
...
export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose }
复制代码
咱们先来看第一行代码,import createStore from './createStore'
,😯,这个我知道,这不就是 redux 中最核心的 API 之一吗?让咱们去揭开它的面纱~
// API
const store = createStore(reducers, preloadedState, enhance)
复制代码
初次看,不知道这三个参数啥意思?不慌,先抽根烟,打开百度翻译,你就知道了。(由于源码中有对这三个参数给出解释)
/** * 建立一个包含状态树的Redux存储 * 更改store中数据的惟一方法是在其上调用 `dispatch()` * * 你的app中应该只有一个store,指定状态树的不一样部分如何响应操做 * 你可使用 `combineReducers` 将几个reducer组合成一个reducer函数 * * @param {Function} reducer 给定当前状态树和要处理的操做的函数,返回下一个状态树 * * @param {any} [preloadedState] 初始状态. 你能够选择将其指定为中的universal apps服务器状态,或者还原之前序列化的用户会话。 * 若是你使用 `combineReducers` 来产生 root reducer 函数,那么它必须是一个与 `combineReducers` 键形状相同的对象 * * @param {Function} [enhancer] store enhancer. 你能够选择指定它来加强store的第三方功能 * 好比 middleware、time travel、persistence, Redux附带的惟一商店加强器是 `applyMiddleware()` * * @returns {Store} Redux Store,容许您读取状态,调度操做和订阅更改。 */
复制代码
了解这三个参数的意思以后呢,咱们再看看它的返回值,中间作了啥先不用管。上边有说过,调用 createStore
方法建立的 store 是一个对象,它包含 4 个方法,因此代码确定是这样的,不是我剁 diao !
// createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false // 是否正在分发事件
function getState() {
// ...
return currentState
}
function subscribe(listener) {
// ...
}
function dispatch(action) {
// ...
return action
}
function replaceReducer(nextReducer) {
// ...
}
function observable() {
// ...
}
dispatch({ type: ActionTypes.INIT })
// ...
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable,
}
}
复制代码
就这些代码,想必都看得懂,可是不得不佩服写这段代码的人啊!!首先经过闭包进行了内部变量私有化,外部是没法访问闭包内的变量。其次呢经过对外暴露了接口,以达到外部对内部属性的访问。
这不就是沙箱吗?沙箱,就是让你的程序跑在一个隔离的环境下,不对外界的其余程序形成影响。咱们的 createStore
对内保护内部数据的安全性,对外经过开发的接口,进行访问和操做。🐂 🍺 ~
💥 建议直接去看源码文件,由于里边对于每个接口的注释很详细~
不难看到,上边经过 subscribe
进行接口注册订阅函数,咱们能够细看这个函数作了什么事情~
function subscribe(listener) {
...
let isSubscribed = true
ensureCanMutateNextListeners();
nextListeners.push(listener)
return function unsubscribe() {
if(!isSubscribed) {
return
}
// reducer执行中,你可能没法取消store侦听器
if (isDispatching) {}
isSubscribed = false
// 从 nextListeners 中去除掉当前 listener
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
复制代码
其实这个方法主要作的事情就是 : 注册 listener,同时返回一个取消事件注册的方法。当调用 store.dispatch 的时候调用 listener ~
思路真的是很严谨了,定义了 isSubscribed
、isDispatching
来避免意外的发生,同时还对传入对 lister
进行类型判断。考虑到有些人会取消订阅,因此还提供了一个取消订阅的unsubscribe
。
紧接着咱们再来看看 dispatch,主要是用与发布一个 action 对象,前边有说到了,你想要修改 store 中的数据,惟一方式就是经过 dispatch action,咱们来看看它作了什么事情~
function dispatch(action) {
if (!isPlainObject(action)) {
}
if (typeof action.type === 'undefined') {
}
// 调用dispatch的时候只能一个个调用,经过dispatch判断调用的状态
if (isDispatching) {
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 遍历调用各个listener
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
复制代码
不是吧,阿 sir,这么严格,前边就作了各类限制,下边这段 try {} finally {}
也是神操做啊,为了保证 isDispatch
在函数内部状态的一致,在 finally 的时候都会将其改成 false
。牛掰~
从源码注释里边,我也看到这么一段话 ~
It will be called any time an action is dispatched, and some part of the state tree may potentially have changed.
You may then call
getState()
to read the current state tree inside the callback.
意味着,当你执行了以前订阅的函数 listener 以后,你必须,经过 store.getState()
去那最新的数据。由于这个订阅函数 listener 是没有参数的,真的很严格。
老舍先生的《四世同堂》十九中有一句化 : “他以为老大实在有可爱的地方,因而,他决定趁热打铁,把话都说净。”,是的,趁热打铁,既然咱们说到了 dispatch(action)
, 那咱们接着说一说: bindActionCreators
~
不知道各位有没有写过这样的代码~
import { bindActionCreators } from 'redux';
import * as pengActions from '@store/actions/peng';
import * as kuanActions from '@store/actions/kuan';
import * as userActions from '@store/actions/user';
const mapDispatchToProps => dispatch => {
return {
...bindActionCreators(pengActions, dispatch);
...bindActionCreators(kuanActions, dispatch);
...bindActionCreators(userActions, dispatch);
}
}
复制代码
咱们来讲说,这个 bindActionCreators
它到底作了什么事情。首先来看官方源码注释:
store.dispatch(MyActionCreator.doSomething)
function bindActionCreator(actionCreator, dispatch) {
return function (this, ...args) {
return dispatch(actionCreator.apply(this, args))
}
}
// bindActionCreators 指望获得的是一个 Object 做为 actionCreators 传进来
export default function bindActionCreators(actionCreators, dispatch) {
// 若是只是传入一个action,则经过bindActionCreator返回被绑定到dispatch的函数
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
}
const boundActionCreators = {} // 最终导出的就是这个对象
for (const key in actionCreator) {
const actionCreator = actionCreator[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
复制代码
对了,这里你们必定要记住,Action 的取名尽可能不要重复,举个 🌰
小彭和阿宽都有一个需求,那就是发起一个修改年龄的 action,原本两不相干,井水不犯河水,因而他两洋洋洒洒的在代码中写下了这段代码 ~
// pengAction.js
export function changeAge(params, callback) {
return {
type: 'CHANGE_AGE',
params,
callback,
}
}
// kuanAction.js
export function changeAge(params, callback) {
return {
type: 'CHANGE_AGE',
params,
callback,
}
}
复制代码
你说巧不巧,产品让阿华去作一个需求,须要点击按钮的时候,把小彭和阿宽的年龄都改了。阿华想用 bindActionCreators
装 B,因而写下了这段代码
const mapDispatchToProps => dispatch => {
return {
...bindActionCreators(pengActions, dispatch);
...bindActionCreators(kuanActions, dispatch);
}
}
复制代码
按照咱们对 bindActionCreators
的源码理解,它应该是这样的 😯
pengActions = {
changeAge: action,
}
export default function bindActionCreators(pengActions, dispatch) {
// ...
const boundActionCreators = {}
for (const key in pengActions) {
// key就是changeAge
const actionCreator = pengActions[changeAge]
// ...
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
return boundActionCreators
}
复制代码
因此最终,这段代码结果是这样的
const mapDispatchToProps => dispatch => {
return {
changeAge, // ...bindActionCreators(pengActions, dispatch);
changeAge // ...bindActionCreators(kuanActions, dispatch);
}
}
复制代码
问题知道在哪了吧,因此如何解决呢,我我的见解, 你要么就 actionName 不要同样,能够叫 changePengAge
、changeKuanAge
,要么就是多包一个对象。
const mapDispatchToProps => dispatch => {
return {
peng: {
...bindActionCreators(pengActions, dispatch);
},
kuan: {
...bindActionCreators(kuanActions, dispatch);
}
}
}
复制代码
既然前边都说了,整个应用的 state
都存储在一颗 state tree 中,而且只存在于惟一一个 store 中, 那么咱们来看看这到底是何方神圣~
小彭项目初次搭建的时候,要求小,状态管理比较方便,因此呢,都放在了一个 reducer 中,后边随着不断迭代,因而不断的往这个 reducer
中塞数据。
典型的屁股决定脑壳,因而有一天,可能某个天使,给 redux 的开发团队提了一个 issue
, “哎呀,你能不能提供一个 API,把个人全部 reducer 都整合在一块啊,我想分模块化的管理状态”
好比用户模块,就叫
userReducer
,商品模块,咱们叫shopReducer
,订单模块,咱们称之为orderReducer
。既然那么多个 reducer,该如何合并成一个呢 ?
因而 redux 提供了 combineReducers
这个 API,看来 redux 的时间管理学学的很好,你看,这么多个 reducer
,都能整合在一块儿,想必花了很大的功夫~
那咱们看看 combineReducers 作了什么事情吧 ~ 在此以前,咱们看看咱们都怎么用这玩意的~
// 两个reducer
const pengReducer = (state = initPengState, action) => {}
const kuanReducer = (state = initKuanState, action) => {}
const appReducer = combineReducers({
pengReducer,
kuanReducer,
})
复制代码
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers) // 获得全部的reducer名
// 1. 过滤reducers中不是function的键值对,过滤后符合的reducer放在finalReducers中
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// 2. 再一次过滤,判断reducer中传入的值是否合法
let shapeAssertionError: Error
try {
// assertReducerShape 函数用于遍历finalReducers中的reducer,检查传入reducer的state是否合法
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// 3. 返回一个函数
return function combination(state, action) {
// 严格redux又上线了,各类严格的检查
// ...
let hasChanged = false // 就是这逼,用来标志这个state是否有更新
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
// 这也就是为何说combineReducers黑魔法--要求传入的Object参数中,reducer function的名称和要和state同名的缘由
const reducer = finalReducers[key]
const previousStateForKey = state[key]
// 将reducer返回的值,存入nextState
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
// 若是任一state有更新则hasChanged为true
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
}
}
复制代码
这个源码其实很少也不难,跟着阿宽这样看下来,也不是很吃力吧?那这里就延伸了一个问题,为何 redux 必须返回一个新的 state ? 返回旧的不行吗 ?
伊索寓言有句话我特喜欢 : 逃出陷阱比掉入陷阱难之又难,是的,reducer 也有陷阱~ 众所周知啊,reducer 必须是个纯函数,这里有小伙伴懵逼了,这 TM 怎么又多出了一个知识点,不用管,我也不打算多讲。自行百度~
咱们来看看,通常状况下咱们都怎么写 reducer 的
function pengReducer(state = initialState, action) {
switch (action.type) {
// 这种方式
case 'CHANGE_AGE':
return {
...state,
age: action.data.age,
}
// 或者这种方式都行
case 'ADD_AGE':
return Object.assign({}, state, {
age: action.data.age,
})
}
}
复制代码
假设,咱们不是这么写的,咱们直接修改 state,而不是返回一个新的 state,会是怎样的结果~
function pengReducer(state = initialState, action) {
switch (action.type) {
// 或者这种方式都行
case 'CHANGE_AGE':
state.age = action.data.age
return state
}
}
复制代码
当咱们触发 action 以后,你会发出 : 卧槽,页面为何没变化 ...
回到咱们的源码,咱们能够来看~
const nextStateForKey = reducer(previousStateForKey, action)
复制代码
这里主要就是,获得经过 reducer 执行以后的 state,它不是一个 key,它是一个 state,而后呢,往下继续执行了这行代码~
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
复制代码
比较新旧两个对象是否一致,进行的是浅比较法
,因此,当咱们 reducer 直接返回旧的 state 对象时,Redux 认为没有任何改变,从而致使页面没有更新。
❓ 这就是为何!返回旧的 state 不行,须要返回一个新的 state 缘由。咱们都知道啊,在 JS 中,比较两个对象是否彻底同样,那只能深比较,然而,深比较在真实的应用中代码是很是大的,很是耗性能的,而且若是你的对象嵌套足够神,那么须要比较的次数特别多~
因此 redux 就采起了一个较为“委婉”的解决方案:当不管发生任何变化时,都要返回一个新的对象,没有变化时,返回旧的对象~
跪了,感受 redux 源码中,最难的莫过于中间件了,在说这玩意以前,咱们先来聊聊,一些有趣的东西~
一提到 react,不知道你们第一印象是什么,可是有一个词,我以为绝大部分对人都应该听过,那就是 : 💗 函数式编程 ~
怎么理解,在 JS 中,函数能够看成是变量传入,也能够赋值给一个变量,甚至于,函数执行的返回结果也能够是函数。
const func = function () {}
// 1. 看成参数
function demo1(func) {}
// 2. 赋值给另外一个变量
const copy_func = func
// 3. 函数执行的返回结果是函数
function demo2() {
return func
}
复制代码
在函数式编程语言中,数据是不可变的,全部的数据一旦产生,就不能改变其中的值,若是要改变,那就只能生成一个新的数据。
可能有些小伙伴会有过这个库 : seamless-immutable
,在 redux 中,强调了,不能直接修改 state 的值(上边有说了,不听课的,出去吃屁),只能返回一个新的 state ~
怎么理解,大伙估计都写了好久的多参数,看到这个懵了啊,我也懵了,可是这就是规矩,无规矩,不成方圆 ~
因此当你看中间件的代码时,你就不会奇怪了,好比这行代码 ~
const middleware = (store) => (next) => (action) => {}
复制代码
换成咱们可以理解的形式,那就是 :
const middleware = (store) => {
return (next) => {
return (action) => {}
}
}
复制代码
这里有人就疑问了,尼玛,这不就是依赖了三个参数吗,那能不能这样写啊?
const middleware = (store, next, action) => {}
复制代码
💐 just you happy ! 你高兴就好,可是函数式编程就是要求,只能有一个参数,这是规矩,懂 ? 在我地盘,你就只能给我装怂 !
说说组合 compose,这个是个啥玩意,咱们来看一段代码 :
const compose = (f, g) => {
return (x) => {
return f(g(x))
}
}
const add = function (x) {
return x + 2
}
const del = function (x) {
return x - 1
}
// 使用组合函数,🧬 基因突变,强强联合
const composeFunction = compose(add, del)(100)
复制代码
猜一下,执行 composeFunction 打印什么?答对的,给本身鼓个掌 👏
好了,我已经把最为强大的忍术: 函数式编程术语之 compose 组合函数,教给你了~
这里又有小伙伴懵圈了,怎么又来了一个知识点?不慌,容阿宽给你简单介绍一下 ? 咱们上边说了 compose 函数,那么组合函数和洋葱模型有什么关系呢 ?
洋葱模型是本质上是一层层的处理逻辑,而在函数式编程世界里,意味着用函数来作处理单元。先不说其余,咱们先上一个 🌰,帮助你们理解~
let middleware = []
middleware.push((next) => {
console.log('A')
next()
console.log('A1')
})
middleware.push((next) => {
console.log('B')
next()
console.log('B1')
})
middleware.push((next) => {
console.log('C')
})
let func = compose(middleware)
func()
复制代码
猜猜打印顺序是个啥 ?没错,打印结果为 : A -> B -> C -> B1 -> A1
哎哟,不错哦,好像有点感受了。当程序运行到 next()
的时候会暂停当前程序,进入下一个中间件,处理完以后才会仔回过头来继续处理。
这两张图应该是老图了,并且是唠嗑到洋葱模式必贴的图,就跟你喝酒同样,必定要配花生米(别问为何,问就是规矩)
咱们看这张图,颇有意思哈,会有两次进入同一个中间件的行为,并且是在全部第一次的中间件执行以后,才依次返回上一个中间件。你品,你细品~
好了,不逼逼了,因为个人查克拉
不足,关于其余的函数式编程的忍术要求,就不一一讲了,ok,这里打了个预防针,咱们再来看看 applyMiddleware
到底作了什么 丧心病狂 的事情吧~
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, ...args) => {
const store = createStore(reducer, ...args)
let dispatch: Dispatch = () => {}
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
}
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch,
}
}
}
复制代码
代码极其简短,让咱们看一下,干了啥事~ 首先呢返回一个以 createStore
为参数的匿名函数,而后呢,这个函数返回另外一个以 reducer, ...args (实际就是 initState, enhancer)
为参数的匿名函数, 接着定义了一个链 chain,这个就颇有意思了。
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
复制代码
咱们先是把传入的 middlewares
进行剥皮,并给中间件 middleware
都以咱们定义的 middlewareAPI
做为参数注入,因此咱们每个中间件的上下文是 dispatch 和 getState,为何?为何要注入这两个玩意?
getState:这样每一层洋葱均可以获取到当前的状态。
dispatch:为了能够将操做传递给下一个洋葱
ok,这样执行完了以后,chain 实际上是一个 (next) => (action) => { ... }
函数的数组,也就是中间件剥开后返回的函数组成的数组。以后咱们以 store.dispatch
做为参数进行注入~ 经过 compose
对中间件数组内剥出来的高阶函数进行组合造成一个调用链。调用一次,中间件内的全部函数都将被执行。
// 或许换成这种形式,你更加能明白~
function compose(...chain) {
return store.dispatch => {
// ...
}
}
复制代码
上边说到,这逼就是将咱们传入的 chain
造成一个调用链,那咱们 see see,它是怎么作到的~
export default function compose(...funcs) {
if (funcs.length === 0) {
return (arg) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
复制代码
还记得上边教大家的组合 compose 吗,咱们试着还原一下常人能看得懂的样子 ~
(a, b) => (...args) => a(b(...args))
// 常人能看得懂的
(a, b) => {
return (...args) {
return a(b(...args))
}
}
复制代码
两个字,牛皮 🐂🍺 不得不感慨,果真是大佬。 那么下边,咱们来一步步捋一捋这究竟是个啥东西。
dispatch 是用来干吗的?
🙋 我会我会,dispatch 是用来分发 action 的
,good,那么,咱们能够获得第一个函数
(store.dispatch) => (action) => {}
复制代码
问题又来了,咱们的 compose 通过一顿骚操做后获得的一组结构相同的函数,最终合并成一个函数。
dispatch
,又要传递 action
,那么咱们怎么搞?高阶函数用起来middleware = (store.dispatch, store.getState) => (next) => (action) => {}
复制代码
ok,那有人就好奇了,这个 next 是个啥玩意啊?其实传入中间件的 next 实际上就是 store.dispatch,奇奇怪怪的问题又出现了
redux 开发者利用了闭包的特性,将内部的 dispatch 与外部进行强绑定,MD,🐂🍺
// 实例demo
let dispatch = () => {}
middlewares.map((middleware) =>
middleware({
getState,
dispatch() {
return dispatch
},
})
)
复制代码
因此你应该可以明白源码中这段代码的真谛了吧?
//真实源码
let middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
}
// 其实你把 middlewareAPI 写到 middleware 里边,就等价于上边那玩意了
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
复制代码
而后接下来咱们须要作些什么?重要的话说三遍,上边说了两边,这边再说一边,compose 处理后获得的是一个函数,那么这个函数到底该怎样调用呢。传入 store.dispatch
就行了呀~
// 真实源码
dispatch = compose(...chain)(store.dispatch)
复制代码
这段代码实际上就等价于:
dispatch = chain1(chain2(chain3(store.dispatch)))
复制代码
chain一、chain二、chain3 就是 chain 中的元素,进行了一次柯里化,稳。dispatch 在这里边扮演了什么角色?
你能够这么理解,中间件其实就是咱们自定义了一个 dispatch,而后这个 dispatch 会按照洋葱模型进行 pipe
what the fuck ! 🐂 🍺 爆粗口就对了。不过这里我仍是有一个疑惑,但愿看到这的大哥们,能解疑一下 ~
留给个人疑惑: 为何在 middlewareAPI 中,dispatch 不是直接写成 store.dispatch, 而是用的匿名函数的闭包引用?
// 为何不这么写....
let middlewareAPI = {
getState: store.getState,
dispatch: (action) => store.dispatch(action),
}
复制代码
到了这一步,还没听懂的小伙伴,能够再多看一遍,正所谓温故而知新,多看看,多捋捋,就能知道啦~
这篇文章写了我五天,设计到的知识点略多,能够说是有些知识点,现学现用,不过问题不大,由于延伸的知识点不是本文的重点~经过写这篇文章,能够说是加深了我对 redux 的认识。不知道有没有小伙伴跟我同样,想去看源码,正面刚,刚不过,去看一些博客文章对其解读,又太难,可能我没 get 到做者想表达的意思,或者是对于其中的一些知识点,一带而过,因此我想把我遇到的问题,在学习的路上踩到的坑,跟你们一同分享,固然个人理解也不必定正确,理解有误可一同交流。奥力给,不说了,我去准备动手作 demo 了,期待下一篇吧 ~