React

React 是一个用于构建用户界面的 JavaScript 库。

  • 声明式。以声明式编写 UI,通过数据的状态转移来动态更新 UI。
  • 组件化。构建管理自身状态的封装组件,然后对其组合以构成复杂的 UI。通过 Javascript 组件保持状态与 DOM 分离。
  • 一次学习。React 还可以使用 Node 进行服务器渲染,或使用 React Native 开发原生移动应用。

组件(Component)

通过 JSX 我们可以声明式的定义 UI 组件。组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。组件代码表现形式可以是一种函数或者是对象。

可以根据单一功能原则来判定组件的范围,也就是说,一个组件原则上只能负责一个功能。UI(或者说组件结构)大多数时候会与数据模型一一对应,它们都会倾向于遵守相同的信息结构

状态(Props & State)

状态代表某一时刻的数据快照,伴随时间的推移状态也在发生变化,组件会渲染输出不同的 DOM UI。

我们将组件想象成一个函数,props 是传递组件的(类似于函数的形参),而 state 是在组件内被组件自己管理的(类似于在一个函数内声明的变量)。

状态完全由 React 控制的组件称为受控组件,由非 React 控制的称为非受控组件,例如由原生 DOM 控制等。

组件遵循单向的数据流,**所以我们绝不能修改输入的 props。**像纯函数一样工作的组件称为纯组件(PureComponent),纯组件通过浅比较 props 和 state 来确定是否需要重新渲染组件,同样的输入始终返回相同的结果。

除了使用 props 来传递状态控制组件渲染,通过修改内部的 state 也会触发组价重新渲染,内部的 state 通过调用专有的函数触发更新,对 state 的修改可能会被 React 合并成一个调用延迟更新。

有时候我们的组件需要对外部的输入作出响应,通常我们是通过事件回调暴露给外部信息。

一个包含这些元素的示例组件:

// 输入 props
function Form(props) {
   // 内部 state
  const [content, setContent] = useState(‘’);
  
  return (
    <form onSubmit={() => props.onSubmit({ content })}> // 事件回调
     <input type="text" name="firstName" 
     		onChange={ (event) => setName(event.target.value) }  /> // 内部状态修改
      <button type="submit">Submit</button>
    </form>
  );
}

共享及复用

当我们的组件越来越多时,组件间需要共享状态的情形也越来越频繁,这时我们可以将共享状态向上提升以复用状态:

除了向上提升达到复用,还可以通过其它模式达到复用:

  • 使用上下文 Context 为组件间共享数据,而无需为每层组件手动添加 props 并自上而下传递,非常适用于某些类型的属性:(例如:地区偏好,UI 主题)。

  • 使用 Render Props 将一个组件封装的状态或行为共享给其他需要相同状态的组件。

    Render Props 使用一个值为函数的 prop 在组件间共享代码,该函数返回 React 元素,组件内部通过调用此函数来实现自己的渲染逻辑。 并不一定非要使用名为 render 的 prop,也可以使用 children,JSX 的 children 就是元素内部包裹的内容。

    {/*
      使用 render prop 传递内部封装状态给组件
    */}
    <DataProvider render={data => (
      <h1>Hello {data.target}</h1>
    )}/>
    
    {/*
      使用 children prop 传递内部封装状态给组件
    */}
    <DataProvider>
    {
      data => <h1>Hello {data.target}</h1>
    }
    </DataProvider>
    
  • 使用 React Hook

    Hook 使你在无需修改组件结构的情况下复用状态逻辑。 Hook 使用函数式编程使我们的组件更加纯粹干净,将纯函数外的内部状态和副作用都分离出来,并使用相应的 Hook 函数代理,它还由一些方便使用的其它钩子函数。

除了复用状态,通过一些模式也可以实现复用组件逻辑:

  • 高阶组件(Higher-Order Components - HOC)

    高阶组件是参数为组件,返回值为新组件的函数。 有时候不同组件间会有通用的处理逻辑或某种特性,我们可以将它提到高阶组件中而保持原始组件的简单,例如一些副作用:日志记录、数据读取写入、权限控制等等。高阶组件为组件提供某种特性而不侵入组件,并最大化可组合性。

Redux

我们之前提到 Redux 可以帮我们统一管理共享状态,随着 JavaScript 单页应用程序的需求变得越来越复杂,我们的代码必须管理比以往更多的状态。此状态可以包括服务器响应和缓存数据,以及尚未持久化到服务器的本地创建的数据。 UI 状态的复杂性也在增加,因为我们需要管理活动路由,选中的选项卡、加载指示器、分页控件等。但如何在大型应用中合理的管理状态并让状态的变化更有可预测性,Redux 给出了自己的答案。

Redux 可以用三个基本的原则来描述:

  • 单一数据源

你整个应用程式的 state,被储存在一个树状物件放在唯一的 store 里面。

  • State 是只读的

    唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

  • 使用纯函数来执行修改

    为了描述 action 如何改变 state 树 ,你需要编写 reducers

单向数据流

  • state:驱动应用的真实数据源头
  • view:基于当前状态的 UI 声明性描述
  • actions:根据用户输入在应用程序中发生的事件,并触发状态更新

接下来简要介绍 “单向数据流(one-way data flow)”

  • 用 state 来描述应用程序在特定时间点的状况
  • 基于 state 来渲染出 View
  • 当发生某些事情时(例如用户单击按钮),我们会 dispatch 一个 action,通过 reducer 函数根据发生的事情进行更新 state,生成新的 state
  • 基于新的 state 重新渲染 View

动画的方式来表达数据流更新:

数据流更新动画

使用

内部我们可以使用 Redux Toolkit configureStore API 创建一个 Redux store;

Redux state 由 reducer 函数来更新;

  • Reducers 总是通过复制现有状态值,并更新副本来不可变地生成新状态
  • Redux Toolkit createSlice 函数为您生成“slice reducer”逻辑切片,并让您编写“mutable 可变”代码,内部自动将其转变为安全的不可变更新
  • 这些切片化 reducer 函数被添加到 configureStore 中的 reducer 字段中,并定义了 Redux store 中的数据和状态字段名称

React 组件使用 useSelector 钩子从 store 读取数据;

  • 选择器函数接收整个 state 对象,并且返回需要的部分数据
  • 每当 Redux store 更新时,选择器将重新运行,如果它们返回的数据发生更改,则组件将重新渲染

React 组件使用 useDispatch 钩子 dispatch action 来更新 store;

  • createSlice 将为我们添加到切片的每个 reducer 函数生成 action creator 生成器函数
  • 在组件中调用 dispatch(someActionCreator()) 来 dispatch action
  • Reducers 将运行,检查此 action 是否相关,并在适当时返回新状态

异步逻辑

就其本身而言,Redux store 对异步逻辑一无所知。它只知道如何同步 dispatch action,通过调用 root reducer 函数更新状态,并通知 UI 某些事情发生了变化。任何异步都必须发生在 store 之外。

但是,如果您希望通过调度或检查当前 store 状态来使异步逻辑与 store 交互,该怎么办? 这就是 Redux 中间件 的用武之地。它们扩展了 store,并允许您:

  • dispatch action 时执行额外的逻辑(例如打印 action 的日志和状态)
  • 暂停、修改、延迟、替换或停止 dispatch 的 action
  • 编写可以访问 dispatchgetState 的额外代码
  • dispatch 如何接受除普通 action 对象之外的其他值,例如函数和 promise,通过拦截它们并 dispatch 实际 action 对象来代替

Redux 有多种异步中间件,每一种都允许您使用不同的语法编写逻辑。最常见的异步中间件是 redux-thunk,它可以让你编写可能直接包含异步逻辑的普通函数。Redux Toolkit 的 configureStore 功能默认自动设置 thunk 中间件我们推荐使用 thunk 作为 Redux 开发异步逻辑的标准方式

早些时候,我们看到了Redux 的同步数据流是什么样子。当我们引入异步逻辑时,我们添加了一个额外的步骤,中间件可以运行像 AJAX 请求这样的逻辑,然后 dispatch action。这使得异步数据流看起来像这样:

Redux 异步数据流

Thunk middleware 并不是 Redux 处理异步 action 的唯一方式:

计算并记忆衍生数据

useSelector 使用严格 === 的引用比较来比较其结果,因此只要选择器结果是新的引用,组件就会重新渲染!所以使用 map 等产生新引用的方法时要格外注意;

Reselect

或者我们可以使用 Reselect 库提供的 createSelector 创建可记忆的(Memoized)、可组合的 selector 函数。Reselect selectors 可以用来高效地计算 Redux store 里的衍生数据。该库已经包含在 Redux Toolkit 中。

selector 不仅可以接收 Redux store state 作为参数,也可以接收 props。在依赖的 props 和 state 不发生变化的情况下,计算结果会被缓存到该函数的内存中。

⚠️ 当我们组合多个选择器来构建新组合器时,所有“输入选择器”都应该接受相同类型的参数,这一点很重要。

const selectItems = state => state.items
const selectItemId = (state, itemId) => itemId

const selectItemById = createSelector(
  [selectItems, selectItemId],
  (items, itemId) => items[itemId]
)

// 使用 state 和参数 42 调用 
const item = selectItemById(state, 42)

/*
内部,Reselect 会使用该参数调用每一个输入 selector:

const firstArg = selectItems(state, 42);  
const secondArg = selectItemId(state, 42);  
  
const result = outputSelector(firstArg, secondArg);  
return result;  
*/

proxy-memoize

proxy-memoize 是一个相对较新的记忆选择器库,它使用独特的实现方法。它依赖 ES6 Proxy 对象来跟踪尝试读取嵌套值,然后在以后的调用中仅比较嵌套值以查看它们是否已更改。在某些情况下,这可以提供比重新选择更好的结果。

一个很好的例子是派生一系列 todo 描述的选择器:

import { createSelector } from 'reselect'

const selectTodoDescriptionsReselect = createSelector(
  [state => state.todos],
  todos => todos.map(todo => todo.text)
)

不幸的是,如果内部的任何其他值 state.todos 发生变化,这将重新计算派生数组,例如切换 todo.completed 标志。派生数组的内容是相同的,但是由于输入 todos 数组发生了变化,它必须计算一个新的输出数组,并且它有一个新的引用。

相同的选择器 proxy-memoize 可能如下所示:

import memoize from 'proxy-memoize'

const selectTodoDescriptionsProxy = memoize(state =>
  state.todos.map(todo => todo.text)
)

与 Reselect 不同,proxy-memoize 它可以检测到只有字段正在被访问,并且只有在其中一个字段发生更改 todo.text 时才会重新计算其余字段。

它还有一个内置 size 选项,可让您为单个选择器实例设置所需的缓存大小。

它与 Reselect 有一些权衡和不同之处:

  • 所有值都作为单个对象参数传入
  • 它要求环境支持 ES6 Proxy 对象(不支持 IE11)
  • 它更神奇,而 Reselect 更明确
  • 基于 Proxy 的跟踪行为有一些边缘情况
  • 它比较新未被广泛使用

综上所述,Redux 官方鼓励考虑将 proxy-memoize 其用作 Reselect 的可行替代方案

使用技巧

不可变更新

在 React 或者 Redux 中,为了更快速的对比状态变更或保证状态的可预测性,我们一般不直接修改原始的 state,而是以返回副本的形式实现不可变更新。但是对于一个层级比较深的对象的修改和拷贝代码会显得特别繁琐,但是一些库为我们实现了简单易用的语法糖,可以简介的实现不可变更新。这些库包含:immutability-helperobject-path-immutable,或者我们也可以使用 ES6 的解构语法。

将 Redux action 动作暴露给外部

有时候我们想隐藏 action 的分发操作给外部使用,这个组件可能完全不知道 Redux 的存在,所以它也不会拿到 dispatch 函数。例如,我们需要在网络请求 axios 库的前后拦截器中触发一个动作。我们可以使用 bindActionCreators 创建这样的 dispatch action 分发函数。具体可以参考:Redux-bindActionCreators-使用