Redux Scratch

从零实现Redux

// 定一个 reducer
function reducer (state, action) {
  /* 初始化 state 和 switch case */
}

// 生成 store
const store = createStore(reducer)

// 监听数据变化重新渲染页面
store.subscribe(() => renderApp(store.getState()))

// 首次渲染页面
renderApp(store.getState())

// 后面可以随意 dispatch 了,页面自动更新
store.dispatch(...)

createStore

Redux是围绕着store为核心的。store是一个包含状态、更新方法(dispatch())和读取方法(subscribe()/getState())JavaScript对象。还有listeners(监听器)用于组件订阅状态变化执行的函数。store的形式定义如下:

const store = {
  state: {}, // 状态是一个对象
  listeners: [], // 监听器是一个函数数组
  dispatch: () => {}, // dispatch是一个函数
  subscribe: () => {}, // subscribe是一个函数
  getState: () => {}, // getState是一个函数
};

为了使用这个仓库对象来管理状态,我们要够一个createStore()函数,代码如下:

const createStore = (reducer, initialState) => {
  const store = {};
  store.state = initialState;
  store.listners = [];

  store.getState = () => store.state;

  store.subscribe = (listner) => {
    store.listners.push(listener);
  };

  store.dispatch = (action) => {
    store.state = reducer(store.state, action);
    store.listeners.forEach((listener) => listener());
  };

  return store;
};

createStore函数接收两个参数,一个是reducer和一个initialStatereducer函数会在后续详细介绍,现在只要知道这是一个指示状态应该如何更新的函数。createStore函数开始于创建一个store对象。然后通过store.state = initialState进行初始化,如果开发者没有提供则值会是undefinedstate.listeners会被初始化为空数组。store中定义的第一个函数是getState()。当调用时只是返回状态,store.getState = () => store.state。

状态订阅

我们允许UI订阅(subscribe)状态的变化。订阅实际上是传递一个函数给subscribe方法,并且这个函数作为监听器会被添加到监听器数组中。typeof listener === ‘function’的结果是true。在每一个状态变化的时候,我们会遍历所有的监听器函数数组,并逐个执行。

store.listeners.forEach((listener) => listener());

接下来,定义了dispatch函数。dispatch函数是当用户和UI交互时,组件进行调用的。dispatch接收 一个单一的action对象参数。这个action应该要完全描述用户接收到的交互。action和当前状态一起, 会被传递到reducer函数,并且返回一个新的状态。在新的状态被reducer创建后,监听器数组会被遍历,并且每个函数会执行。通常,getState函数在监听器函数内部会被调用,因为监听的目的是响应状态变化。

注意到数据流向是一个非常线性和同步的过程。监听器函数添加到一个单独的监听器数组中。当用户和应用交互时,会产生一个用于dispatchaction。这个action会创建一个可预测和独立的状态改变。接着这个监听器数组被遍历,让每个监听器函数被调用。这个过程是一个单向的数据流。只有一个途径在应用中创建和响应数据变化。没有什么特别的技巧发生,只是一步一步针对交互并遵循明确统一模式的路径。

Reducer函数

reducer是一个接收stateaction的函数,并返回新的状态。形式如下:

const reducer = (prevState, action) => {
  let nextState = {}; // 一个表示新状态的对象

  // ...
  // 使用前一个状态和action创建新状态的代码
  // ...

  return nextState;
};

这里的prevState, nextStateaction都是JavaScript对象。让我们详细看一下action对象来理解它是如何用于更新状态的。我们知道一个action会包含一个唯一的字符串type来标识由用户触发的交互。

例如,假设你使用Redux来创建一个简单的todo list应用。当用户点击提交按钮来添加项目到列表中时,将会触发一个带有ADD_TODO类型的action。这是一个既对人类可读和理解,并且对Redux关于aciton目的也是清晰的指示。当添加一个项目时,它将会包含一个texttodo内容作为负载(payload)。因此,添加一个todo到列表中,可以通过以下的action对象来完全表示:

const todoAction = {
  type: "ADD_TODO",
  text: "Get milk from the store",
};

现在我们可以构建一个reducer来支撑一个todo应用。

const getInitialState = () => ({
  todoList: [],
});

const reducer = (prevState = getInitialState(), action) => {
  switch (action.type) {
    case "ADD_TODO":
      const nextState = {
        todoList: [...prevState.todoList, action.text],
      };

      return nextState;
    default:
      return prevState;
  }
};

// console.log(store.getState()) = { todoList: [] };
//
// store.dispatch({
//  type: 'ADD_TODO',
//  text: 'Get milk from the store',
//});
//
// console.log(store.getState()) => { todoList: ['Get milk from the store'] }

注意每次reducer被调用的时候我们都会创建一个新的对象。我们使用前一次的状态,但是创建了一个 完整全新的状态。这是另一个非常重要的原则能够让redux可预测。通过将状态分割成离散的,开发者 可以精确的指导应用中会发生什么。这里只要了解根据状态的变化来重新渲染UI的特定部分即可。

你通常会看到在Redux中使用switch语句。这是匹配字符串比较方便的一个方法,在我们的例子中, actiontype为例,对应更新状态的代码块。这个使用ifelse语句来写没有差别,如下:

if (action.type === "ADD_TODO") {
  const nextState = {
    todoList: [...prevState.todoList, action.text],
  };

  return nextState;
} else {
  return prevState;
}

Redux对于reducer中的内容实际上是无感知的。这是一个开发者定义的函数,用来创建一个新的状态。 实际上,用户控制了几乎所有——reducer,被使用的action,通过订阅被执行的监听器函数。Redux就 像一个夹层将这些内容进行联系起来,并提供一个通用的接口来和状态进行交互。

完整应用

import React, { useEffect, useState } from "react";
import { Action } from "redux";

interface Todo {
  title: string;
  content: string;
}

interface InitialState {
  todos: Todo[];
}

interface Store {
  state: any;
  listeners: Function[];
  dispatch: Function;
  subscribe: Function;
  getState: () => InitialState;
}

const todos: Todo[] = [
  {
    title: "title",
    content: "content",
  },
];

const getInitialState = () => {
  return {
    todos,
  };
};

const createStore = (reducer: Function, initialState?: InitialState) => {
  const store: Partial<Store> = {};
  store.state = initialState;
  store.listeners = [];

  store.getState = () => store.state;

  store.subscribe = (listener: Function) => {
    store.listeners!.push(listener);
  };

  store.dispatch = (action: Action) => {
    console.log("> Action", action);
    store.state = reducer(store.state, action);
    store.listeners!.forEach((listener) => listener());
  };

  return store;
};

const reducer = (
  state = getInitialState(),
  action: {
    type: "ADD_TODO";
    payload: Todo;
  }
) => {
  switch (action.type) {
    case "ADD_TODO":
      const nextState = {
        todos: [...state.todos, action.payload],
      };

      return nextState;
    default:
      return state;
  }
};

const store = createStore(reducer);

store.dispatch!({}); // 设置初始化状态

export const ReduxScratch = () => {
  const [slosh, setSlosh] = useState(0);

  useEffect(() => {
    store.subscribe!(() => {
      setSlosh(Math.random());
    });
  }, []);

  return (
    <div>
      {store.getState!().todos.map((todo) => (
        <div>
          <h1>{todo.title}</h1>
          <p>{todo.content}</p>
        </div>
      ))}

      <button
        key={slosh}
        onClick={() => {
          const num = Math.random();
          store.dispatch!({
            type: "ADD_TODO",
            payload: {
              title: `title:${num}`,
              content: `content:${num}`,
            },
          });
          setSlosh(num);
        }}
      >
        点击更新
      </button>
    </div>
  );
};

Links

下一页