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

Read Redux #37

Open
XXHolic opened this issue Jun 21, 2019 · 0 comments
Open

Read Redux #37

XXHolic opened this issue Jun 21, 2019 · 0 comments
Labels

Comments

@XXHolic
Copy link
Owner

XXHolic commented Jun 21, 2019

目录

引子

之前看过一些对于 redux 源码的解读,现在想按照自己的想法写一写。

从 2018.04.18 Redux v4.0.0 之后就没有发过大的版本了,现在处于一个很稳定的阶段。

下面是查看代码所处分支、时间、commit 的信息:

  • 代码分支——master
  • commit 时间——2019.06.17
  • commit content——removed distinctState() filter (#3450)
  • commit hash——beb1fc29ca6ebe45226caa3a064476072cd9ed26

基本概念

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可以和很多框架配合使用。

Redux 三大原则:

  • 单一数据源(Single source of truth):整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
  • State 是只读的(State is read-only):唯一改变 state 的方法就是触发 actionaction 是一个用于描述已发生事件的普通对象。
  • 使用纯函数来执行修改(Changes are made with pure functions):为了描述 action 如何改变 state tree ,你需要编写 reducer

Action 描述有事情发生,它把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。
Action 创建函数 就是生成 action 的方法。

Reducers 指定了应用状态的变化如何响应 action 并发送到 store

action 描述“发生了什么”,使用 reducer 来根据 action 更新 stateStore 就是把它们联系到一起的对象。

详细介绍见官方文档

下面结合官方文档中的例子去看源码。

主要实现

在文档 基础(Basic Tutorial) 一章中,先提供了一个纯 JavaScript 的例子,这里取其中一部分完整的逻辑如下。

/*
 * actions.js 文件
 */
// action 类型
export const ADD_TODO = "ADD_TODO";
// action 创建函数
export function addTodo(text) {
  return { type: ADD_TODO, text };
}

/*
 * reducers.js 文件
 */
const initialState = {
  todos: []
};

function todosFun(state = [], action) {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, { text: action.text, completed: false }];
    default:
      return state;
  }
}

function todoApp(state = initialState, action) {
  return {
    todos:todosFun(state.todos, action)
  };
}

export default todoApp;

/*
 * index.js 文件
 */
import { createStore } from 'redux';
import { addTodo } from "./actions";
import todoApp from './reducers';

let store = createStore(todoApp);

// 每次 state 更新时,打印日志
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

// 发起 action
store.dispatch(addTodo('Learn about redux'))

// 停止监听 state 更新
unsubscribe();

从例子里面可以发现,actionreducer 都是很常见的函数和对象,在 index.js 文件里面才出现了跟 redux 相关的调用,首先看下 createStore(todoApp) 做了什么。

API 文档中说是创建一个 Redux store 来以存放应用中所有的 state,有三个参数依次是:

  1. reducer:是一个方法,这个方法接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。
  2. preloadedState:初始 state。
  3. enhancer:是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。

createStore(todoApp) 中用到了第一个参数,传参也符合文档描述要求。看了源码后,发现除了文档说的作用,还有其它的作用。

先列出 createStore 方法中主要组成

function createStore(reducer, preloadedState, enhancer) {
  // 主要变量
  var currentReducer = reducer
  var currentState = preloadedState
  var currentListeners = []
  var nextListeners = currentListeners
  var isDispatching = false

  // 方法
  function ensureCanMutateNextListeners() {} // 对 currentListeners 浅复制
  function getState() {} // 获取当前 state
  function subscribe(listener) {} // 添加监听
  function dispatch(action) {} // 分发 action ,这是唯一能够让 state 改变的方式。
  function replaceReducer(nextReducer) {} // 替换当前 reducer
  function observable() {} // 可观察/反应库的互操作点

  // 主要执行逻辑
  dispatch({
    type: ActionTypes.INIT
  })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

其中 ActionTypes.INIT 并不是 createStore 的内部变量,找到后是这样的:

/**
 * 这些是 Redux 内部保留的私有 action 类型
 */
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()}`
}

export default ActionTypes

差不多齐了,可以发现在 createStore 执行后除了对外提供一些常见的接口,主要就执行了一次 dispatch, 触发了一次 state 初始化操作,那么接着就是看下 dispatch 方法里面做了什么。除去检查和容错处理代码,主要的逻辑如下:

  function dispatch(action) {
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

处理的逻辑:

  1. 将分发的状态置为 true ,表示正在分发 action。
  2. 设置当前 state,其中 currentReducer 就是一开始传给 createStore 的参数 todoApp,也就是 reducers.js 文件里面导出的方法 todoApp,这时传入的 ActionTypes.INIT 在程序中没有匹配,返回了默认的 initialState
  3. 检查是否有监听程序,如果有,就执行相关监听程序。

初始化的过程就这样结束了。这个函数里面就是 redux 的主要实现逻辑,简单来说就是将我们自己编写的 actionreducer,通过 dispatch 方法一一对应起来,每次想要改变 state,必须要通过 dispatch 方法。例如 index.js 文件里面后面执行的语句:

store.dispatch(addTodo('Learn about redux'))

addTodo 是 action 创建函数,返回的结果是 { type: 'ADD_TODO', 'Learn about redux' },通过 dispatch 方法,将这个结果传递了 reducers.js 里面的 todoApp 方法接收,进行相应的处理后,返回了新的 state{todos:[{text: 'Learn about redux', completed: false}]}

更加详细的代码和注释见源文件 createStore.js

其它 API 实现

下面针对其它 API 的实现介绍一下。

combineReducers

使用:combineReducers(reducers)

作用:是把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore 方法。

参数:reducers 是一个对象,它的值(value)对应不同的 reducer 函数。

列出主要相关代码:

// 错误处理和提示
function getUndefinedStateErrorMessage(key, action) {}
// 错误处理和提示
function getUnexpectedStateShapeWarningMessage(inputState,reducers,action,unexpectedKeyCache) {}
// 对 reducer 的检查、判断以及错误提示
function assertReducerShape(reducers) {}

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 (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

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

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

    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)
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

combineReducers 方法主要逻辑是:

  1. 获取 reducers 中属性值是方法的属性,并单独提取出属性名形成数组 finalReducerKeys。
  2. 对数据 reducers 数据正确性进行检查,例如是否有值等等。
  3. 返回一个函数,当执行 dispatch 的时候,根据 finalReducerKeys 数组循环所有的 reducer 函数并执行,得到最新的 state。

applyMiddleware

使用:applyMiddleware(...middlewares)

作用:扩展 Redux 的一种方式,可以让包装 store 的 dispatch 方法来达到想要的目的,例如使用 redux-thunk 中间件来实现异步 action。

返回值:是一个函数

首先来看下官网在 高级(Advanced Tutorial) 中讲解 异步 Action(Async Actions) 使用的示例:

import thunk from 'redux-thunk'

const store = createStore(
  rootReducer,
  applyMiddleware(
    thunk
  )
)

上面有提到 createStore 方法的第二个参数是初始的 state,这里怎么传了一个函数?原因是在 createStore 对参数做了下面的判断:

export default function createStore(reducer, preloadedState, enhancer) {
  // 之前代码省略

  // 如果第 2 个参数是函数 且 第 3 个参数 undefined,就把第 2 个参数值赋给第 3 个参数
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  // 如果 enhancer 有效,执行 enhancer 方法并返回结果。
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }
  // 之后代码省略
}

这样就相当于是执行了

applyMiddleware(thunk)(createStore)(reducer, preloadedState)

再来看 applyMiddleware 方法做了什么处理,下面是源码:

import compose from './compose'

/**
 * @param {...Function}
 * @returns {Function}
 */
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 正常的进行初始化
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
    // 每个 middleware 需要传入的参数
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args) //这里 dispatch 是一个引用,后面修改了这里也会影响
    }
    // 传入的参数通过 ... 转换放入 middlewares 数组中,统一执行传入的函数
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 返回按照传入参数顺序的调用函数,如果是多个,会返回一个嵌套的函数
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

以示例中 thunk 为例,看下最终的 dispatch 是什么样子。下面是 redux-thunk 源码。

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

// es5 的语法形式
function createThunkMiddleware(extraArgument) {
  return function (_ref) {
    var dispatch = _ref.dispatch,
        getState = _ref.getState;
    return function (next) {
      return function (action) {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }

        return next(action);
      };
    };
  };
}

按照上面的逻辑,将这个函数传入其中:

const chain = [thunk(middlewareAPI)];
dispatch = thunk(middlewareAPI)(store.dispatch);

// 将其展开
dispatch = action => {
  if (typeof action === 'function') {
    return action(dispatch, getState, undefined);
  }
  return store.dispatch(action);
};

当示例中调用同步的 action 的时候 store.dispatch(selectSubreddit('reactjs')),实际上执行的是 redux 自己原本的 dispatch 方法。下面看下异步的 action 。

// actions.js
function fetchPosts(subreddit) {
  return dispatch => {
    dispatch(requestPosts(subreddit))
    return fetch(`http://www.reddit.com/r/${subreddit}.json`)
      .then(response => response.json())
      .then(json => dispatch(receivePosts(subreddit, json)))
  }
}

// index.js
store
  .dispatch(fetchPosts('reactjs'))
  .then(() => console.log(store.getState())
)

异步的 action 返回的是一个函数,当 dispatch 时,就会执行这个函数,这个函数返回的是一个 Promise,里面有相关的请求处理,等结果返回后,还是调用 redux 最基本的 dispatch 方法触发 state 更新。

compose

从右到左组合函数。下面是源码:

export default function compose(...funcs) {
    if (funcs.length === 0) {
        // infer the argument type so it is usable in inference down the line
        return (arg) => arg;
    }
    if (funcs.length === 1) {
        return funcs[0];
    }
    return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

就像注释里面的举例,将 compose(f,g,h)(...args) 转换为 f(g(h(...args)))

思考

看完 Redux 的主要逻辑后,一开始觉得有点惊讶,它好的地方除了文档上的描述的优点,还有什么?后来想到里面强调的一个“纯函数”的概念,去了解后才感觉到其中妙的地方。

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

纯函数带来的好处:

  • 可移植性
  • 可测试性
  • 合理性
  • 并行代码

再来看一下在学校里面函数的概念:

函数是不同数值之间的特殊关系:每一个输入值返回且只返回一个输出值。。

纯函数就是数学上的函数。这一点让我想到平时编程的时候,好像也写过类似 y=x+1 的程序,但 Redux 在实现的时候不仅运用这种思想,在整体使用方式上,个人觉得也是遵循了这种思想, Action、Reducer 分别相当于函数中的 x、y,Redux 相当于函数的对应关系。

回想起之前工作中碰到使用 MVC 模式,其中掺杂了不同的相互调用,数据状态混乱,排查问题耗时麻烦。现在想想如果也采用这种函数的思想,也是可以在一定程度上,达到状态清晰的效果。还有就是对抽取公共方法的思考,为了兼容各种情况,对于相同的输入可能得到不同的结果,这种方式是否合理,使用纯函数的方式是否更好。

把简单的思想巧妙的运用和扩展,这是个人感觉 Redux 最妙的地方。

参考资料

🗑️

看了一个纪录片《地平线系列之:阻止男性自杀》,上映的时间是 2019.9,里面有些信息:

  • 在英国,四分之三的自杀者是男性
  • 全世界每 40 秒就有一人自杀身亡
  • 自杀会传染,尤其是身边有自杀成功案例
  • 在英国每一起自杀平均会造成 150 万英镑的损失

在中国这似乎是个很忌讳的问题。

37-poster

@XXHolic XXHolic added the read label Jun 21, 2019
This was referenced May 1, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant