恋の歌的logo
Auto
归档 标签

Redux源码解读 - combineReducers篇

发表于2020-07-11 09:54
更新于2023-02-13 18:28
分类于编程
总字数2.6K
阅读时长 ≈9 分钟

Redux源码解读 - combineReducers篇。

这次讲讲combineReducers这个函数。

其实之前学习过React,也用过和React配套的React-Redux

combineReducers 🔗

既然不知道它是干嘛的。

直接上官网找找文档就行了。

以下是在中文网摘过来的一段话。

随着应用变得越来越复杂,可以考虑将 reducer 函数 拆分成多个单独的函数,拆分后的每个函数负责独立管理state的一部分。 combineReducers辅助函数的作用是,把一个由多个不同reducer函数作为valueobject,合并成一个最终的 reducer函数,然后就可以对这个reducer调用createStore方法。合并后的reducer可以调用各个子reducer,并把它们返回的结果合并成一个state对象。 由combineReducers()返回的state对象,会将传入的每个reducer返回的state按其传递给combineReducers()时对应的key进行命名。

不是很难理解。

如果把全部的处理action的逻辑写在一个reducer,这个reducer就会显得很臃肿,也不容易维护。

合并不同处理逻辑的reducer,返回一个新的reducer,通过新的reducer来创建store

可以写个小例子来看看这个函数的效果:

javascript
const aReducer = (state = { val: 1 }, action) => {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        val: state.val + 1,
      };
    case "decrement":
      return {
        ...state,
        val: state.val - 1,
      };
    default:
      return state;
  }
};

const bReducer = (state = { val: 2 }, action) => {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        val: state.val + 2,
      };
    case "decrement":
      return {
        ...state,
        val: state.val - 2,
      };
    default:
      return state;
  }
};

const reducer = combineReducers({
  aReducer,
  bReducer,
});

const store = createStore(reducer);

console.log(store.getState());

store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch({
  type: "increment",
});

运行之后可以出现:

到这里基本就可以明白基本的流程了。

每个reducer都有自己的一个命名,这个命名会在state中体现。

每次dispatch,会遍历每一个传入的reducer,更新state

ok,那先把源码贴上来:

javascript
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);
  const finalReducers = {};
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i];

    if (process.env.NODE_ENV !== "production") {
      if (typeof reducers[key] === "undefined") {
        warning(`No reducer provided for key "${key}"`);
      }
    }

    if (typeof reducers[key] === "function") {
      finalReducers[key] = reducers[key];
    }
  }
  const finalReducerKeys = Object.keys(finalReducers);

  // This is used to make sure we don't warn about the same
  // keys multiple times.
  let unexpectedKeyCache;
  if (process.env.NODE_ENV !== "production") {
    unexpectedKeyCache = {};
  }

  let shapeAssertionError;
  try {
    assertReducerShape(finalReducers);
  } catch (e) {
    shapeAssertionError = e;
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError;
    }

    if (process.env.NODE_ENV !== "production") {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      );
      if (warningMessage) {
        warning(warningMessage);
      }
    }

    let hasChanged = false;
    const nextState = {};
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i];
      const reducer = finalReducers[key];
      const previousStateForKey = state[key];
      const nextStateForKey = reducer(previousStateForKey, action);
      if (typeof nextStateForKey === "undefined") {
        const errorMessage = getUndefinedStateErrorMessage(key, action);
        throw new Error(errorMessage);
      }
      nextState[key] = nextStateForKey;
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length;
    return hasChanged ? nextState : state;
  };
}

看起来很长,拆开开其实就会很清晰。

javascript
const reducerKeys = Object.keys(reducers);
const finalReducers = {};
for (let i = 0; i < reducerKeys.length; i++) {
  const key = reducerKeys[i];

  if (process.env.NODE_ENV !== "production") {
    if (typeof reducers[key] === "undefined") {
      warning(`No reducer provided for key "${key}"`);
    }
  }

  if (typeof reducers[key] === "function") {
    finalReducers[key] = reducers[key];
  }
}
const finalReducerKeys = Object.keys(finalReducers);

第一段代码。

先是获取了每个reducer的名字,存到reducerKeys里面。

定义了一个字面对象量finalReducers

然后根据reducerKeys的长度做了一个循环。

这个循环两个判断。

第一个判断,如果传入了一个值为undefined的属性,非生产环境下就会有一个警告。

第二个判断,只把值的类型为function的才添加到finalReducers里面。

循环结束后获取finalReducers的属性名组成的数组,存到finalReducerKeys

这段的作用就是防止传入不符合规则的对象。

比如下面这样:

javascript
combineReducers({
  a: undefined,
  b: 1,
  c: "abc",
});

这些属性名对应的值都是不符合规则的,都要排除掉。

javascript
let unexpectedKeyCache;
if (process.env.NODE_ENV !== "production") {
  unexpectedKeyCache = {};
}

第二段代码。

就在非生产环境下初始化一个字面对象变量unexpectedKeyCache而已,现在还不知是干什么的,先跳过。

javascript
let shapeAssertionError;
try {
  assertReducerShape(finalReducers);
} catch (e) {
  shapeAssertionError = e;
}

第三段代码

往函数assertReducerShape传入了finalReducers变量。

对函数assertReducerShape的运行进行了异常捕获。

来看看assertReducerShape这个函数。

javascript
function assertReducerShape(reducers) {
  Object.keys(reducers).forEach((key) => {
    const reducer = reducers[key];
    const initialState = reducer(undefined, { type: ActionTypes.INIT });

    if (typeof initialState === "undefined") {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
          `If the state passed to the reducer is undefined, you must ` +
          `explicitly return the initial state. The initial state may ` +
          `not be undefined. If you don't want to set a value for this reducer, ` +
          `you can use null instead of undefined.`
      );
    }

    if (
      typeof reducer(undefined, {
        type: ActionTypes.PROBE_UNKNOWN_ACTION(),
      }) === "undefined"
    ) {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
          `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
          `namespace. They are considered private. Instead, you must return the ` +
          `current state for any unknown actions, unless it is undefined, ` +
          `in which case you must return the initial state, regardless of the ` +
          `action type. The initial state may not be undefined, but can be null.`
      );
    }
  });
}

这里面主要对每个reducer做了两个判断。

第一个判断,初始化时对于传进的初始状态undefined不能返回undefined

必须在Redux内部初始化时(调用一次dispatchactionActionTypes.INITActionTypes.INITRedux内部的action)。

每一个reducer都不能返回undefined

第二个判断其实我并不是很能理解,不过从抛出错误的信息可以知道。

大意就是要求我们不要去处理Redux内部的action,对于未知的aciton都要返回一个不是undefined的数据。

Redux内部的action有三个。

javascript
const randomString = () =>
  Math.random().toString(36).substring(7).split("").join(".");

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`,
};

第三部分的代码到这里就结束,主要对reducer的返回值做了断言,并将断言的结果保存在shapeAssertionError中。

javascript
return function combination(state = {}, action) {
  if (shapeAssertionError) {
    throw shapeAssertionError;
  }

  if (process.env.NODE_ENV !== "production") {
    const warningMessage = getUnexpectedStateShapeWarningMessage(
      state,
      finalReducers,
      action,
      unexpectedKeyCache
    );
    if (warningMessage) {
      warning(warningMessage);
    }
  }

  let hasChanged = false;
  const nextState = {};
  for (let i = 0; i < finalReducerKeys.length; i++) {
    const key = finalReducerKeys[i];
    const reducer = finalReducers[key];
    const previousStateForKey = state[key];
    const nextStateForKey = reducer(previousStateForKey, action);
    if (typeof nextStateForKey === "undefined") {
      const errorMessage = getUndefinedStateErrorMessage(key, action);
      throw new Error(errorMessage);
    }
    nextState[key] = nextStateForKey;
    hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
  }
  hasChanged =
    hasChanged || finalReducerKeys.length !== Object.keys(state).length;
  return hasChanged ? nextState : state;
};

第四部分代码。

就是返回一个新的reducer函数了。

开始就先对之前保存的断言就行了判断,如果发现reducer不符合规则,直接抛出错误。

接下来。

javascript
if (process.env.NODE_ENV !== "production") {
  const warningMessage = getUnexpectedStateShapeWarningMessage(
    state,
    finalReducers,
    action,
    unexpectedKeyCache
  );
  if (warningMessage) {
    warning(warningMessage);
  }
}

这一段代码传入了旧的statereducers数组,请求的action,和之前没用过的一个空的unexpectedKeyCache对象。

看看getUnexpectedStateShapeWarningMessage这个函数的实现。

javascript
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  // 获取reducers的属性名组成的数组
  const reducerKeys = Object.keys(reducers);
  // 初始化state还是更新state
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? "preloadedState argument passed to createStore"
      : "previous state received by the reducer";

  // reducers为空,即 reducers = {}
  if (reducerKeys.length === 0) {
    return (
      "Store does not have a valid reducer. Make sure the argument passed " +
      "to combineReducers is an object whose values are reducers."
    );
  }

  // state必须为一个字面对象量
  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    );
  }

  // reducers.hasOwnProperty(key)
  // 找到那些inputState里面有的但是在reducers自身没有的属性名
  // 即 inputState = {a: 1, b: 2}  reducers = {a: aReducer}
  // 那么b就会被传入unexpectedKeys中
  // unexpectedKeyCache[key]
  // 之前已经确认为不包括的属性名就不用再找出来了
  const unexpectedKeys = Object.keys(inputState).filter(
    (key) => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  );

  // 在unexpectedKeyCache中标记这些新的不包括在reducers内的属性名
  unexpectedKeys.forEach((key) => {
    unexpectedKeyCache[key] = true;
  });

  // 如果action为替换reducer那就没事,因为替换也意味着reducers的结构也会发生改变
  if (action && action.type === ActionTypes.REPLACE) return;

  // 如果unexpectedKeys存在项,即inputState存在了不包括在reducers中的属性名
  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? "keys" : "key"} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    );
  }
}

这个函数主要就是检查inputState的结构是否符合reducers

javascript
let hasChanged = false;
const nextState = {};
for (let i = 0; i < finalReducerKeys.length; i++) {
  const key = finalReducerKeys[i];
  const reducer = finalReducers[key];
  const previousStateForKey = state[key];
  const nextStateForKey = reducer(previousStateForKey, action);
  if (typeof nextStateForKey === "undefined") {
    const errorMessage = getUndefinedStateErrorMessage(key, action);
    throw new Error(errorMessage);
  }
  nextState[key] = nextStateForKey;
  hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}

这个循环便是整个返回函数中最重要的部分了。

定义了一个标志变量和hasChange和一个下一个状态的对象nextState

循环已经筛选过的reducers,也就是finalReducerKeys

  • key reducer的名字;
  • reducer key对应的reducer
  • previousStateForKey reducer对应的旧状态;
  • nextStateForKey reducer执行后的新状态。

然后判断新的状态是不是undefined,是的话报错,因为这是不符合规则的。

然后在新的状态对象上添加对应keynextState

最后确定新的状态是不是和旧的状态是否相同,保存在hasChange中。

javascript
hasChanged =
  hasChanged || finalReducerKeys.length !== Object.keys(state).length;
return hasChanged ? nextState : state;

最后就是确定要不要返回新的状态,如果每个reducer返回的都和旧状态相同的话。

逻辑上整个状态就应该和原来相同。

除了循环内的判断,最后做了一个finalReducerKeys.length !== Object.keys(state).length判断。

只要旧的statekey值集合长度和finalReducerKeys的长度不一致就返回nextState

到这里基本上就把这个函数看完了。

需要注意的是,在文档中有对这函数的一个注意事项。

本函数设计的时候有点偏主观,就是为了避免新手犯一些常见错误。也因些我们故意设定一些规则,但如果你自己手动编写根redcuer时并不需要遵守这些规则。 每个传入 combineReducers 的 reducer 都需满足以下规则:

  • 所有未匹配到的action,必须把它接收到的第一个参数也就是那个state原封不动返回。
  • 永远不能返回undefined。当过早return时非常容易犯这个错误,为了避免错误扩散,遇到这种情况时combineReducers会抛异常。
  • 如果传入的state就是undefined,一定要返回对应reducer的初始state。根据上一条规则,初始state禁止使用undefined。使用ES6的默认参数值语法来设置初始state很容易,但你也可以手动检查第一个参数是否为 undefined

虽然combineReducers自动帮你检查reducer是否符合以上规则,但你也应该牢记,并尽量遵守。即使你通过 Redux.createStore(combineReducers(...), initialState) 指定初始statecombineReducers 也会尝试通过传递 undefinedstate来检测你的reducer是否符合规则。因此,即使你在代码中不打算实际接收值为undefinedstate,也必须保证你的reducer在接收到undefined时能够正常工作。

所以在源码中可以看到很多判断语句。

#Redux
#JavaScript
哦呐该,如果没有评论的话,瓦达西...