React笔记(七) - 手写redux

来吧, 手写redux, 本篇博客针对于对redux有使用经验想交流原理的同学和朋友, 如果你对redux尚未了结, 笔者建议你先去看看redux的基本使用

目录:

  1. createStore的实现
  2. bindActionCreator的实现
  3. combineReducers的实现
  4. applyMiddleWare的实现

createStore的实现

初始化数据仓库

要实现createStore, 我们先要知道createStore的工作流程和实现效果

  1. createStore接收三个参数

    • reducer(必填)

      这个没什么好说的, redux相关的reducer处理函数

    • defaultState(可选)

      仓库默认值, 不能为函数

    • enhanced(可选)

      redux增强中间件

  2. createStore返回一个对象, 对象中包含如下属性

    • dispatch

      分发一个action

    • getState

      拿到当前数据仓库状态

    • subcribe

      注册一个监听器, 监听器是一个无参函数, 该函数会在dispatch分发一个action以后执行, 执行完毕以后返回一个函数, 用于取消监听

功能大概有了解, 那我们就开始吧

在工程的src目录下新建一个目录redux

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HVGCR9ZQ-1595036610185)(./blogImages/目录结构.png)]

在redux目录下新建一个createStore.js, 我们知道createStore.js接收reducerdefaultState作为前两个参数(第三个参数我们先不考虑), 而且会返回一个对象, 对象中存在几个方法, 所以我们第一步先把结构撘出来

// createStore.js
// 我们先考虑两个参数的情况, 第三个参数等到applyMiddleWare的时候在回来加上
export default function(reducer, defaultState) {
    // 保存一下变量
    let curReducer = reducer; 
    let curState = defaultState; 
    const listeners = []; // 所有通过subcribe订阅的函数都会放在这个数组中

    function dispatch(action) {}
    function getState() {}
    function subcribe(listener){}

    // 将三个函数丢出去
    return {
        dispatch,
        getState,
        subcribe
    }
}

然后我们来一个函数一个函数的实现, 首先实现就是dispatch, 我们知道dispatch接收的参数action必须是一个平面对象, 所以我们得先写一个辅助函数用来判定一个对象是否为平面对象(符合单一职责原则), 在redux目录下新建一个util.js, 用来存放我们在redux库中要用到的一些工具函数

// util.js
// 判断一个对象是否是平面对象, 是的话返回true, 否则返回false
export function isPlainObject(obj) {
    if(Object.getPrototypeOf(obj) === Object.prototype) {
        return true;
    }else return false
}

有了上面的isPlainObject辅助, 我们的dispatch函数就好写多了, 同时我们知道dispatch接收的参数action必须有值, 所以我们也要对action进行校验

// createStore.js
import { isPlainObject } from './util.js'; // 引入isPlainObject

export default function(reducer, defaultState) {
    ...
    function dispatch(action) {
        if(!isPlainObject(action)) {
            // 如果不是平面对象直接抛出错误
            throw new Error('action must be a plain object');
        }

        if(action.type === undefined) {
            throw new Error('action must have property of type');
        }
    }
    ...
}

前两层校验过了以后, 我们就要真正做事了,就是触发reducer并更改当前的状态, 然后我们要执行所有放在listeners中的函数, 因为我们知道在subcribe中订阅的监听器都会在数据一改变就马上执行

// createStore.js
import { isPlainObject } from './util.js'; // 引入isPlainObject

export default function(reducer, defaultState) {
    ...
    function dispatch(action) {
        ...
        // 将当前状态和action丢给reducer执行并将返回值重新赋值给当前状态
        curState = curReducer(curState, action);

        // 执行listeners中的方法
        listeners.forEach(listener => typeof listener === 'function' && listener());
    }
    ...
}

这个时候disptach函数已经基本OK了, 我们来写getState函数, 这哥们非常的简单啊, 不就是返回当前状态

...
export default function(reducer, defaultState) {
    ...
    function dispatch(action) {
        ...
    }

    function getState() {
        // 直接将当前状态丢出去就可
        return curState;
    }
    ...
}
...

还有一个subcribe方法, 这哥们接收一个函数作为参数, 用来设置监听器, 并且返回一个函数用来移除当前监听器, 写法也非常的简单

...
export default function(reducer, defaultState) {
    ...
    function dispatch(action) {
        ...
    }

    function getState() {
       ...
    }

    function subcribe(lisenter) {
        if(typeof listener !== 'function') {
            // 如果传入的listener不是一个函数直接报错
            throw new Error('listener must be a function'); 
        }

        listeners.push(lisenter); // 如果是一个函数则推入lisenters数组

        // 返回一个函数用来移除监听器
        return () => {
           const curIndex = lisenters.findIndex(listener); 
           if(curIndex === -1) return;
           lisenters.splice(index, 1); 
        }
    }
    ...
}
...

还有一个小细节需要注意, 我们知道reducer会在一开始就被执行一次, 且传递的action type类型为内置的特殊类型, 所以我们也安排上, 我们先在util.js里添加一个生成特殊格式的工具函数

// util.js
// react官方内置的初始化类型为@@redux/INIT + 六位的随机字符串, 所以我们需要先生成字符串
export function getRandomStr(length = 6) {
    return Math.random().toString(36).substr(2, length);
}

// 整整暴露出去的获得特殊类型的方法
export function getSpecailActionTypes(type) {

    const actionTypes = {
        INIT() {
            // 因为react的特殊类型生成的随机字符串还会用.分开, 所以我们还是得待处理一下
            return `@@/redux/INIT${getRandomStr().split('').join('.')}` ;
        }
    }

    return actionTypes[type]();
}

这个时候我们在createStore.js中直接使用就好了

// createStore.js
import { isPlainObject, getSpecailActionTypes } from './util.js';  // 继续引入getSpecailActionTypes
...

export default function(reducer, defaultState) {
    ...
    function dispatch(action) {
        ...
    }
    function getState() {
        ...
    }
    function subcribe(listener){
        ...
    }

    // 在return出去之前直接触发一次特殊的action
    dispatch({
        type: getSpecailActionTypes('INIT')
    })

    // 将三个函数丢出去
    return {
        dispatch,
        getState,
        subcribe
    }
}

就这样我们的createStore方法就写完了? 是不是感觉相当的easy啊, 这个时候我们在redux目录下创建一个index.js, 将createStore在index文件里导出

// index.js
export { default as createStore } from './createStore';

官方真正的createStore还会返回replaceReducer和一个Symbol属性, 但是这两哥们都不太常用, 笔者这里也就不编写这两哥们的源码

bindActionCreator的实现

提供自动dispatch生成器

bindActionCreator增强的功能

  1. bindActionCreator接收两个参数

    • actions: 可以为一个函数或者一个对象, 对象的每一项必须是函数
    • dispatch: actions对应的dispatch分发方法
  2. bindActionCreator返回值取决于actions, actions是什么状态, 返回值是将actions增强后的状态, 增强后的状态可以不用dispatch直接出发action

开肝

首先我们知道, bindActionCreator既然用于增强原来的action功能(通过bindActionCreator包装后的actions可以不用dispatch就直接出发action), 那我们先将增强功能的辅助函数写好, 在redux目录下新增一个bindActionCreator函数

// bindActionCreator函数
export default bindActionCreator(actions, dispatch) {

}

/**
 * @param {*} func 要被增强的函数
 * @param {*} dispatch 当前的分发函数
 * 这哥们返回一个函数表示被增强过的新函数, 返回的函数无非就是会自动dispatch
 */
function getAutoDispatchActionCreator(func, dispatch) {
    // 如果传入的func不是函数则抛出错误
    if(typeof func !== 'function') {
        throw new Error('action creator must be a function');
    }

    // 将参数都收进来, args => 等于要传递给action的payload
    return function(...args) {
        dispatch(func(...args));
    }
}

辅助函数写好了以后, 其实工作就简单了许多, 我们接下来回到bindActionCreator函数中来处理一下逻辑, 如果传入的actions是一个函数, 我们直接返回增强过后的函数就可以了, 如果传入的actions是一个对象, 我们是需要将对象拆开将其中每一个函数都进行增强以后返回一个新对象的, 如又不是对象又不是函数, 报错是最好的选择

// bindActionCreator函数
export default bindActionCreator(actions, dispatch) {
    // 如果是函数, 我们直接利用辅助函数增强并返回
    if(typeof actions === 'function') {
        return getAutoDispatchActionCreator(actions, dispatch);
    }else if(Object.getPrototypeOf(actions) === Object.prototype) {
        // 如果actions为一个对象
        const lastActionCreator = {}; 
        for(let key in actions) {
            lastActionCreator[key] = getAutoDispatchActionCreator(actions[key], dispatch);
        }
        return lastActionCreator;
    }else {
        // 如果既不是对象又不是函数则报错
        throw new Error('unExpected actions creator type');
    }
}
...

OK, 我们的bindActionCreator其实也写完了, 还是挺简单的不难, 最后我们将它在index中导出

// index.js
export { default as createStore } from './createStore';
export { default as bindActionCreator } from './bindActionCreator';

combineReducer的实现

合并reducer

combineReducers做的事情也非常的简单

  1. 接收一个对象作为参数, 对象里为需要合并的reducer

  2. 返回一个新的reducer处理函数

我们先把架子搭起来, 然后其实combineReducers对于传入的对象是有要求的(1. 传入的reducers对象必须是一个平面对象 2. reducers中的每一个reducer必须有初值), 所以我们顺带写一个辅助函数来校验一下传入的对象, 我们同样在redux目录下新建一个combineReducers.js

// combineReducers.js
// 因为要验证是不是平面对象, 所以我们将isPlainObject引入
import { isPlainObject } from './util.js';

/**
 * @param {*} reducers 需要被合并的reducers
 */ 
export default function(reducers) {

}

// 校验reducers的方法
function validateReducers(reducers) {
    // 如果不是平面对象就报错
    if(!isPlainObject(reducers)) {
        throw new Error('reducers must be a plain object'); 
    }
}

我们知道, 除去校验reducers是不是为平面对象以外, 我们还要校验么每个reducer是不是有初始值, react在书写的时候是调用了两个特殊的action来校验, 一次是INIT, 一次是UNKOWN, 如果两次校验有一次返回了undefined, 则抛出错误, 至于为什么要两次, 因为他怕有心的开发者强行利用if else来躲过INIT筛查, 说到这儿, 我们打开工具箱, 找到getSpecailActionTypes在里面补一条内容

// util.js
...
// 整整暴露出去的获得特殊类型的方法
export function getSpecailActionTypes(type) {

    const actionTypes = {
        INIT() {
           ...
        },
        UNKOWN() {
            return `@@/redux/UNKOWN${getRandomStr().split('').join('.')}` ;
        }
    }

    return actionTypes[type]();
}
...

然后我们在combineReducers.js中引入该函数, 进行校验

// combineReducers.js
import { isPlainObject, getSpecailActionTypes } from './util.js';
...
// 校验reducers的方法
function validateReducers(reducers) {
    // 如果不是平面对象就报错
    if(!isPlainObject(reducers)) {
        throw new Error('reducers must be a plain object'); 
    }

    // 校验每个reducer有无初值
    for(let reducer of reducers) {
        if(reducers.hasOwnProperty(reducer)) {
            // 开始第一次校验, 传入INIT
            const firstResp = reducer(undefined, {
                type: getSpecailActionTypes('INIT')
            })

            // 校验失败就抛出错误
            if(firstResp === undefined) {
            throw new Error("reducer can't return undefined");
            }

            // 开始第二次校验, 传入UNKOWN
            const secResp = reducer(undefined, {
                type: getSpecailActionTypes('UNKOWN')
            })
            if(firstResp === undefined) {
            throw new Error("reducer can't return undefined");
            }
        }
    }
}
...

辅助校验方法写完了, 我们回到主函数中, 在主函数中, 我们需要调用一次校验的方法, 如果校验通过, 我们就返回一个新的reducer出去, 新的reducer应该要拿到reducers中每个reducer的状态保存进一个对象, 这里可能有点绕, 笔者建议你多看几遍多拆分步骤理解

// combineReducers.js
export default function(reducers) {
    // 直接开启校验, 校验没有通过都给你直接报错了, 没报错代表校验通过
    validateReducers(reducers); 



    return function(state, action) {
        // 待会要丢出去的对象, 因为别人调用reducer就是要修改状态, 并返回新的状态
        const lastState = {}; 
        
        for(let prop in reducers) {
             if(reducers.hasOwnProperty(prop)) {
                 const curReducer = reducers[prop]; // 当前循环中的reducer
                // 将当前的curReduer执行, state[prop] => 代表返回出去的reducer中的某个属性, 而这个属性就是当前循环中的prop, 第一次会获得初值
                lastState[prop] = curReducer(state[prop], action);

            }
        }
        
        return lastState; // 丢出去

    }
}

这里最难理解的就是lastState[prop] = curReducer(state[prop], action);这一句代码了, 你一定要该清楚里面的每一个变量现在代表的都是什么含义, 然后走一遍你就会清晰很多, 至此我们的combineReducers也写完了, 我们到index.js中将他导出

// index.js
export { default as createStore } from './createStore';
export { default as bindActionCreator } from './bindActionCreator';
export { default as combineReducers } from './combineReducers';

applyMiddleWare的实现

中间件

applyMiddleWare:

  1. 这哥们接受的参数不限制个数, 所有的参数均为中间件函数

    每个中间件的本质就是一个函数, 函数执行以后可以得到一个dispatch创建函数

  2. 这哥们返回一个函数, 该函数为创建数据仓库的函数createStoreHanlder

  3. createStoreHandler接受一个参数为创建仓库的真实方法createStore, 并返回一个函数lastCreateStore, 用于创建仓库

  4. lastCreateStore函数跟createStore方法的参数是完全一致的, 且返回值也跟createStore高度一致, 只是在返回值之前, 我们会对createStore中的dispatch方法进行修改

注意: 上面的流程如果你没学习过redux的这部分功能的话可能不知道咋回事, 或者你理解不那么深, 笔者这里给你整个流程图, 希望可以帮助到你

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNKlXkMF-1595036610187)(./blogImages/中间件流程.png)]

我们来把applyMiddleWare的基本结构搭一下, 免得你看着文字懵, 在redux目录下新建一个applyMiddleWare.js

// applyMiddleWare.js

// 最外层的函数接收的参数不限制个数, 全部为中间件处理函数, 所以我们用rest运算符将他们都收集起来
export default function(...midlleWares) {
    // 返回一个函数, 返回的函数接收一个参数为创建仓库的函数, 因为applyMiddleWare自己是不会创建仓库的
     return function(createStore) {
         // 最后返回出去的函数就是我们用来真正处理仓库的函数了, 接收的参数我们也会给createStore去用
         return function(reducer, defaultState) {
            // 调用第二个函数接收到的createStore来创建仓库
            const store = createStore(reducer, defaultState);
            // 我们先给dispatch函数一个重写值, 这个也是我们最终要丢出去的dispatch
            let dispatch = () => { throw new Error('dispatch can not be used at this moment') }; 
            /// ... 中间这里我们就要处理中间件, 整个处理流程也就是如上图我画的一样

            // 这个函数的返回值就是新的dispatch了
            return {
                ...store,
                dispatch
            }
         }
     }   

}

我们知道我们拿到的每一个中间件, 真正需要的不是这个中间件, 是需要中间件执行以后提供的dispatch创建函数, 所以我们必须执行每一个中间件函数并获得他们返回的dispatch创建函数, 同时我们也知道该dispatch创建函数的功能有限, 他只能使用getState和dispatch两个方法, 所以我们并不需要将store的所有方法都传递给他

// applyMiddleWare.js
export default function(...midlleWares) {
     return function(createStore) {
         return function(reducer, defaultState) {
             ...
            /// ... 中间这里我们就要处理中间件, 整个处理流程也就是如上图我画的一样
            
            // 得到简易的store对象
            const simpleStore = {
                dispatch: store.dispatch,
                getState: store.getState
            }

            // 获得dispatchProducer
            const disptachProducer = middleWares.map(it => it(simpleStore));

            ...
         }
     }   

}

接下来我们要做的就是反向的去调用所有的dispatch创建方法, 为啥要反着调用? 还是上面的图, 因为我们在在将最后一个中间件3包装以后丢出去的dispatch是中间件3包装以后的, 所以我们在执行的时候会直接先走中间件包装过的dispatch, 我们必须保证第一个执行的是中间件1包装过的dispatch, 所以必须反着包装, 所以这里我们要用到一丢丢的函数组合的概念, 新建一个compose.js在redux目录下

// compose.js

export default function compose(...funcs) {

    if(funcs.length === 0) {
        return args => args; // 如果没有要组合的函数, 返回的函数直接原封不动的返回参数
    }else if(funcs.length === 1) {
        return funcs[0]; // 如果只有一个函数的情况
    }

    return function(...args) {
        let lastReturnValue = null; // 记录上一个函数的返回值
        for(let i = funcs.length - 1; i >= 0; i--) {
            const curFunc = func[i];
            if(i === funcs.length - 1) {
                // 进了这个判断代表数组的最后一项
                lastReturnValue = curFunc(...args);
            }else {
                lastReturnValue = curFunc(lastReturnValue);
            }
        }
        return lastReturn;
    }
}

如果你的基础够好, 我们的compose函数可以写成下面这种简化版

// compose.js

export default function compose(...funcs) {

    if(funcs.length === 0) {
        return args => args; // 如果没有要组合的函数, 返回的函数直接原封不动的返回参数
    }else if(funcs.length === 1) {
        return funcs[0]; // 如果只有一个函数的情况
    }

    // 这一句话需要你好好的去理解reduce的功能
    return funcs.reduce((fstFunc, secFunc) => (...args) => fstFunc(secFunc(...args)));
}

OK, compose写完了以后我们在applyMiddleWare中引入并进行调用, 我们将dispatchProducer数组放进compose方法中, compose就是返回了一个新的函数, 我们将新的函数丢进最后的返回值中, 是不是就等于中间件都应用上啦

// applyMiddleWare.js
import compose from './compose';
export default function(...midlleWares) {
     return function(createStore) {
         return function(reducer, defaultState) {
             ...
            const disptachProducer = middleWares.map(it => it(simpleStore));
            // 我们之前是定义了一个dispatch的, 并且最后将这个dispatch丢出去了, 所以我们直接赋值给dispatch
            dispatch = compose(...dispatchProducer)(store.dispatch); 
            ...
         }
     }   
}

这个时候applyMiddleWare.js就写完了, 这个函数很简单, 但是你必须知道每一步到底是在干嘛, 这些函数的返回值最好都打印出来看一看, 你会很容易明白的, 我们将applyMiddleWare导出

// index.js
export { default as createStore } from './createStore';
export { default as bindActionCreator } from './bindActionCreator';
export { default as combineReducers } from './combineReducers';
export { default as applyMiddleWare } from './applyMiddleWare';

别忘了, 我们还要修改一下createStore这个函数, 我们之前只考虑了两个参数的createStore, 现在applyMiddleWare写好了, 所以我们要考虑第三个参数的情况了, createStore是这样子的,如果你第二个参数是一个函数, 那他就会直接当成applyMiddleWare返回的函数, 这些用法就不用多说了吧, 只是稍微提示一下这儿

// createStore.js
import { isPlainObject, getSpecailActionTypes } from './util.js';  // 继续引入getSpecailActionTypes
...
// 加入第三个参数enhanced
export default function createStore(reducer, defaultState, enhanced) {
    // 我们要判断一下第二个参数是不是函数
    if(typeof defaultState === 'function') {
        // 如果是函数,就进行如下操作
        enhanced = defaultState;
        defaultState = undefined;
    }

    // 这里还要判断一下enhanced是不是函数, 为什么呢, 因为第二个参数的话他一定是函数, 第三个我们也要判断确认一下啊
    if(typeof enhanced === 'function') {
        // 进入applyMiddleWare的处理逻辑
        return enhanced(createStore)(reducer, defaultState); // enhanced是applyMiddleWare执行过后返回的函数, 所有这样写你应该看得懂吧
    }
    
    ...
    // 将三个函数丢出去
    return {
        dispatch,
        getState,
        subcribe
    }
}

至此, 咱们的redux已经是全部写完了, 赶紧测试一下吧, 笔者因为懒惰, 就不测试了~, 如果有问题及时联系, 最后的项目目录如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-23xrRr9G-1595036610189)(./blogImages/最终目录结构.png)]

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/107424290