首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >redux你用对了吗?

redux你用对了吗?

作者头像
astonishqft
发布于 2022-05-10 20:34:04
发布于 2022-05-10 20:34:04
9680
举报

redux 的三大原则

redux 的开发和使用必须要遵循三大原则,即:

  1. 单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中
  2. State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
  3. 使用纯函数来执行修改: 为了描述 action 如何改变 state tree ,你需要编写 reducers

关于第一点很容易理解,整个应用应当只有一个 store,全局唯一的 store 有利于更好的管理全局的状态,方便开发调试,对实现“撤销”、“重做”这类的功能也更加方便。

第二点,state 是只读的,因此,我们在任何时候都不应该直接修改 state,唯一能改变 state 的方法就是通过 dispatch 一个 action,间接的来修改,以此来保证对大型应用的状态进行有效的管理。

第三点,要想修改 state,必要要编写 reducer 来进行,reducer 必须是纯函数,reducer 接收先前的 state 和 action,并且返回一个全新的 state。

什么是纯函数?

前面我们介绍 redux 三大原则的时候提到过,修改 state 要编写 reducer,且 reducer 必须是一个纯函数,那么问题来了,什么是纯函数呢?

维基百科里是这么定义纯函数的:

在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数

  • 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

简单总结一下,如果一个函数的返回结果只依赖他的参数,并且在执行过程中没有副作用,我们就把这个函数定义为纯函数。

举个🌰:

代码语言:javascript
复制
const x = 1;
function add(a) {
  return a + x;
}
foo(1); // 2

函数 add 就不是一个纯函数,因为函数 add 的返回值依赖外部变量 x,输入一定的情况下,输出结果不确定。

我们把上面的例子稍微调整下:

代码语言:javascript
复制
const x = 1;
function add(a, b) {
  return a + b;
}
foo(1, 2); // 3

现在的函数就是一个纯函数,因为函数 add 的返回值永远只依赖他的入参 a 和 b,不管外部变量 x 的值如何变化,也不会影响到函数 add 的返回值。

再看一个例子:

代码语言:javascript
复制
function add(obj, a) {
  obj.x = 1;
  return obj.x + a;
}
const temp = {
  x: 4,
  y: 9,
}
add(temp, 10); // 11
console.log(temp.x); // 1

现在,我们给函数 add 传一个对象,并且,在函数 add 内部对这个对象的某个属性进行修改,在执行函数 add 的时候修改了外部传进来的 temp 对象,即产生了副作用,因此这不是一个纯函数。

除了上面说的在纯函数内部不能修改外部变量,在函数内部调用 Dom api 修改页面、发送 ajax 请求,甚至调用 console.log 打印日志都是副作用,在纯函数中都是禁止的,也就说,在纯函数内部我们一般只做计算数据的工作,计算的时候不能依赖函数参数以外的数据。

为什么reducer需要返回一个全新的state

上面我们介绍了什么是纯函数,redux 里面规定 reducer 必须是一个纯函数,并且每个纯函数需要返回一个全新的state,那么这里大家肯定就有一个疑问,为什么 reducer 必须要返回一个全新的 state,直接修改完了 state 再返回不行吗?

带着这个问题,我们来举个例子验证下,假如我们在一个 reducer 里面直接修改 state 的值,再返回修改后的 state 会发生什么。

我们定义三个组件:App、Title 和Content。App 作为Title 和Content 的父组件,有一个默认的 state 状态树,结构如下:

初始state:

代码语言:javascript
复制
{
  book: {
    title: {
      tip: '我是标题',
      color: 'red',
    },
    content: {
      tip: '我是内容',
      color: 'blue',
    },
  }
}

Title组件:

代码语言:javascript
复制
const Title = ({ title }) => {
   console.log('render Title');
   return <div style={{ color: title.color }}>{title.tip}</div>;
}

Content组件:

代码语言:javascript
复制
const Content = ({ content }) => {
   console.log('render Content');
   return <div style={{ color: content.color }}>{content.tip}</div>;
}

App组件:

代码语言:javascript
复制
const App = ({ book, dispatch }) => {
  const changeTitleTip = () => {
    dispatch({
      type: 'book/changeTitleTip',
      payload: {
        title: {
          tip: '修改后的title',
          color: 'green',
        },
      },
    });
  };

  console.log('render App');
  return (
    <div>
      <Button onClick={changeTitleTip}>修改title名称</Button>
      <Title title={book.title} />
      <Content content={book.content} />
    </div>
  );
};

reducer:

代码语言:javascript
复制
reducers: {
  changeTitleTip(state, { payload }) {
    const { title } = payload;
    state.title = title;
    return state;
  },
}

demo非常简单,我们在 App 组件里面触发一个 dispatch,发送一个 action,调用 reducer 来修改 state 里面的 title,我们点击“修改title名称”按钮,发现组件并没有按照我们的预期发生变化,但是查看state里面的数据发现,state的值却变化了。

错误示例 页面并没有如预期发生变化:

错误示例 这个例子很好的验证了 redux 的说法,我们不能直接修改 state,并返回。

现在调整下 reducer,通过 ... 运算符重新新建一个对象,然后把 state 所有的属性都复制到新的对象中,我们禁止直接修改原来的对象,一旦你要修改某些属性,你就得把修改路径上的所有对象复制一遍,例如,我们不写下面的修改代码:

代码语言:javascript
复制
state.title.text = 'hello'

取而代之的是,我们新建一个 state,新建 state.title,新建 state.title.tip。这样做的好处是可以实现共享结构的对象。

比如,state 和 newState 是两个不同的对象,这两个对象里面的 content 属性在我们的场景中是不需要修改的,因此 content 属性可以指向同一个对象,但是因为 title 被一个新的对象覆盖了,所以它们的 title 属性指向的对象是不同的,

使用一个树状结构来表示对象结构的话,结构如下如所示:

共享结构

现在的 reducer:

代码语言:javascript
复制
reducers: {
  changeTitleTip(state, { payload }) {
    const { title } = payload;
    let newState = { // 新建一个 newState
      ...state, // 复制 state 里面的内容
      title: { // 用一个新的对象覆盖原来的 title 属性
        ...state.title, // 复制原来 title 对象里面的内容
        tip: 'hello' // 覆盖 tip 属性
      }
    }
    return newState;
  }
}

重新点击 “修改title名称” 按钮,我们想要的效果就可以实现了。

修改后的效果 好了,知道结果之后我们来稍微探究下背后的原因。

查看 redux 的 combineReducers 源代码

代码语言:javascript
复制
let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
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 actionType = action && action.type
    throw new Error(
      `When called with an action of type ${
        actionType ? `"${String(actionType)}"` : '(unknown type)'
      }, the slice reducer for key "${key}" returned undefined. ` +
        `To ignore an action, you must explicitly return the previous state. ` +
        `If you want this reducer to hold no value, you can return null instead of undefined.`
    )
  }
  nextState[key] = nextStateForKey
  hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
  hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state

我们发现,combineReducers 内部通过 hasChanged = hasChanged || nextStateForKey !== previousStateForKey 来比较新旧两个对象是否一致,来判断返回 nextState 还是 state,出于性能考虑,redux 直接采用了浅比较,也就是说比较的是两个对象的引用地址,所以,当 reducer 函数直接返回旧的 state 对象时,这里的浅比较就会失败,redux 认为没有任何改变,从而导致页面更新出现某些意料之外的事情。

immer

上面我们已经分析了 redux 里面的 reducer 为什么要返回一个全新的 state,但是,如果按照上面 reducer 的写法,要修改的 state 树层级深了之后,修改起来无疑是非常麻烦的,那么有没有什么快捷的方式可以方便我们直接修改 state 呢?

答案是有的。

immer 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对 js 不可变数据结构的需求。当然,除了 immer 之外,还有别的库也同样能解决我们的问题,但是 immer 应该是最简单也是最容易上手的一个库之一了。

如果你的工程使用的是dva,那么可以直接开启 dva-immer,就可以简化 state 的写法。上面的例子就可以这么写:

代码语言:javascript
复制
reducers: {
  changeTitleTip(state, { payload }) {
    const { title } = payload;
    state.title = title;
  }
}

或者直接使用 immer 库来改进我们的 reducer 写法:

安装:

代码语言:javascript
复制
yarn add immer

使用:

代码语言:javascript
复制
import produce from "immer";

const reducer = (state, action) => produce(state, draft => {
  const { title } = payload;
  draft.title = title;
});

总结

本篇文章重点介绍了 redux 的相关概念,什么是纯函数,以及为什么 reducer 需要返回一个全新的 state ?从源码角度分析了需要返回全新state的原因,最后引入了immer库,引入了 immutable 概念,redux 配合 immer 可以方便我们便捷高效的用好 redux。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-11,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • redux 的三大原则
  • 什么是纯函数?
  • 为什么reducer需要返回一个全新的state
  • immer
  • 总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档