手写傻瓜式 React 全家桶之 Redux

文章系列

手写傻瓜式 React 全家桶之 Reduxjavascript

手写傻瓜式 React 全家桶之 React-Reduxjava

本文代码react

1、什么是 Redux

A Predictable State Container for JS Apps(JS应用的可预测状态容器)git

可预测:实际上指的是纯函数(每一个相同的输入,都会有固定输出,而且没有反作用)这个是由 reducer 来保证的,同时方便了测试github

状态容器: 在 web 页面中,每一个 DOM 元素都有本身的状态,好比弹出框有显示与隐藏两种状态,列表当前处于第几页,每页显示多少条就是这个列表的状态,存储这些状态的对象就是状态容器。web

虽然说 Redux 是 React 全家桶的一员,但实际上 Redux 与 React 没有必然联系,它能够应用在任何其余库上,只是跟 React 搭配比较火。 redux Flowredux

Redux 的核心概念

  1. Store: 存储状态的容器, JS 对象
  2. Actions: 对象,描述对状态进行怎样的操做
  3. Reducers: 函数,操做状态并返回新的状态
  4. React Component: 组件,或者称之为视图

Redux 工做流程

  1. 组件经过 dispatch 方法触发 Action
  2. Store 接收 Action 并将 Action 分发给 Reducer
  3. Reducer 根据 Action 类型对状态进行更改并将更改后的状态返回给 Store
  4. 组件订阅了 Store 中的状态,Store 中的状态更新会同步到组件

接下来分别讲解下:设计模式

React Components

就是 React 组件,也就是 UI 层markdown

Store

管理数据的仓库,对外暴露些 APIapp

let store = createStore(reducers);
复制代码

有如下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 经过 subscribe(listener) 注册监听器;
  • 经过 subscribe(listener) 返回的函数注销监听器。

Action

action 就是个动做,在组件里经过 dispatch(action) 来触发 store 里的数据更改

Reducer

action 只是个指令,告诉 store 要进行怎样的更改,真正更改数据的是 reducer。

2、为何要使用 Redux

默认 React 传递数据只能自上而下传递,而下层组件要向上层组件传递数据时,须要上层组件传递修改数据的方法到下层组件,当项目愈来愈大时,这种传递方式会很杂乱。

而引用了 Redux,因为 Store 是独立于组件,使得数据管理独立于组件,解决了组件间传递数据困难的问题

计数器

定义个 store 容器文件,根据 reducer 生成 store

import { createStore } from "redux";
const counterReducer = (state = 0, { type, payload = 1 }) => {
  switch (type) {
    case "ADD":
      return state + payload;
    case "MINUS":
      return state - payload;
    default:
      return state;
  }
};
export default createStore(counterReducer);
复制代码

在组件中

import React, { Component } from "react";
import store from "../store";
export default class Redux extends Component {
  componentDidMount() {
    this.unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }
  componentWillUnmount() {
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }

  add = () => {
    store.dispatch({ type: "ADD", payload: 1 });
  };
  minus = () => {
    store.dispatch({ type: "MINUS", payload: 1 });
  };
  render() {
    return (
      <div className="border"> <h3>加减器</h3> <button onClick={this.add}>add</button> <span style={{ marginLeft: "10px", marginRight: "10px" }}> {store.getState()} </span> <button onClick={this.minus}>minus</button> </div>
    );
  }
}

复制代码
  • 经过 getState 显示 state
  • 点击 add 或 minus 时触发 dispatch 并传递指令(action)
  • 并在 componentDidMount 监听 state 更改,有更改则就 forceUpdate 强制渲染
  • componentWillUnmount 清除监听

加减器

3、开始手写

经过上面的讲解也能够看到,其实主要的就是 createStore 函数,该函数会暴露 getState,dispatch,subScribe 三个函数 因此先搭下架子,建立 createStore.js 文件

export default function createStore(reducer) {
  let currentState;
  // 获取 store 的 state
  function getState() {}
  // 更改 store
  function dispatch() {}
  // 订阅 store 更改
  function subscribe() {}
  return {
    getState,
    dispatch,
    subscribe,
  };
}
复制代码

接着完善下各方法

getState

返回当前的 state

function getState() {
    return currentState
}
复制代码

dispatch

接收 action,并更新 store,经过谁更新的呢: reducer

// 更改 store
  function dispatch(action) {
    // 将当前的 state 以及 action 传入 reducer 函数
    // 返回新的 state 存储在 currentState
    currentState = reducer(currentState, action);
  }
复制代码

subscribe

做用: 订阅 state 的更改

如何作: 采用观察者模式,组件方监听 subscribe ,并传入回调函数,在 subscribe 里注册回调,并在 dispatch 方法里触发回调

let curerntListeners = [];
// 订阅 state 更改
function subscribe(listener) {
  curerntListeners.push(listener);
  return () => {
    const index = curerntListeners.indexOf(listener);
    curerntListeners.splice(index, 1);
  };
}
复制代码

dispatch 方法在更新数据以后,要执行订阅事件。

// 更改store
  function dispatch(action) {
    // store里面数据就更新了
    currentState = reducer(currentState, action);

    // 执行订阅事件
    curerntListeners.forEach(listener => listener());
  }
复制代码

完整代码

将上面计数器里的 redux 改为引用手写的 redux,会发现页面没有最初值 初始值

因此在 createStore 里加上 dispatch({ type: "kkk" }); 给 state 赋初始值,要注意传入的这个 type 要进入 reducer 函数的 default 条件

完整代码以下:

export default function createStore(reducer) {
  let currentState;
  let curerntListeners = [];
  // 获取 store 的 state
  function getState() {
    return currentState;
  }
  // 更改 store
  function dispatch(action) {
    // 将当前的 state 以及 action 传入 reducer 函数
    // 返回新的 state 存储在 currentState
    currentState = reducer(currentState, action);
    // 执行订阅事件
    curerntListeners.forEach((listener) => listener());
  }
  // 订阅 state 更改
  function subscribe(listener) {
    curerntListeners.push(listener);
    return () => {
      const index = curerntListeners.indexOf(listener);
      curerntListeners.splice(index, 1);
    };
  }
  dispatch({ type: "kkk" });
  return {
    getState,
    dispatch,
    subscribe,
  };
}

复制代码

你们也能够自行查看下 Redux 里的 createStore 源码

4、Redux 中间件

说到 Redux,那就不得不提中间件,由于自己 Redux 能作的东西颇有限,好比须要 redux-thunk 来达到异步调用,redux-logger 记录日志等。 中间件就是个函数,在组件发出 Action 和执行 Reducer 这两步之间,添加其余功能,至关于增强 dispatch。

image.png

开发 Redux 中间件

开发中间件是有模板代码的

export default store => next => action => {}
复制代码
  1. 一个中间件接收 store 做为参数,返回一个函数
  2. 返回的这个函数接收 next(老的 dispatch 函数) 做为参数,返回一个新的函数
  3. 返回的新的函数就是增强的 dispatch 函数,在这个函数里能够拿到上面两层传递进来的 store 以及 next

好比模拟写个 logger 中间件

function logger(store) {
  return (next) => {
    return (action) => {
      console.log("====================================");
      console.log(action.type + "执行了!");
      console.log("prev state", store.getState());
      next(action);
      console.log("next state", store.getState());
      console.log("====================================");
    };
  };
}
export default logger;
复制代码

注册中间件

// 在createStore的时候将applyMiddleware做为第二个参数传进去
const store = createStore(
  reducer,
  applyMiddleware(logger)
)
复制代码

能够看出是经过 createStore 的第二个参数来实现的,这个参数官方称为 enhancer。 enhancer 是个参数为 createStore 的函数,并返回个新的 createStore 函数

function enhancer (createStore) {
    return function (reducer) {
      var store = createStore(reducer);
      var dispatch = store.dispatch;
      function _dispatch (action) {
        if (typeof action === 'function') {
          return action(dispatch)
        }
        dispatch(action);
      }
      return {
        ...store,
        dispatch: _dispatch
      }
    }
}
复制代码

实现上 createStore 总共是有三个参数,除了第一个 reducer 参数是必传的以外,第二个 state 初始值,以及第三个 enhancer 都是可选的

下面咱们在手写的 createStore 里加入 enhancer 参数, 以及手写下 applyMiddleware 函数

createStore 里加上 enhancer

function createStore(reducer,enhancer) {
	// 判断是否存在 enhancer
	// 若是存在而且是个函数, 则将 createStore 传递给它, 不是函数则抛出错误
	// 它会返回个新的 createStore
	// 传入 reducer ,执行新的 createStore,返回 store
	// 返回该 store
	if (typeof enhancer !== 'undefined') {
		if (typeof enhancer !== 'function') {
			throw new Error('enhancer必须是函数')
		}
		return enhancer(createStore)(reducer)
	}
	// 没有 enhancer 走原先的逻辑
	// 省略
}
复制代码

手写 applyMiddleware

按上面的分析,applyMiddleware 函数,会接收中间件函数,并返回个 enhancer,因此基本结构为

export default function applyMiddleware(...middlewares) {
  // applyMiddleware 的返回值应该是一个 enhancer
  // enhancer 是个接收 createStore 为参数的函数
  return function (createStore) {
    // enhancer 要返回一个新的 createStore
    return function newCreateStore(reducer) {};
  };
}

复制代码

看下 logger 中间件的结构,

function logger(store) {
  return (next) => {
    return (action) => {
      console.log("====================================");
      console.log(action.type + "执行了!");
      console.log("prev state", store.getState());
      next(action);
      console.log("next state", store.getState());
      console.log("====================================");
    };
  };
}
export default logger;
复制代码

完善下 applyMiddleware

export default function applyMiddleware(middleware) {
  // applyMiddleware 的返回值应该是一个 enhancer
  // enhancer 是个接收 createStore 为参数的函数
  return function (createStore) {
    // enhancer 要返回一个新的 createStore
    return function newCreateStore(reducer) {
      // 建立 store
      let store = createStore(reducer);
      let dispatch = store.dispatch;

      // dispatch 属性必定要写成这种形式,不能直接将 store.dispatch 传入
      // 由于有多个中间件时, dispatch 的值是要获取上一个中间件增强后的 dispatch
      // 这种传递方式有效,是因为 dispatch 是引用类型
      const midApi = {
        getState: store.getState,
        dispatch: (action, ...args) => dispatch(action, ...args),
      };
      // 传入 store 执行中间件的第一层函数
      const chain = middleware(midApi);

      // 将原始的 dispatch 函数做为 next 参数传给 chain,调用中间件的第二层函数
      // 返回增强的 dispatch 覆盖原先的 dispatch
      dispatch = chain(dispatch);

      return {
        ...store,
        dispatch,
      };
    };
  };
}

复制代码

测试: 是能够正常打印出日志的 打印日志

支持多个中间件

上面已经在 applyMiddleware 函数里处理只有一个中间件的状况,那多个的场景呢?

首先咱们再模拟写个 redux-thunk 中间件

默认 redux 只支持同步,而且参数只能是对象形式,redux-thunk 要实现的是你传入一个函数时,我就直接执行该函数,异步操做代码写在传递过来的函数里,若是传递过来的是个对象,则调用下一个中间件

function thunk({ getState, dispatch }) {
  return next => {
    return action => {
      // 若是是个函数,则直接执行,并传入 dispatch 与 getState
      if (typeof action == 'function') {
        return action(dispatch, getState)
      }
      next(action)
    }
  }
}
复制代码

如今要去依次执行各个中间件,要如何依次执行呢?就得采用柯里化,首先写个 compose 函数

function compose(...funs) {
    // 没有传递函数时,则返回参数透传函数
    if (funs.length === 0) {
        return (arg) => arg
    }
    // 传递一个函数时,则直接返回该函数,省去了遍历
    if (funs.length === 1) {
        return funs[0]
    }
    // 传递多个时,则采用 reduce,进行合并
    // 好比执行 compose(f1,f2,f3) 则会返回 (...args) => f1(f2(f3(...args)))
    return funs.reduce((a, b) => {
        return (...args) => {
            return a(b(...args))
        }
    })
}
复制代码

applyMiddleware 函数支持多个中间件:

export default function applyMiddleware(...middlewares) {
  // applyMiddleware 的返回值应该是一个 enhancer
  // enhancer 是个接收 createStore 为参数的函数
  return function (createStore) {
    // enhancer 要返回一个新的 createStore
    return function newCreateStore(reducer) {
      // 建立 store
      let store = createStore(reducer);
      let dispatch = store.dispatch;

      // dispatch 属性必定要写成这种形式,不能直接将 store.dispatch 传入
      // 由于有多个中间件时, dispatch 的值是要获取上一个中间件增强后的 dispatch
      // 这种传递方式有效,是因为 dispatch 是引用类型
      const midApi = {
        getState: store.getState,
        dispatch: (action, ...args) => dispatch(action, ...args),
      };
      // 调用中间件的第一层函数 传递阉割版的 store 对象
      const middlewareChain = middlewares.map((middle) => middle(midApi));
      
      // 用 compose 获得一个组合了全部中间件的函数
      const middleCompose = compose(...middlewareChain);

      // 将原始的 dispatch 函数做为参数逐个调用中间件的第二层函数
      // 返回增强的 dispatch 覆盖原先的 dispatch
      dispatch = middleCompose(dispatch);

      return {
        ...store,
        dispatch,
      };
    };
  };
}
复制代码

验证:

import { createStore } from "../kredux";
import logger from "../kredux/middlewares/logger";
import thunk from "../kredux/middlewares/thunk";
import applyMiddleware from "../kredux/applyMiddleware";

const counterReducer = (state = 0, { type, payload = 1 }) => {
  switch (type) {
    case "ADD":
      return state + payload;
    case "MINUS":
      return state - payload;
    default:
      return state;
  }
};
export default createStore(counterReducer, applyMiddleware(thunk, logger));

复制代码

并将 add 函数更改为异步触发 dispatch

add = () => {
    // store.dispatch({ type: "ADD", payload: 1 });
    store.dispatch(function (dispatch) {
      setTimeout(() => {
        dispatch({ type: "ADD", payload: 2 });
      }, 1000);
    });
  };
复制代码

中间件

5、手写 combineReducers

当业务逻辑复杂时,不可能都写在一个 reducer 里,这时就得使用 combineReducers 将几个 reducer 组合起来。

再添加个 userReducer:

const userReducer = (state = { ...initialUser }, { type, payload }) => {
  switch (type) {
    case "SET":
      return { ...state, ...payload };
    default:
      return state;
  }
};
复制代码

引入 combineReducers ,该函数接收个对象,key 为标识,value 为每一个 reducer

export default createStore(
  combineReducers({ count: counterReducer, user: userReducer }),
  applyMiddleware(thunk, logger)
);
复制代码

根据上面的分析,手写个 combineReducers ,它要返回个 reducer 函数,reducer 函数天然是要接收 state 跟 action 并返回新的 state

export default function combineReducers(reducers) {
  return function reducer(state = {}, action) {
    let nextState = {};
    // 遍历全部的 reducers,并依次触发返回新的 state
    for (let key in reducers) {
      nextState[key] = reducers[key](state[key], action);
    }
    return nextState;
  };
}

复制代码

6、总结

  1. Redux 自己就只是个可预测性状态容器,状态的更改都是经过发出 Action 指令,Action 指令会分发给 Reducer ,再由 Reducer 返回新的状态值,组件进行监听,但状态值更改了,就从新渲染。
  2. Redux 是典型的观察者模式,subscribe 时注册回调事件,dispatch action 时执行回调
  3. Redux 重点在于 createStore 函数,该函数会返回三个方法,其中 getState 用于返回当前的 state,subscribe 用于订阅 state 的更改,dispatch 更新 store,并执行回调事件
  4. 默认的 Redux 只支持传入对象,以及只能执行同步,要知足更多的场景就须要采用中间件
  5. 中间件是个模板为export default store => next => action => {} 的函数
  6. 注册中间件,就要使用 createStore 的 enhancer
  7. Redux 的中间件是对 dispatch 进行增强,这就是典型的装饰者模式,能够看出 Redux 里处处是设计模式
相关文章
相关标签/搜索