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

从 redux 状态中取值 #7

Open
liujinxing opened this issue Oct 30, 2019 · 0 comments
Open

从 redux 状态中取值 #7

liujinxing opened this issue Oct 30, 2019 · 0 comments
Labels
help wanted Extra attention is needed

Comments

@liujinxing
Copy link
Member

liujinxing commented Oct 30, 2019

从 Redux Store 中取值

关于这个主题,Redux 官网的专业说法是 Computing Derived Data,即 计算衍生数据。本章节将会探讨如何在项目中处理好这个主题。

选择器 selector

从 Redux Store 中取值的方法,我们称之为选择器。形如下面的函数:

const getTodos = (state) => state.todos;

接收 Redux 状态,从状态中取值,可能会有二次计算,然后返回取值、计算结果。

稍微复杂一点的选择器:

const getTodo = (state, todoId) => state.todos.find(todo => todo.id === todoId);

再复杂一点的选择器:

const getFilteredTodos = (state) => state.todos.filter(
    todo => todo.state === state.filterText
);

在 React 组件中使用选择器

在 React 项目中,可以使用 react-reduxuseSelector ,结合选择器,从 Redux 状态中取值。

function TodoList() {
    const todos = useSelector(getTodos);
}
import { partialRight } from 'lodash';

function TodoItem({ id }) {
    const todo = useSelector(partialRight(getTodo, id));
}
function FilteredTodoList() {
    const todos = useSelector(getVisibleTodos);
}

partialRight

lodash/pratialRight 函数是用来给指定函数添加上固定的右侧参数。如下所示:

function greet(greeting, name) {
  return greeting + ' ' + name;
}

const greetJacking = _.partialRight(greet, 'Jacking');
greetJacking('hi');
// => 'hi Jacking'
greetJacking('你好');
// => '你好 Jacking'

应用了函数编程中的“偏函数”概念。大家可以一点一滴学习函数编程的核心概念。

在其他场景使用选择器

选择器只是简单的函数,所以可以在任何场景下当成函数去使用。例如:

function Demo4() {
  const store = useStore();
  const handleClick = useCallback(() => {
    const todos = getTodos(store.getState());
        
    console.log(`一共有${todos.length}条待办记录。`);
  }, [store]);
    
  return <Button onClick={handleClick}>显示待办条数</Button>;
}

组合使用选择器

组合性是一种理想的编程状态。函数恰恰符合这个预期。如上面的例子中,从状态中取 todos 是一个公共的需求,所以我们稍微改造一下方法:

const getTodos = (state) => state.todos;
const getTodo => (state, todoId) => getTodos(state).find(todo => todo.id === todoId);
const getFilteredTodos = (state) =>
	getTodos(state)
		.filter(todo => todo.state === state.filterText);

上面的实现还是比较简陋的,下面我们使用reselect,再简化一下:

import { createSelector } from 'reselect';

const getTodos = (state) => state.todos;
const getTodo = createSelector(getTodos, (todos, todoId) => todos.find(todo => todo.id === todoId));
const getFilterText = (state) => state.filterText;
const getFilteredTodos = createSelector(
    getTodos, getFilterText,
    (todos, filterText) => todos.filter(todo => todo.state === state.filterText)
);

函数组合(function compose) 是函数编程的核心依据。

缓存选择器

reselect 除了帮助简化选择器组合,还有一个重要特性,即计算缓存。如:

const getTodos = (state) => state.todos;
const getFilterText = (state) => state.filterText;

const getFilteredTodos = createSelector(
    getTodos, getFilterText,
    (todos, filterText) => todos.filter(todo => todo.state === state.filterText)
);

getTodosgetFilterText 都是取对象属性操作,执行效率非常高,可以忽略不计。getFilteredTodos 是数组过滤操作,要耗费更多的执行时间,如果数组比较庞大,频繁执行 getFilteredTodos 函数可能会成为性能瓶颈。

如何解决呢?使用函数计算缓存的技巧即可。我们希望达到一个效果,即:只有当 todosfilterText 发生了变化,才会再次执行 getFilteredTodos ,获取新的值,否则返回上次执行的结果。刚好上面的写法就实现了此效果。

在 form-designer 中的错误做法

form-designer是将从状态中取数据作为React组件的状态这个过程封装成独立的部分,如下所示:

function useTodo(todoId) {
    return useSelector(state => state.todos.find(todo => todo.id === todoId));
}

这样的做法本身没有问题,但是却有一个核心的难题:无法组合使用

比如下面的用法就有很大的问题:

function useTodos() {
    return useSelector(state => state.todos);
}

function useTodo(todoId) {
    const todos = useTodos();
    const todo = todos.find(item => item.id === todoId);
}

useTodo hook 有一个重要的性能问题:当任何一个待办事项变化了,使用 useTodo 的组件都会重绘。可以看下个章节的分析。

如何高效地使用 useSelector

react-redux 从 7.0 开始提供 useSelector 这个从 Redux 状态中提取值的 hook。它的用法是:

function TodoItem({ id }) {
    const todo = useSelector(state => state.todos.find(todo => todo.id === id));
    
    return <div>{todo && todo.title}</div>;
}

有时我们可能会这样去用:

function TodoItem({ id }) {
    const todos = useSelector(state => state.todos);
    const todo = todos.find(item => item.id === id);
    
    return <div>{todo && todo.title}</div>;
}

第二种用法的问题是,当别的待办事项数据发生了变化,当前 TodoItem 组件也会跟着重绘。这样可能会带来严重的性能问题。

重点

使用 useSelector 从 Redux 状态中提取数据时,返回的一定要一步到位直达目的。它的返回值范围越小越好。

会在下面的章节中分析原因。

避免重绘的特例

有时候,useSelector 返回的数据已经是最小范围了,但是发现还是组件重绘太频繁,如下面的示例:

function TodoItem({ id }) {
    const todo = useSelector(state => {
        const todo = todos.find(item => item.id === id);
        if (!todo) {
            return;
        }
        return {
            ...todo,
            selected: state.selectedTodoId === id,
        };
    });
    
    if (!todo) {
        return null;
    }
    
    return <div className={classNames('todo', {
            'todo--selected': todo.selected,
        })}>{todo.title}</div>;
}

你会发现,上面的组件会在 redux 状态有任何变化时重绘。问题就出在 useSelector 的返回值每次都是不一样的对象。我们可以通过下面的三种手段解决问题。

手段1:拆分

function TodoItem({ id }) {
    const selected = useSelector(state => state.selectedTodoId === id);
    const todo = useSelector(state => state.todos.find(item => item.id === id));
    
    if (!todo) {
        return null;
    }
    
    return <div className={classNames('todo', {
            'todo--selected': selected,
        })}>{todo.title}</div>;
}

手段2:使用缓存选择器

selectors.tsx:

import { createSelector } from 'reselect';

const selectedTodoId = (state) => state.selectedTodoId;
const todos = (state) => state.todos;
const todoSelector = createSelector(todos, selectedTodoId, (_, id) => id, (todos, selectedTodoId, id) => {
    const item = todos.find(item => item.id === id);
    if (!item) {
        return null;
    }
    return {
        ...item,
        selected: selectedTodoId === id,
    };
});

Demo.tsx:

function Demo() {
	const todo = useSelector(todoSelector);
    
    if (!todo) {
        return null;
    }
    
    return <div className={classNames('todo', {
            'todo--selected': todo.selected,
        })}>{todo.title}</div>;
}

手段3:使用浅比较函数

useSelector() 会在 Redux 状态发生变化后比较 selector 函数返回值是否与上次返回值相等,它的相等比较用的是Object.is。但是可以指定 useSelector() 使用浅比较,如下所示:

import { shallowEqual } from 'react-redux';

function TodoItem({ id }) {
    const todo = useSelector(state => {
        const todo = todos.find(item => item.id === id);
        if (!todo) {
            return;
        }
        return {
            ...todo,
            selected: state.selectedTodoId === id,
        };
    }, shallowEqual);
    
    if (!todo) {
        return null;
    }
    
    return <div className={classNames('todo', {
            'todo--selected': todo.selected,
        })}>{todo.title}</div>;
}

建议:先采用手段1和手段2,最后没有再好的办法时,才采用手段3。

剖析 useSelector

带着问题看 useSelector 怎么实现的:

  • useSelector 是怎么感知到 Redux 状态变化的?
  • 在感知到 Redux 状态变化时,又是怎么通知组件重绘的?
  • 怎么做到 selector 返回值相同时,不重绘组件的?

先看一个最基本的实现:

function useSelector(selector) {
    const store = useStore();
    const [state, setState] = useState(selector(store));
    
    useEffect(() => {
        return store.subscribe(() => {
            setState(selector(store));
        });
    }, [store]);
    
    return state;
}

这个实现忽略了很多其他细节,实际实现要考虑很多边际问题,这里不展开讨论。

从这个基础实现可以很好地回答上面三个问题:

  • 通过 storesubscribe 方法添加状态变更监听器,解决问题1。
  • 通过setState 新的值,通知组件重绘,解决问题2。
  • 因为setState 设置相同的值,不会引起组件重绘,解决问题3。

在项目中正确使用选择器

选择器放在什么位置?

首先,建议在项目中建立一个 selectors 文件夹,存放选择器,如下:

模块:

MODULE_ROOT
|__ src
    |__ state
    |__ selectors -------> 选择器文件夹
    |__ views
    |__ helpers
    |__ constants.ts
    |__ index.ts

项目:

PROJECT_ROOT
|__ src
    |__ state
    |__ selectors -------> 选择器文件夹
    |__ components
    |__ pages
    |__ helpers
    |__ constants.ts
    |__ index.ts

测试要求

选择器是项目中重要的代码,里面甚至有可能有一些业务逻辑,单元测试是有必要的。因为选择器是简单的纯函数,所以单元测试也是很简单的。选择器特别适用测试驱动开发。要求大家在平时项目中对这部分采用测试驱动开发。

与 useSelector 结合着使用

import React from 'react';
import { useSelector } from 'react-redux';
import { partialRight } from 'lodash';
import todoSelector from '../selectors/todoSelector';

function TodoItem() {
    const todo = useSelector(partialRight(todoSelector));
    
    return <div>{todo.title}</div>;
}

与 reselect 结合着使用

  • 发挥函数的组合特性
  • 利用缓存减少计算,提升性能
const getTodos = (state) => state.todos;
const getFilterText = (state) => state.filterText;

const getFilteredTodos = createSelector(
    getTodos, getFilterText,
    (todos, filterText) => todos.filter(todo => todo.state === state.filterText)
);

性能最后的武器:浅比较

请把这一招作为最后的招数。

import { shallowEqual } from 'react-redux';

function Demo() {
    const todo = useSelector(state => {
        const todo = todos.find(item => item.id === id);
        if (!todo) {
            return;
        }
        return {
            ...todo,
            selected: state.selectedTodoId === id,
        };
    }, shallowEqual);
}

参考资料

@liujinxing liujinxing added the help wanted Extra attention is needed label Oct 30, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant