Open shenxuxiang opened 4 years ago
首先我们要弄清楚 reduxjs 的思想、作用是什么,这样我们才能开始下一步的构思。在我看来 reduxjs 核心就是一种单一数据源的概念,数据存储在一个函数的 state 变量中,只能通过固定的方法去修改和获取 dispatch()、getState()。
reduxjs
state
dispatch()、getState()
在 SPA 应用中,reduxjs 被广泛使用。对数据进行统一管理、实现数据共享,通常组件和组件之间、页面和页面之间可以数据共享。在 react 开发中,我经常将共用的数据和异步请求数据存放在 state 中。通过 props 的形式存在,只要在一个组件中对数据源进行了修改,其他共享的组件都会及时得到更新和渲染UI界面。
react
props
现在我们知道了关于 redux 的关键思想和用途,接下来我们一步一步实现它。我会按照下面这个列表的顺序给大家详细说明:
redux
function createStore(reducer, initState) { // 声明一个初始化用的 action const INIT_ACTION = undefined; // 绑定监听事件的集合 const listeners = []; // 这就是我们一直说的那个【数据源】 // 参数 initState 可以有,也可以没有。一般情况下不需要传递 let state = initState ? initState : {}; function dispatch(action) { // action 必须是一个纯对象,不能是其他的类型 if (Object.prototype.toString.call(action) === '[object Object]') { throw new Error('Actions must be plain objects'); } // 注意:这里是最终还是通过调用 reducer 方法 state = reducer(state, action); // 遍历 listeners for (let i = 0; i < listeners.length; i++) { listeners[i](); } } // 获取 state 数据 function getState() { return state; } // 绑定监听事件 function subscription(listener) { listeners.push(listener); // 取消监听,将事件从 listeners 中移除 return function() { const idx = listeners.indexOf(listener); if (idx >= 0) { listeners.splice(idx, 1); } } } // 这是啥意思了,其实这是在调用 createStore() 时,就初始化了一个 state dispatch(INIT_ACTION); // 通过对象,将这些内部函数传递到外部。不要怀疑,这就是一个典型的闭包 return { dispatch, getState, subscription, }; }
从 createStore 方法中我们可以看出来,其实他就是 js模块。利用了局部变量和闭包的特性,将 state 隐藏起来,只能通过闭包的形式进行访问和修改。
createStore
js模块
首先 reduce 它是一个函数,我们可以自己定义。我们可以把我们的项目想像成如下的一个场景,修改用户的信息:
function userName(state = {}, action = {}) { switch (action.type) { case 'name': return { ...state, name: action.data }; case 'age': return { ...state, age: action.data }; case 'sex': return { ...state, sex: action.data }; // 必须设置 default,直接返回 state default: return state; } }
如果我们的项目中只需要这一种交互场景,那么定义 userName() 就够了。这个时候 我们把 userName 传递给 createStore
const { getState } = createStore(userName); // 返回的是一个 {} console.log(getState());
上面的代码在执行 createStore(userName) 时,内部执行一次 dispatch(INIT_ACTION) ,从而在 dispatch 方法内部调用了 userName({}, undefined)。所以打印的结果是一个空对象。
createStore(userName)
dispatch(INIT_ACTION)
userName({}, undefined)
如果交互场景比较多的时候呢,一个 reducer 肯定不够用啊,那么这个时候我们可能会定义多个类似 userName 这个的 reducer 函数,所以我们还需要定义一个工具函数 combineReducers,将多个 reducer 函数组合成一个 reducer 函数。
reducer
userName
combineReducers
function combineReducers(reducers) { const keys = Object.keys(reducers); const finallyKeys = []; for (let i = 0; i < keys.length; i++) { if (typeof reducers[keys[i]] !== 'function') throw Error('reducer must be a function'); finallyKeys.push(keys[i]); } // 看,最后返回的还是一个 function return function(state = {}, action) { let hasChange = false; const newState = {}; // 遍历所有的 reducer 函数 finallyKeys.forEach(key => { // 获取这个 reducer 函数对应的 state。注意它可能是一个 undefined // 没错,在 createStore() 中执行 dispatch(INIT_ACTION),这个时候 prevState_key 可能就是一个 unudefined const prevState_key = state[key]; const reducer = reducers[key]; // 调用该 reducer,返回一个新的 state const nextState_key = reducer(prevState_key, action); // 注意这里,如果 reducer 函数返回的是一个 undefined。那么这里就会报错了 // 所以我们在定义 reducer 函数时,应该有一个限制:如果没有匹配到 action 的 type 。应该默认返回 previous state。 if (typeOf nextState_key === 'undefined') { throw Error('to ignore an action, you must explicitly return the previous state'); } // 当 reducer 执行完成时,会在 newState 上添加一个新属性,属性值就是 nextState_key // 其实,从这个地方我们就应该可以猜测到,最终得到的 state【数据源】,它的结果应该和我们传入的 reducers 结构是一样的 newState[key] = nextState_key; hasChange = hasChange || nextState_key !== prevState_key; }); return hasChange ? newState : state; } }
结合之前的 createStore,我们看看下面的 demo:
function menu(state = {}, action = {}) { switch (action.type) { case 'home': return { ...state, home: action.data }; case 'list': return { ...state, list: action.data }; case 'detail': return { ...state, detail: action.data }; default: return state; } } const reducer = combineReducers({ userName, menu }); const { getState } = createStore(userName); // 返回的是一个 { userName: {}, menu: {} } // 这里和我们传递给 combineReducers() 中的参数的结构是一致的。 console.log(getState());
上面的 reducer 是 userName, menu 的一个组合体,所以每次调用 dispatch(action) 时,都会遍历所有的 reducers。还有一个很重要的地方就是,每个 reducer 函数在没有匹配到 action.type 时,必须把 reducer() 的参数 state 作为返回值,否则就报错。
userName, menu
dispatch(action)
reducers
action.type
reducer()
reduxjs 还有一个非常厉害的功能,就是可以利用中间件,做很多事情。比如说,我们比较常用的 redux-thunk、redux-logger 等。
redux-thunk、redux-logger
// 这里先不考虑参数为空的情况 function compose() { const middleware = [...arguments]; // 这里利用了redux 高阶函数 // 第一次执行时,将 middleware 中的第一个和第二个元素赋值给 a、b。然后将返回的结果函数 fn 赋值给 a。 // 第二次执行时,a 就是上一次的执行结果,这个时候将 middleware 中的第三个元素赋值给 b。然后将返回的结果函数 fn 赋值给 a。 // 第三次,第四次。依次类推。。。 return middleware.reduce(function(a, b) { return function fn () { return a(b.apply(null, arguments)); } }); } function applyMiddlyWare(createStore) { return function(reducer) { // 接收中间件作为参数 return function(...middlewares) { const { dispatch, getState, subscription } = createStore(reducer); // 将 dispatch 赋值给变量 _dispatch let _dispatch = dispatch; const disp = (...args) => { _dispatch(...args); } // 将上面定义 disp 内部函数,传递给每一个中间件函数 // 所以上面的 disp 就构成了一个闭包 const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState })); // 这里又对变量 _dispatch 进行了赋值。这里理解可能有点绕,后面再详细介绍 // 注意这里是一个科里化函数的调用, 参数 dispatch 是原始,没有进过改造的 _dispatch = compose(...chain)(dispatch); return { dispatch: _dispatch, getState, subscription, } } } }
到这里为止,reduxjs 就基本实现了。但是我们的探讨还没有结束,继续往下看
从上面的代码我们可以看出来,applyMiddlyWare 函数其实就是对 createStore 的一层封装,最终输出的 dispatch 是经过中间件改造过的。现在我们来看看这个 dispatch 到底是什么,它和我们传入的中间件有什么关系???
applyMiddlyWare
dispatch
const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState })); _dispatch = compose(...chain)(dispatch);
上面的两行代码,先遍历执行中间件,再将变量 chain 传递给 compose 函数。所以我们应该可以猜测到,表达式 middleware({ dispatch: disp, getState }) 应该返回一个函数,不然 compose 中的 reduce 就没有办法执行了。
chain
compose
middleware({ dispatch: disp, getState })
reduce
这里还要考虑到中间件执行的策略,所有的中间件必须串联起来,挨个往下执行。所以中间件应该还应该接收另一个中间件作为参数。所以现在我们可以大致的猜测到一个中间件应该是这样的:
function middleware({ dispatch, getState }) { return function (nextMiddleware) { return function () { // 这里应该先执行一些任务,然后再去执行下一个中间件 ... nextMiddleware(); } } }
这个时候其实中间件的模型还不够完整,少了一些东西。少了什么了,就是 action 呀!applyMiddlyWare 函数通过中间件对 dispatch 进行改造。所以还是要接收 action 才能对 state 进行修改。所以这下我们清楚了
action
function middleware({ dispatch, getState }) { return function (nextMiddleware) { return function (action) { // 在调用 nextMiddleware 之前可以进行一些操作 console.log(1111); // 必须将 action 传递给下一个中间件 const result = nextMiddleware(action); // 在调用 nextMiddleware 之后可以进行一些操作 console.log(222); return result; } } }
现在我们清楚了中间件的模型了,可以来专门研究一下 applyMiddlyWare 函数返回的 dispatch 是啥玩意了
function compose() { const middleware = [...arguments]; return middleware.reduce(function(a, b) { return function fn () { return a(b.apply(null, arguments)); } }); } function one(next) { console.log('one'); return function one_(action) { console.log('这是中间件one,你可以在这里做很多事情', action); return next(action) } } function two(next) { console.log('two'); return function two_(action) { console.log('这是中间件two,你可以在next调用之前做一些事情', action); const result = next(action); console.log('这是中间件two,也可以在next调用之后做一些事情', action); return result; } } function three(next) { console.log('three'); return function three_(action) { console.log('这是中间件three,你可以在这里做很多事情', action); return next(action) } } // 可以把它当作 createStore 函数返回的 dispatch 方法 function dispatch(action) { console.log(action); } // 我这么写,大家应该可以理解哈。因为 compose 函数接收到的其实是 middleware({ dispatch, getState }) 返回的结果 // 所以这里的 one, two, three 可以理解为是 middleware({ dispatch, getState }) 返回的结果 // 这里只是做一个简单的 demo,用不到 dispatch, getState。 var disp = compose(one, two, three)(dispatch);
我们把 compose(one, two, three)(dispatch) 这段代码用我们自己的代码实现一下,大致就是下面这样的效果:
compose(one, two, three)(dispatch)
var fn = (function(one, two, three) { var first = function() { return one(two.apply(null, arguments)); }; var next = function() { return first(three.apply(null, arguments)); }; return next })(one, two, three); var disp = fn(dispatch);
当调用 fn(dispatch) 时,three.apply(null, dispatch) 开始执行,返回一个 three_ 函数。继续往下执行。
fn(dispatch)
three.apply(null, dispatch)
three_
first(three_) 开始执行,然后执行 two.apply(null, three_),two 执行完成,返回一个 two_ 函数。继续往下执行。
first(three_)
two.apply(null, three_)
two
two_
one(two_) 开始执行,并返回一个 one_ 函数,这个函数最终作为 fn(dispatch) 执行的最终结果,并赋值给变量 disp。
one(two_)
one_
disp
disp(action) 执行时,先调用 one_(action) 然后是 two_(action) 最后是 three_(action)。注意最后一个中间件接收的参数不是中间件参数了,而是原始的 dispatch 方法。所以会在最后一个中间件中执行 dispatch(action),从而调用 rducer 函数修改数据源【state】。
disp(action)
one_(action)
two_(action)
three_(action)
执行 disp({data: 1200, type: 'username'})这段代码,看下打印的结果是啥
disp({data: 1200, type: 'username'})
这下我们就非常清楚了,原来经过 applyMiddlyWare 改造后输出的 dispatch 方法,在调用时,会挨个执行每一个传入 applyMiddlyWare 函数的中间件,并在最后一个中间件中调用原始的 dispatch() 方法。
dispatch()
// 中间件1 function thunk ({dispatch, getState}) { return function (next) { return function(action) { if (typeof action === 'function') { action({dispatch, getState}); } else { return next(action); } } } } // 中间件2 function dialog ({dispatch, getState}) { return function (next) { return function(action) { console.log('prevstate:', getState()); const result = next(action); console.log('nextstate:', getState()); return result; } } }
// 模拟用户http请求 function getUserName(name) { return ({dispatch}) => { setTimeout(() => { dispatch({type: 'name', data: name}) }, 0); } } function getUserAge(age) { return ({dispatch}) => { setTimeout(() => { dispatch({type: 'age', data: age}) }, 0); } } function getUserSex(sex) { return ({dispatch}) => { setTimeout(() => { dispatch({type: 'sex', data: sex}) }, 0); } } function getHome(value) { return ({dispatch}) => { setTimeout(() => { dispatch({type: 'home', data: value}) }, 0); } } function getList(value) { return ({dispatch}) => { setTimeout(() => { dispatch({type: 'list', data: value}) }, 0); } } function getDetail(value) { return ({dispatch}) => { setTimeout(() => { dispatch({type: 'detail', data: value}) }, 0); } }
// userName, menu 直接复制前面的代码 var reducer = combineReducers({ userName, menu }); var { dispatch, getState, subscription } = applyMiddlyWare(store)(reducer)(thunk, dialog); console.log(getState(), 'initState'); const name_button = document.querySelector('.name'); const age_button = document.querySelector('.age'); const sex_button = document.querySelector('.sex'); const home_button = document.querySelector('.home'); const list_button = document.querySelector('.list'); const detail_button = document.querySelector('.detail'); const addListener = document.querySelector('.addListener'); const removeListener = document.querySelector('.removeListener'); name_button.onclick = function() { dispatch(getUserName('shenxuxiang')) }; age_button.onclick = function() { dispatch(getUserAge('29')) }; sex_button.onclick = function() { dispatch(getUserSex('man')) }; home_button.onclick = function() { dispatch(getHome('home_page')) }; list_button.onclick = function() { dispatch(getList('list_page')) }; detail_button.onclick = function() { dispatch(getDetail('detail_page')) }; let removeListen; addListener.onclick = function() { removeListen = subscription(function() { console.log('我们添加了一个事件监听器', getState()) }) }; removeListener.onclick = function() { removeListen && removeListen(); };
首先我们要弄清楚
reduxjs
的思想、作用是什么,这样我们才能开始下一步的构思。在我看来reduxjs
核心就是一种单一数据源的概念,数据存储在一个函数的state
变量中,只能通过固定的方法去修改和获取dispatch()、getState()
。在 SPA 应用中,
reduxjs
被广泛使用。对数据进行统一管理、实现数据共享,通常组件和组件之间、页面和页面之间可以数据共享。在react
开发中,我经常将共用的数据和异步请求数据存放在state
中。通过props
的形式存在,只要在一个组件中对数据源进行了修改,其他共享的组件都会及时得到更新和渲染UI界面。现在我们知道了关于
redux
的关键思想和用途,接下来我们一步一步实现它。我会按照下面这个列表的顺序给大家详细说明:createStore()
从
createStore
方法中我们可以看出来,其实他就是js模块
。利用了局部变量和闭包的特性,将state
隐藏起来,只能通过闭包的形式进行访问和修改。reduce、combineReducers
首先 reduce 它是一个函数,我们可以自己定义。我们可以把我们的项目想像成如下的一个场景,修改用户的信息:
如果我们的项目中只需要这一种交互场景,那么定义 userName() 就够了。这个时候 我们把 userName 传递给 createStore
上面的代码在执行
createStore(userName)
时,内部执行一次dispatch(INIT_ACTION)
,从而在 dispatch 方法内部调用了userName({}, undefined)
。所以打印的结果是一个空对象。如果交互场景比较多的时候呢,一个
reducer
肯定不够用啊,那么这个时候我们可能会定义多个类似userName
这个的reducer
函数,所以我们还需要定义一个工具函数combineReducers
,将多个reducer
函数组合成一个reducer
函数。结合之前的
createStore
,我们看看下面的 demo:上面的
reducer
是userName, menu
的一个组合体,所以每次调用dispatch(action)
时,都会遍历所有的reducers
。还有一个很重要的地方就是,每个reducer
函数在没有匹配到action.type
时,必须把reducer()
的参数state
作为返回值,否则就报错。applyMiddleware
reduxjs
还有一个非常厉害的功能,就是可以利用中间件,做很多事情。比如说,我们比较常用的redux-thunk、redux-logger
等。到这里为止,reduxjs 就基本实现了。但是我们的探讨还没有结束,继续往下看
从上面的代码我们可以看出来,
applyMiddlyWare
函数其实就是对createStore
的一层封装,最终输出的dispatch
是经过中间件改造过的。现在我们来看看这个dispatch
到底是什么,它和我们传入的中间件有什么关系???中间件原理
上面的两行代码,先遍历执行中间件,再将变量
chain
传递给compose
函数。所以我们应该可以猜测到,表达式middleware({ dispatch: disp, getState })
应该返回一个函数,不然compose
中的reduce
就没有办法执行了。这里还要考虑到中间件执行的策略,所有的中间件必须串联起来,挨个往下执行。所以中间件应该还应该接收另一个中间件作为参数。所以现在我们可以大致的猜测到一个中间件应该是这样的:
这个时候其实中间件的模型还不够完整,少了一些东西。少了什么了,就是
action
呀!applyMiddlyWare
函数通过中间件对dispatch
进行改造。所以还是要接收action
才能对state
进行修改。所以这下我们清楚了改造后的 dispatch 具体是个啥
现在我们清楚了中间件的模型了,可以来专门研究一下
applyMiddlyWare
函数返回的dispatch
是啥玩意了我们把
compose(one, two, three)(dispatch)
这段代码用我们自己的代码实现一下,大致就是下面这样的效果:当调用
fn(dispatch)
时,three.apply(null, dispatch)
开始执行,返回一个three_
函数。继续往下执行。first(three_)
开始执行,然后执行two.apply(null, three_)
,two
执行完成,返回一个two_
函数。继续往下执行。one(two_)
开始执行,并返回一个one_
函数,这个函数最终作为fn(dispatch)
执行的最终结果,并赋值给变量disp
。disp(action)
执行时,先调用one_(action)
然后是two_(action)
最后是three_(action)
。注意最后一个中间件接收的参数不是中间件参数了,而是原始的dispatch
方法。所以会在最后一个中间件中执行dispatch(action)
,从而调用 rducer 函数修改数据源【state】。执行
disp({data: 1200, type: 'username'})
这段代码,看下打印的结果是啥这下我们就非常清楚了,原来经过
applyMiddlyWare
改造后输出的dispatch
方法,在调用时,会挨个执行每一个传入applyMiddlyWare
函数的中间件,并在最后一个中间件中调用原始的dispatch()
方法。最后自己实现一个 reduxjs 的应用
中间件定义
effects 方法定义
初始化 state, 绑定到 DOM
最后,要过年了,祝大家新年快乐。