手写React全家桶 - Redux篇

234 阅读5分钟

咋们一步一步来,先搞他个mini-redux

中文文档

1. mini版实现

1.1 通过原库实现

思路:先使用一个简单的页面,使用Redux和React搭建起来,然后再自己实现对应的函数。

page

import React, { Component } from 'react';
import store from '../store';

export default class ReduxPage extends Component {
  // 如果点击按钮不能更新,查看是否订阅(subscribe)状态变更。
  componentDidMount() {
    this.unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }

  componentWillUnmount() {
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }

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

  minus = () => {
    store.dispatch({ type: 'MINUS' });
  };

  render() {
    return (
      <div style={{ padding: 24 }}>
        <h3>ReduxPage</h3>
        <p>{store.getState()}</p>
        <button type="button" onClick={this.add}> + add</button>
        <button type="button" onClick={this.minus}> - minus</button>
      </div>
    );
  }
}

store

import { createStore } from 'redux';

function countReducer(state = 0, action) {
  switch (action.type) {
  case 'ADD':
    return state + 1;
  case 'MINUS':
    return state - 1;
  default:
    return state;
  }
}

const store = createStore(countReducer);

export default store;

效果

1.2 分析

点击按钮 => 执行store.dispatch函数 (参数只能支持对象) => store由redux的countReducer方法创建 (参数就是我们定义的reducer纯函数) => 页面通过store.getState()获取到store的数据 => 想要页面更新,必须使用store.subscribe订阅状态变更 => store数据改变后会触发subscribe,然后页面调用forceUpdate就更新页面了

总结:redux需要下面的函数

  1. dispatch 提交更新
  2. createStore 创建store
  3. getState 获取状态值
  4. subscribe 变更订阅

1.3 createStore

调用这个函数返回store,store里面包含dispatch,subscribe,getState

// function countReducer(state = 0, action) {
//   switch (action.type) {
//   case 'ADD':
//     return state + 1;
//   case 'MINUS':
//     return state - 1;
//   default:
//     return state;
//   }
// }
// const store = createStore(countReducer); 传入了定义的reducer
export default function createStore(reducer) {
  let currentState;
  const currentListeners = [];
  function getState() {
    return currentState;
  }

  // store.dispatch({ type: 'ADD' }); 传入了一个action
  function dispatch(action) {
    // 执行一下reducer
    currentState = reducer(currentState, action);
    // 数据更新后,把所有的listener执行一遍
    currentListeners.forEach(listener => listener());
  }

  // 传入了一个回调函数,数据更新了,就执行这个回调
  function subscribe(listener) {
    currentListeners.push(listener);
  }

  // 初始值,手动发一个dispatch,为避免和用户创建的一样,源码里面采用了随机字符串。
  dispatch({ type: 'REDUX/MIN_REDUX' });

  return {
    subscribe,
    getState,
    dispatch
  };
}

然后把咋们store里面引入redux的位置替换为我们的createStore就完成了。

import { createStore } from 'redux'; // 替换下store中createStore函数路径

2. 进阶 - 中间件

异步数据流

默认情况下,createStore() 所创建的 Redux store 没有使用 middleware,所以只支持 同步数据流

你可以使用 applyMiddleware() 来增强 createStore()

2.1 使用中间件

咋们在page页里面添加两个按钮并绑定事件

asyncAdd = () => {
    store.dispatch((dispatch) => {
        setTimeout(() => {
            dispatch({ type: 'ADD' });
        }, 1000);
    });
}
promiseMinus = () => {
    store.dispatch(Promise.resolve({
        type: 'MINUS',
        payload: 1000
    }));
}

<button style={{ margin: '0 8px' }} type="button" onClick={this.asyncAdd}> + async add</button>

<button style={{ margin: '0 8px' }} type="button" onClick={this.promiseMinus}> - promise minus</button>

上面代码中,dispatch中我们传入了一个函数,以及Promise。

store中引入redux-promise支持promise, redux-thunk支持异步数据, redux-logger打印store数据变更日志。

import { applyMiddleware, createStore } from 'redux';
import promise from 'redux-promise';
import thunk from 'redux-thunk';
import logger from 'redux-logger';

const store = createStore(countReducer, applyMiddleware(thunk, logger, promise));

效果如下:

2.2 applyMiddleware

由上面的用法,我们可以看出,applyMiddleware支持传入多个中间件,作用为增强store的功能,使dispatch支持Promise和异步数据流。

createStore函数目前支持两个参数了,但是我们上面写的代码只支持一个参数。添加下面的代码,如果有第二个参数就把第二个参数执行了,我们需要增加store所有传入了参数store,然后执行的时候,需要用到reducer。

export default function createStore(reducer, enhancer) {
  if (enhancer) {
    return enhancer(createStore)(reducer);
  }
  ......
}

applyMiddleware实现

import compose from './compose';

export default function applyMiddleware(...middlewares) {
  // createStore就是我们实现的createStore函数,支持两个参数,一个参数是reducer,第二个参数是enhancer
  // args就是我们执行第二个参数enhancer时传入参数 => reducer, 这里具体的就是countReducer
  return createStore => (...args) => {
    // 柯里化,执行一下只传入第一个参数的createStore,我们就获取到了store,store里面有dispatch
    const store = createStore(...args);
    let { dispatch } = store;
    // 获取到dispatch后,我们需要对它进行增强
    const middlewareAPI = {
      getState: store.getState,
      // disptch原本有哪些参数,都传进去
      dispatch: (...params) => dispatch(...params)
    };
    // 传入每个中间件都需要的getState和需要加强的dispatch
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    // 执行每一个中间件就ok了,利用到了我们上面写的compse函数。
    // 这里有点绕,需要弄懂compose函数,到底在干啥。
    // applyMiddleware(thunk, logger, promise)使用的时候,我们传入了3个中间件
    // 使用compose 等价于 thunk(logger(promise(dispatch)))
    // 第一次执行promise,返回一个回调函数, 第二次执行logger同样返回一个回调函数,最后执行完thunk时,才会执行外部的真正的回调函数
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      // 返回增强后的dispatch
      dispatch
    };
  };
}
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)));
}

2.3 redux-logger

先来实现一个最简单的redux-logger, 打印store的变化

// const chain = middlewares.map(middleware => middleware(middlewareAPI));
// 在上面的代码中,我们传入了middlewareAPI,解构获取到getState
export default function logger({ getState }) {
  // compose聚合函数中,执行reduce的时候,fn1和f2,next就是f1。
  return next => action => {
    console.log('***********************************');
    console.log(`action ${action.type} @ ${new Date().toLocaleString()}`);
    // 执行一下getState(),获取当前的state。
    const prevState = getState();
    console.log('prev state', prevState);

    // 执行dispatch
    const returnValue = next(action);
    // 获取到执行后的state
    const nextState = getState();
    console.log('next state', nextState);

    console.log('***********************************');
    return returnValue;
  };
}

2.4 redux-thunk

export default function thunk({ getState, dispatch }) {
  return next => action => {
    // 如果dispatch传入的是一个函数,那么执行这个函数,执行完这个函数后,返回一个函数,就进入回调了
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }
    return next(action);
  };
}

2.5 redux-promise

import isPromise from 'is-promise';

// 简版
export default function promise({ dispatch }) {
  return next => action => (isPromise(action) ? action.then(dispatch)
    : next(action));
}

3. combineReducers

项目中不可能只有一个Reducer,这个时候就需要combineReducers来把所有Reducer合并为一个了。

3.1 用法

page

addTodo = () => {
    store.dispatch({ type: 'ADD_TODO', payload: ' world!' });
}

<h2>ReduxPage</h2>
<h4>Count</h4>
<p>{store.getState().count}</p>

<h4 style={{ marginTop: '24px' }}>Todo</h4>
<p>{store.getState().todos}</p>
<button type="button" onClick={this.addTodo}>添加todo</button>

store

import { applyMiddleware, createStore, combineReducers } from 'redux';

function todoReducer(state = ['hello'], action) {
  switch (action.type) {
  case 'ADD_TODO':
    return state.concat([action.payload]);
  default:
    return state;
  }
}

// 只有一个reducer
// const store = createStore(countReducer, applyMiddleware(thunk, logger, promise));

// 多个reducer合并
const store = createStore(
  combineReducers({
    count: countReducer,
    todos: todoReducer
  }),
  applyMiddleware(thunk, logger, promise)
);

3.2 实现

export default function combineReducers(reducers) {
  return function combination(state = {}, action) {
    const nextState = {};
    let hasChanged = false;
    Object.keys(reducers).forEach(key => {
      const reducer = reducers[key];
      nextState[key] = reducer(state[key], action);
      hasChanged = hasChanged || nextState[key] !== state[key];
    });
    // 源码里面提供了replaceReducer,
    // replaceReducer => {a: 0, b: 1} 替换为了{a: 0}
    // 所以需要比较两次的length发生变化没有
    hasChanged = hasChanged || Object.keys(nextState).length !== Object.keys(state).length;
    return hasChanged ? nextState : state;
  };
} 

4.bindActionCreator

配合react-redux使用,文档

实现

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args));
}

// 用法,给creators添加dispatch
// let creators = {
//   add: () => ({ type: 'ADD' }),
//   minus: () => ({ type: 'MINUS' })
// };
// creators = bindActionCreators(creators, dispatch);
export default function bindActionCreators(actionCreators, dispatch) {
  const boundActionCreators = {};
  Object.keys(actionCreators).forEach(key => {
    const actionCreator = actionCreators[key];
    boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
  });
}

结尾

仓库地址:github.com/claude-hub/… , 求star~。