Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

完全理解 redux(从零实现一个 redux) #22

Open
brickspert opened this issue Nov 11, 2018 · 105 comments
Open

完全理解 redux(从零实现一个 redux) #22

brickspert opened this issue Nov 11, 2018 · 105 comments

Comments

@brickspert
Copy link
Owner

brickspert commented Nov 11, 2018

完全理解 redux

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

目录

  1. 前言
  2. 状态管理器
    • 简单的状态管理器
    • 有计划的状态管理器
  3. 多文件协作
    • reducer 的拆分和合并
    • state 的拆分和合并
  4. 中间件 middleware
  5. 完整的 redux
  6. 最佳实践
    • 纯函数
  7. 总结

前言

记得开始接触 react 技术栈的时候,最难理解的地方就是 redux。全是新名词:reducer、store、dispatch、middleware 等等,我就理解 state 一个名词。

网上找的 redux 文章,要不有一本书的厚度,要不很玄乎,晦涩难懂,越看越觉得难,越看越怕,信心都没有了!

花了很长时间熟悉 redux,慢慢的发现它其实真的很简单。本章不会把 redux 的各种概念,名词解释一遍,这样和其他教程没有任何区别,没有太大意义。我会带大家从零实现一个完整的 redux,让大家知其然,知其所以然。

开始前,你必须知道一些事情:

  • redux 和 react 没有关系,redux 可以用在任何框架中,忘掉 react。
  • connect 不属于 redux,它其实属于 react-redux,请先忘掉它,下一章节,我们会介绍它。
  • 请一定先忘记 reducer、store、dispatch、middleware 等等这些名词。
  • redux 是一个状态管理器。

Let's Go!

状态管理器

简单的状态管理器

redux 是一个状态管理器,那什么是状态呢?状态就是数据,比如计数器中的 count。

let state = {
  count: 1
}

我们来使用下状态

console.log(state.count);

我们来修改下状态

state.count = 2;

好了,现在我们实现了状态(计数)的修改和使用了。

读者:你当我傻吗?你说的这个谁不知道?捶你👊!

笔者:哎哎哎,别打我!有话好好说!redux 核心就是这个呀!我们一步一步扩展开来嘛!

当然上面的有一个很明显的问题:修改 count 之后,使用 count 的地方不能收到通知。我们可以使用发布-订阅模式来解决这个问题。

/*------count 的发布订阅者实践------*/
let state = {
  count: 1
};
let listeners = [];

/*订阅*/
function subscribe(listener) {
  listeners.push(listener);
}

function changeCount(count) {
  state.count = count;
  /*当 count 改变的时候,我们要去通知所有的订阅者*/
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i];
    listener();
  }
}

我们来尝试使用下这个简单的计数状态管理器。

/*来订阅一下,当 count 改变的时候,我要实时输出新的值*/
subscribe(() => {
  console.log(state.count);
});

/*我们来修改下 state,当然我们不能直接去改 state 了,我们要通过 changeCount 来修改*/
changeCount(2);
changeCount(3);
changeCount(4);

现在我们可以看到,我们修改 count 的时候,会输出相应的 count 值。

现在有两个新的问题摆在我们面前

  • 这个状态管理器只能管理 count,不通用
  • 公共的代码要封装起来

我们尝试来解决这个问题,把公共的代码封装起来

const createStore = function (initState) {
  let state = initState;
  let listeners = [];

  /*订阅*/
  function subscribe(listener) {
    listeners.push(listener);
  }

  function changeState(newState) {
    state = newState;
    /*通知*/
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }

  return {
    subscribe,
    changeState,
    getState
  }
}

我们来使用这个状态管理器管理多个状态 counter 和 info 试试

let initState = {
  counter: {
    count: 0
  },
  info: {
    name: '',
    description: ''
  }
}

let store = createStore(initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(`${state.info.name}${state.info.description}`);
});
store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count);
});

store.changeState({
  ...store.getState(),
  info: {
    name: '前端九部',
    description: '我们都是前端爱好者!'
  }
});

store.changeState({
  ...store.getState(),
  counter: {
    count: 1
  }
});

到这里我们完成了一个简单的状态管理器。

这里需要理解的是 createStore,提供了 changeStategetStatesubscribe 三个能力。

本小节完整源码见 demo-1

有计划的状态管理器

我们用上面的状态管理器来实现一个自增,自减的计数器。

let initState = {
  count: 0
}
let store = createStore(initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(state.count);
});
/*自增*/
store.changeState({
  count: store.getState().count + 1
});
/*自减*/
store.changeState({
  count: store.getState().count - 1
});
/*我想随便改*/
store.changeState({
  count: 'abc'
});

你一定发现了问题,count 被改成了字符串 abc,因为我们对 count 的修改没有任何约束,任何地方,任何人都可以修改。

我们需要约束,不允许计划外的 count 修改,我们只允许 count 自增和自减两种改变方式!

那我们分两步来解决这个问题

  1. 制定一个 state 修改计划,告诉 store,我的修改计划是什么。
  2. 修改 store.changeState 方法,告诉它修改 state 的时候,按照我们的计划修改。

我们来设置一个 plan 函数,接收现在的 state,和一个 action,返回经过改变后的新的 state。

/*注意:action = {type:'',other:''}, action 必须有一个 type 属性*/
function plan(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1
      }
    default:
      return state;
  }
}

我们把这个计划告诉 store,store.changeState 以后改变 state 要按照我的计划来改。

/*增加一个参数 plan*/
const createStore = function (plan, initState) {
  let state = initState;
  let listeners = [];

  function subscribe(listener) {
    listeners.push(listener);
  }

  function changeState(action) {
    /*请按照我的计划修改 state*/  
    state = plan(state, action);
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }

  return {
    subscribe,
    changeState,
    getState
  }
}

我们来尝试使用下新的 createStore 来实现自增和自减

let initState = {
  count: 0
}
/*把plan函数*/
let store = createStore(plan, initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(state.count);
});
/*自增*/
store.changeState({
  type: 'INCREMENT'
});
/*自减*/
store.changeState({
  type: 'DECREMENT'
});
/*我想随便改 计划外的修改是无效的!*/
store.changeState({
  count: 'abc'
});

到这里为止,我们已经实现了一个有计划的状态管理器!

我们商量一下吧?我们给 plan 和 changeState 改下名字好不好?**plan 改成 reducer,changeState 改成 dispatch!**不管你同不同意,我都要换,因为新名字比较厉害(其实因为 redux 是这么叫的)!

本小节完整源码见 demo-2

多文件协作

reducer 的拆分和合并

这一小节我们来处理下 reducer 的问题。啥问题?

我们知道 reducer 是一个计划函数,接收老的 state,按计划返回新的 state。那我们项目中,有大量的 state,每个 state 都需要计划函数,如果全部写在一起会是啥样子呢?

所有的计划写在一个 reducer 函数里面,会导致 reducer 函数及其庞大复杂。按经验来说,我们肯定会按组件维度来拆分出很多个 reducer 函数,然后通过一个函数来把他们合并起来。

我们来管理两个 state,一个 counter,一个 info。

let state = {
  counter: {
    count: 0
  },
  info: {
    name: '前端九部',
    description: '我们都是前端爱好者!'
  }
}

他们各自的 reducer

/*counterReducer, 一个子reducer*/
/*注意:counterReducer 接收的 state 是 state.counter*/
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1
      }
    default:
      return state;
  }
}
/*InfoReducer,一个子reducer*/
/*注意:InfoReducer 接收的 state 是 state.info*/
function InfoReducer(state, action) {
  switch (action.type) {
    case 'SET_NAME':
      return {
        ...state,
        name: action.name
      }
    case 'SET_DESCRIPTION':
      return {
        ...state,
        description: action.description
      }
    default:
      return state;
  }
}

那我们用 combineReducers 函数来把多个 reducer 函数合并成一个 reducer 函数。大概这样用

const reducer = combineReducers({
    counter: counterReducer,
    info: InfoReducer
});

我们尝试实现下 combineReducers 函数

function combineReducers(reducers) {

  /* reducerKeys = ['counter', 'info']*/
  const reducerKeys = Object.keys(reducers)

  /*返回合并后的新的reducer函数*/
  return function combination(state = {}, action) {
    /*生成的新的state*/
    const nextState = {}

    /*遍历执行所有的reducers,整合成为一个新的state*/
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i]
      const reducer = reducers[key]
      /*之前的 key 的 state*/
      const previousStateForKey = state[key]
      /*执行 分 reducer,获得新的state*/
      const nextStateForKey = reducer(previousStateForKey, action)

      nextState[key] = nextStateForKey
    }
    return nextState;
  }
}

我们来尝试下 combineReducers 的威力吧

const reducer = combineReducers({
  counter: counterReducer,
  info: InfoReducer
});

let initState = {
  counter: {
    count: 0
  },
  info: {
    name: '前端九部',
    description: '我们都是前端爱好者!'
  }
}

let store = createStore(reducer, initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count, state.info.name, state.info.description);
});
/*自增*/
store.dispatch({
  type: 'INCREMENT'
});

/*修改 name*/
store.dispatch({
  type: 'SET_NAME',
  name: '前端九部2号'
});

本小节完整源码见 demo-3

state 的拆分和合并

上一小节,我们把 reducer 按组件维度拆分了,通过 combineReducers 合并了起来。但是还有个问题, state 我们还是写在一起的,这样会造成 state 树很庞大,不直观,很难维护。我们需要拆分,一个 state,一个 reducer 写一块。

这一小节比较简单,我就不卖关子了,用法大概是这样(注意注释)

/* counter 自己的 state 和 reducer 写在一起*/
let initState = {
  count: 0
}
function counterReducer(state, action) {
  /*注意:如果 state 没有初始值,那就给他初始值!!*/  
  if (!state) {
      state = initState;
  }
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      }
    default:    
      return state;
  }
}

我们修改下 createStore 函数,增加一行 dispatch({ type: Symbol() })

const createStore = function (reducer, initState) {
  let state = initState;
  let listeners = [];

  function subscribe(listener) {
    listeners.push(listener);
  }

  function dispatch(action) {
    state = reducer(state, action);
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }
  /* 注意!!!只修改了这里,用一个不匹配任何计划的 type,来获取初始值 */
  dispatch({ type: Symbol() })

  return {
    subscribe,
    dispatch,
    getState
  }
}

我们思考下这行可以带来什么效果?

  1. createStore 的时候,用一个不匹配任何 type 的 action,来触发 state = reducer(state, action)
  2. 因为 action.type 不匹配,每个子 reducer 都会进到 default 项,返回自己初始化的 state,这样就获得了初始化的 state 树了。

你可以试试

/*这里没有传 initState 哦 */
const store = createStore(reducer);
/*这里看看初始化的 state 是什么*/
console.dir(store.getState());

本小节完整源码见 demo-4

到这里为止,我们已经实现了一个七七八八的 redux 啦!

中间件 middleware

中间件 middleware 是 redux 中最难理解的地方。但是我挑战一下用最通俗的语言来讲明白它。如果你看完这一小节,还没明白中间件是什么,不知道如何写一个中间件,那就是我的锅了!

中间件是对 dispatch 的扩展,或者说重写,增强 dispatch 的功能!

记录日志

我现在有一个需求,在每次修改 state 的时候,记录下来 修改前的 state ,为什么修改了,以及修改后的 state。我们可以通过重写 store.dispatch 来实现,直接看代码

const store = createStore(reducer);
const next = store.dispatch;

/*重写了store.dispatch*/
store.dispatch = (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

我们来使用下

store.dispatch({
  type: 'INCREMENT'
});

日志输出为

this state { counter: { count: 0 } }
action { type: 'INCREMENT' }
1
next state { counter: { count: 1 } }

现在我们已经实现了一个完美的记录 state 修改日志的功能!

记录异常

我又有一个需求,需要记录每次数据出错的原因,我们扩展下 dispatch

const store = createStore(reducer);
const next = store.dispatch;

store.dispatch = (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

这样每次 dispatch 出异常的时候,我们都会记录下来。

多中间件的合作

我现在既需要记录日志,又需要记录异常,怎么办?当然很简单了,两个函数合起来呗!

store.dispatch = (action) => {
  try {
    console.log('this state', store.getState());
    console.log('action', action);
    next(action);
    console.log('next state', store.getState());
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

如果又来一个需求怎么办?接着改 dispatch 函数?那再来10个需求呢?到时候 dispatch 函数肯定庞大混乱到无法维护了!这个方式不可取呀!

我们需要考虑如何实现扩展性很强的多中间件合作模式。

  1. 我们把 loggerMiddleware 提取出来

    const store = createStore(reducer);
    const next = store.dispatch;
    
    const loggerMiddleware = (action) => {
      console.log('this state', store.getState());
      console.log('action', action);
      next(action);
      console.log('next state', store.getState());
    }
    
    store.dispatch = (action) => {
      try {
        loggerMiddleware(action);
      } catch (err) {
        console.error('错误报告: ', err)
      }
    }
  2. 我们把 exceptionMiddleware 提取出来

    const exceptionMiddleware = (action) => {
      try {
        /*next(action)*/
        loggerMiddleware(action);
      } catch (err) {
        console.error('错误报告: ', err)
      } 
    }
    store.dispatch = exceptionMiddleware;
  3. 现在的代码有一个很严重的问题,就是 exceptionMiddleware 里面写死了 loggerMiddleware,我们需要让 next(action)变成动态的,随便哪个中间件都可以

    const exceptionMiddleware = (next) => (action) => {
      try {
        /*loggerMiddleware(action);*/
        next(action);
      } catch (err) {
        console.error('错误报告: ', err)
      } 
    }
    /*loggerMiddleware 变成参数传进去*/
    store.dispatch = exceptionMiddleware(loggerMiddleware);
  4. 同样的道理,loggerMiddleware 里面的 next 现在恒等于 store.dispatch,导致 loggerMiddleware 里面无法扩展别的中间件了!我们也把 next 写成动态的

    const loggerMiddleware = (next) => (action) => {
      console.log('this state', store.getState());
      console.log('action', action);
      next(action);
      console.log('next state', store.getState());
    }

到这里为止,我们已经探索出了一个扩展性很高的中间件合作模式!

const store = createStore(reducer);
const next = store.dispatch;

const loggerMiddleware = (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

store.dispatch = exceptionMiddleware(loggerMiddleware(next));

这时候我们开开心心的新建了一个 loggerMiddleware.js,一个exceptionMiddleware.js文件,想把两个中间件独立到单独的文件中去。会碰到什么问题吗?

loggerMiddleware 中包含了外部变量 store,导致我们无法把中间件独立出去。那我们把 store 也作为一个参数传进去好了~

const store = createStore(reducer);
const next  = store.dispatch;

const loggerMiddleware = (store) => (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));

到这里为止,我们真正的实现了两个可以独立的中间件啦!

现在我有一个需求,在打印日志之前输出当前的时间戳。用中间件来实现!

const timeMiddleware = (store) => (next) => (action) => {
  console.log('time', new Date().getTime());
  next(action);
}
...
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));

本小节完整源码见 demo-6

中间件使用方式优化

上一节我们已经完全实现了正确的中间件!但是中间件的使用方式不是很友好

import loggerMiddleware from './middlewares/loggerMiddleware';
import exceptionMiddleware from './middlewares/exceptionMiddleware';
import timeMiddleware from './middlewares/timeMiddleware';

...

const store = createStore(reducer);
const next = store.dispatch;

const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));

其实我们只需要知道三个中间件,剩下的细节都可以封装起来!我们通过扩展 createStore 来实现!

先来看看期望的用法

/*接收旧的 createStore,返回新的 createStore*/
const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);

/*返回了一个 dispatch 被重写过的 store*/
const store = newCreateStore(reducer);

实现 applyMiddleware

const applyMiddleware = function (...middlewares) {
  /*返回一个重写createStore的方法*/
  return function rewriteCreateStoreFunc(oldCreateStore) {
     /*返回重写后新的 createStore*/
    return function newCreateStore(reducer, initState) {
      /*1. 生成store*/
      const store = oldCreateStore(reducer, initState);
      /*给每个 middleware 传下store,相当于 const logger = loggerMiddleware(store);*/
      /* const chain = [exception, time, logger]*/
      const chain = middlewares.map(middleware => middleware(store));
      let dispatch = store.dispatch;
      /* 实现 exception(time((logger(dispatch))))*/
      chain.reverse().map(middleware => {
        dispatch = middleware(dispatch);
      });

      /*2. 重写 dispatch*/
      store.dispatch = dispatch;
      return store;
    }
  }
}

让用户体验美好

现在还有个小问题,我们有两种 createStore 了

/*没有中间件的 createStore*/
import { createStore } from './redux';
const store = createStore(reducer, initState);

/*有中间件的 createStore*/
const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);
const newCreateStore = rewriteCreateStoreFunc(createStore);
const store = newCreateStore(reducer, initState);

为了让用户用起来统一一些,我们可以很简单的使他们的使用方式一致,我们修改下 createStore 方法

const createStore = (reducer, initState, rewriteCreateStoreFunc) => {
    /*如果有 rewriteCreateStoreFunc,那就采用新的 createStore */
    if(rewriteCreateStoreFunc){
       const newCreateStore =  rewriteCreateStoreFunc(createStore);
       return newCreateStore(reducer, initState);
    }
    /*否则按照正常的流程走*/
    ...
}

最终的用法

const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);

const store = createStore(reducer, initState, rewriteCreateStoreFunc);

本小节完整源码见 demo-7

完整的 redux

退订

不能退订的订阅都是耍流浪!我们修改下 store.subscribe 方法,增加退订功能

  function subscribe(listener) {
    listeners.push(listener);
    return function unsubscribe() {
      const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

使用

const unsubscribe = store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count);
});
/*退订*/
unsubscribe();

中间件拿到的store

现在的中间件拿到了完整的 store,他甚至可以修改我们的 subscribe 方法,按照最小开放策略,我们只用把 getState 给中间件就可以了!因为我们只允许你用 getState 方法!

修改下 applyMiddleware 中给中间件传的 store

/*const chain = middlewares.map(middleware => middleware(store));*/
const simpleStore = { getState: store.getState };
const chain = middlewares.map(middleware => middleware(simpleStore));

compose

我们的 applyMiddleware 中,把 [A, B, C] 转换成 A(B(C(next))),是这样实现的

const chain = [A, B, C];
let dispatch = store.dispatch;
chain.reverse().map(middleware => {
   dispatch = middleware(dispatch);
});

redux 提供了一个 compose 方式,可以帮我们做这个事情

const chain = [A, B, C];
dispatch = compose(...chain)(store.dispatch)

看下他是如何实现的

export default function compose(...funcs) {
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

当然 compose 函数对于新人来说可能比较难理解,你只需要他是做什么的就行啦!

省略initState

有时候我们创建 store 的时候不传 initState,我们怎么用?

const store = createStore(reducer, {}, rewriteCreateStoreFunc);

redux 允许我们这样写

const store = createStore(reducer, rewriteCreateStoreFunc);

我们仅需要改下 createStore 函数,如果第二个参数是一个object,我们认为他是 initState,如果是 function,我们就认为他是 rewriteCreateStoreFunc。

function craeteStore(reducer, initState, rewriteCreateStoreFunc){
    if (typeof initState === 'function'){
    rewriteCreateStoreFunc = initState;
    initState = undefined;
  }
  ...
}

2 行代码的 replaceReducer

reducer 拆分后,和组件是一一对应的。我们就希望在做按需加载的时候,reducer也可以跟着组件在必要的时候再加载,然后用新的 reducer 替换老的 reducer。

const createStore = function (reducer, initState) {
  ...
  function replaceReducer(nextReducer) {
    reducer = nextReducer
    /*刷新一遍 state 的值,新来的 reducer 把自己的默认状态放到 state 树上去*/
    dispatch({ type: Symbol() })
  }
  ...
  return {
    ...
    replaceReducer
  }
}

我们来尝试使用下

const reducer = combineReducers({
  counter: counterReducer
});
const store = createStore(reducer);

/*生成新的reducer*/
const nextReducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});
/*replaceReducer*/
store.replaceReducer(nextReducer);

replaceReducer 示例源码见 demo-5

bindActionCreators

bindActionCreators 我们很少很少用到,一般只有在 react-redux 的 connect 实现中用到。

他是做什么的?他通过闭包,把 dispatch 和 actionCreator 隐藏起来,让其他地方感知不到 redux 的存在。

我们通过普通的方式来 隐藏 dispatch 和 actionCreator 试试,注意最后两行代码

const reducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});
const store = createStore(reducer);

/*返回 action 的函数就叫 actionCreator*/
function increment() {
  return {
    type: 'INCREMENT'
  }
}

function setName(name) {
  return {
    type: 'SET_NAME',
    name: name
  }
}

const actions = {
  increment: function () {
    return store.dispatch(increment.apply(this, arguments))
  },
  setName: function () {
    return store.dispatch(setName.apply(this, arguments))
  }
}
/*注意:我们可以把 actions 传到任何地方去*/
/*其他地方在实现自增的时候,根本不知道 dispatch,actionCreator等细节*/
actions.increment(); /*自增*/
actions.setName('九部威武'); /*修改 info.name*/

我眼睛一看,这个 actions 生成的时候,好多公共代码,提取一下

const actions = bindActionCreators({ increment, setName }, store.dispatch);

来看一下 bindActionCreators 的源码,超级简单(就是生成了刚才的 actions)

/*核心的代码在这里,通过闭包隐藏了 actionCreator 和 dispatch*/
function bindActionCreator(actionCreator, dispatch) {
  return function () {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

/* actionCreators 必须是 function 或者 object */
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error()
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

bindActionCreators 示例源码见 demo-8

大功告成

完整的示例源码见 demo-9,你可以和 redux 源码做一下对比,你会发现,我们已经实现了 redux 所有的功能了。

当然,为了保证代码的理解性,我们少了一些参数验证。比如 createStore(reducer)的参数 reducer 必须是 function 等等。

最佳实践

纯函数

什么是纯函数?

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

通俗来讲,就两个要素

  1. 相同的输入,一定会得到相同的输出
  2. 不会有 “触发事件”,更改输入参数,依赖外部参数,打印 log 等等副作用
/*不是纯函数,因为同样的输入,输出结果不一致*/
function a( count ){
   return count + Math.random();
}

/*不是纯函数,因为外部的 arr 被修改了*/
function b( arr ){
    return arr.push(1);
}
let arr = [1, 2, 3];
b(arr);
console.log(arr); //[1, 2, 3, 1]

/*不是纯函数,以为依赖了外部的 x*/
let x = 1;
function c( count ){
    return count + x;
}

我们的 reducer 计划函数,就必须是一个纯函数!

只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

总结

到了最后,我想把 redux 中关键的名词列出来,你每个都知道是干啥的吗?

  • createStore

    创建 store 对象,包含 getState, dispatch, subscribe, replaceReducer

  • reducer

    reducer 是一个计划函数,接收旧的 state 和 action,生成新的 state

  • action

    action 是一个对象,必须包含 type 字段

  • dispatch

    dispatch( action ) 触发 action,生成新的 state

  • subscribe

    实现订阅功能,每次触发 dispatch 的时候,会执行订阅函数

  • combineReducers

    多 reducer 合并成一个 reducer

  • replaceReducer

    替换 reducer 函数

  • middleware

    扩展 dispatch 函数!

你再看 redux 流程图,是不是大彻大悟了?

redux 流程图

(redux 流程图)
@brickspert brickspert changed the title 完全理解 redux 完全理解 redux(从零实现一个 redux) Nov 11, 2018
@EncounterOnly
Copy link

大神的理解很透彻,通俗易懂,学习了。

@brickspert
Copy link
Owner Author

@Mrzhangyu 感谢认可。谢谢

@maojiebaibaibai
Copy link

看了这么多redux的教程,这个真的是很通俗易懂了,不过最后中间件的地方还得好好捋一捋,还是有点晕,感谢大神

@didihaha
Copy link

这个讲的通俗易懂,赞--

@brickspert
Copy link
Owner Author

@maojiebaibaibai 中间件那里,建议跟着写一遍。自己实现两个中间件就会了!

@houwhu
Copy link

houwhu commented Nov 21, 2018

这个写的真的很好,比网上看到很多redux的教程写的好

@dumuchenglin123
Copy link

厉害厉害,真的是用心了

@aokijiliao
Copy link

厉害,讲的很好,

@Cxp18028008119
Copy link

前端 真难

@JasonGaoG
Copy link

这个真心赞一个。

@getOnce
Copy link

getOnce commented Jan 22, 2019

点赞。

@TangTangJia
Copy link

解释的非常清晰就算初学者也能看懂 感谢作者

@acmu
Copy link

acmu commented Mar 26, 2019

为什么你这么厉害,真的通俗易懂唉

@mrliangsgit
Copy link

这个是真的大神,实在佩服,十分感谢,希望继续出关于dva实战的文章

@xiaotanit
Copy link

666,是看过的关于redux最棒的教程

@codinglilong
Copy link

啥时候能实现下react-redux跪求哦

@dutaoli
Copy link

dutaoli commented Jun 24, 2019

真是太厉害了,终于明白了种种的种种

@Mstian
Copy link

Mstian commented Jun 25, 2019

多文件协作第三段代码
/注意:countReducer 接收的 state 是 state.info/
应该是InfoReducer 接收的state是 state.info

@florida0723
Copy link

赞👍

@venlee1993
Copy link

非常棒,赞

@Jay-cool-maker
Copy link

看的很透彻 感谢大神

@cuileikun
Copy link

讲解的通俗易懂,点赞

@AganCN
Copy link

AganCN commented Jul 18, 2019

感谢

@jinceon
Copy link

jinceon commented Jul 18, 2019

送上我的膝盖

@xingyongchao
Copy link

通过简述过程中把redux源码解析出来, 大神666

@barryzhang123
Copy link

牛逼 讲得很透彻

@378056350
Copy link

功力得多么深厚,才能把这个讲的这么透彻啊

@Lin-Kangjing
Copy link

果然是大神,这是我看的最清晰的redex叫教程了。感谢!!!!

@BarneyRoos
Copy link

真是一种享受,要是多一些这样的文章该多好!

@Captain-Tab
Copy link

之前学习过,现在来复习。写的真棒 👍

@coolpail
Copy link

优秀

export default function compose(...funcs) {
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

数组是长度为1时候reduce默认就是第一个元素,这边判断是不是多余了

1 similar comment
@coolpail
Copy link

优秀

export default function compose(...funcs) {
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

数组是长度为1时候reduce默认就是第一个元素,这边判断是不是多余了

@1597403461
Copy link

perfect

@yaoyanweb
Copy link

yaoyanweb commented Jun 3, 2020

111111111WX20200603-145702@2x

大佬 这个地方有点小错误,不过整篇文章还是很通俗易懂,加油!

@BugUpdater
Copy link

111111111WX20200603-145702@2x

大佬 这个地方有点小错误,不过整篇文章还是很通俗易懂,加油!

这个没错,你自己运行下试试。state是当前的状态,state.name和state.description是旧的值,如果换成这两个那就相当于没修改了。action里才是要更新的新值,所以这里从action中取是没问题的~ 你再想想哈

@shentaoliu
Copy link

跟着写了一遍,很赞啊,很全面!

@bingo-keith
Copy link

讲得很好,跟着走了一遍,循序渐进的,很好理解

@pengcc
Copy link

pengcc commented Aug 16, 2020

文章很不错,循序渐进,通俗易懂。

/* 实现 exception(time((logger(dispatch))))*/
chain.reverse().map(middleware => {
   dispatch = middleware(dispatch);
});

从语义话看,这里我建议用forEach更好呢。

chain.reduce((a, b) => b(a), dispatch)

这样写可能更优雅,作者的本义可能是想跳过compose的实现

我也认同reduce 更好(看到后边发现了),但map是没有必要的,forEach更合适,这里没有必要创建一个无用的新数组

@beyondouyuan
Copy link

大佬讲得非常通透易懂哇!不过总结那里有一句话“dispatch( action ) 触发 action,生成新的 state”,感觉可能会有一丢丢的歧义【可能会有人理解成dispath和reducer一样都可以更改state,但实际上,更改state的地方只有一个,那就是reducer,无论是redux文档的解释还是大佬代码里的表现显示都是如此】。多加几个字,改成“dispatch( action ) 触发 action,通知reducer按计划生成新的 state”,这样子也许更好一点?当然这只是小弟的个人感觉哈

@7neves
Copy link

7neves commented Sep 18, 2020

大佬,通透👍

@jxj666
Copy link

jxj666 commented Oct 21, 2020

终于看完了,临摹了一遍,收获很大
https://github.com/jxj322991/redux

@kvsur
Copy link

kvsur commented Dec 17, 2020

@yanhuomili
Copy link

牛逼

@nmnmtttt
Copy link

谢谢大神,很有帮助!

@byranb
Copy link

byranb commented May 27, 2021

牛逼!

@MrCuriosity
Copy link

写的真好,看过的有关文章里最循序渐进通俗易懂的了

applyMiddleware那里把store抽出来返回函数,参数是action,有点依赖注入和惰性求值的味道,虽然不完全是纯函数

如果中间件那里加上异步处理的解析,比如外部的runSaga(watchers),那就更棒了

@kvdo2
Copy link

kvdo2 commented May 13, 2022

谢谢!

@liuhui666
Copy link

liuhui666 commented May 13, 2022 via email

@DongHY1
Copy link

DongHY1 commented Jan 28, 2023

unsubscribe的实现方式有问题,以作者给出的DEMO-9,index.js中的代码为例

const un = store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count);
});
store.dispatch({
  type: 'INCREMENT'
});
un()
store.dispatch({
  type: 'INCREMENT'
});
console.log(store.getState()) // { counter: { count: 3 } }

取消订阅后,count的值仍然发生了变化

@pangh-space
Copy link

pangh-space commented Jan 28, 2023 via email

@liuhui666
Copy link

liuhui666 commented Jan 28, 2023 via email

@Ylg12345
Copy link

可以的,值得学习

@pangh-space
Copy link

pangh-space commented Jun 29, 2023 via email

@liuhui666
Copy link

liuhui666 commented Jun 29, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests