Hooks 时代下的状态管理方案

date
Apr 1, 2022
slug
react-hooks-state
status
Published
tags
React
React Hooks
summary
在 React 推出 React Hooks 之前我们最常会使用 Redux 来做状态管理方案,集中化管理数据,可预测性,丰富的周边工具。几乎就是 React 下状态管理的正确答案。但是使用广泛并不意味着没有缺点:相对复杂的概念,繁重的模板代码,在大型工程下姑且可以作为最佳实践一般,规范不同成员的编写。但是小工程用起来总感觉是 短袖套棉袄,工程本身没多少逻辑代码,硬加了很多 Redux 的模板代码
type
Post

前言

在 React 推出 React Hooks 之前我们最常会使用 Redux 来做状态管理方案,集中化管理数据,可预测性,丰富的周边工具。几乎就是 React 下状态管理的正确答案。但是使用广泛并不意味着没有缺点:相对复杂的概念,繁重的模板代码,在大型工程下姑且可以作为最佳实践一般,规范不同成员的编写。但是小工程用起来总感觉是 短袖套棉袄,工程本身没多少逻辑代码,硬加了很多 Redux 的模板代码
Redux
核心流程图:
notion image
概念:
  • Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个 Store。
  • State:Store 对象包含所有数据,如果想得到某个时点的数据,就要对 Store 生成快照,这种时点的数据集合,就叫做 State。
  • Action:State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
  • Action Creator:View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦,所以我们定义一个函数来生成 Action,这个函数就叫 Action Creator。
  • Reducer:Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
  • dispatch:是 View 发出 Action 的唯一方法。
一个例子:
// action.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';

export function increaseCount() {
  return ({ type: INCREMENT});
}

export function decreaseCount() {
  return ({ type: DECREMENT});
}

export function resetCount() {
  return ({ type: RESET});
}

// reducer.js
import {INCREMENT, DECREMENT, RESET} from '../actions/index'

const INITIAL_STATE = {
  count: 0,
  history: [],
}

function handleChange(state, change) {
  const {count, history} = state;
  return ({
    count: count + change,
    history: [count + change, ...history]
  })
}

export default function counter(state = INITIAL_STATE, action) {
  const {count, history} = state;
  switch(action.type) {
    case INCREMENT:
      return handleChange(state, 1);
    case DECREMENT:
      return handleChange(state, -1);
    case RESET:
      return (INITIAL_STATE)
    default:
      return state;
  }
}

// store.js
export const CounterStore = createStore(reducers)
原理:
核心代码在 createStore.js ,主要关注两个函数
  • subscribe 函数
    • 注册监听事件,然后返回取消订阅的函数,所有的订阅函数统一放一个数组里(nextListeners,只维护这个数组
  • dispatch 函数
    • 调用 Reducer,传参(currentState,action)。
    • 按顺序执行 listener。
    • 返回 action。
React Hooks 来临,得益于良好的逻辑封装能力和简洁的 API(最突出的就是使用极其方便的 useContext ),近似代数效应的表现
什么是代数效应?
代数效应函数式编程中的一个概念,用于将副作用函数调用中分离,将代码中的 what 和 how 分开
一个例子:
function getTotalPicNum(user1, user2) {
  const num1 = getPicNum(user1);
  const num2 = getPicNum(user2);

  return picNum1 + picNum2;
}
在这个函数里我们计算图片总数,但是不需要关心 getPicNum 的实现,单一职责。
假如获取图片总数需要请求的方式呢?
async function getTotalPicNum(user1, user2) {
  const num1 = await getPicNum(user1);
  const num2 = await getPicNum(user2);

  return picNum1 + picNum2;
}
但是 async / await 是具有传染性的,为此你不得不也改变调用 getTotalPicNum 函数的方式,它从同步变成了异步
我们假设有一个实现了代数效应的语法 perform ,它能够在执行到的时候自动暂停原有代码,在获得 picNum 之后再恢复原有代码的执行
function getPicNum(name) {
  const picNum = perform name;
  return picNum;
}

try {
  getTotalPicNum('xiaoming', 'xiaohong');
} handle (who) {
	// 或者执行一个请求
  switch (who) {
    case 'xiaoming':
      resume with 230;
    case 'xiaohong':
      resume with 122;
    default:
      resume with 0;
  }
}
React Hooks 在 React 函数编程中实现了类似代数效应的效果,你无需关心 useStateuseReduceruseRef这样的Hook 是如何保存你的数据。 但它不是真正的代数效应
 
通俗易懂的代数效应
朋友你听说过 代数效应 吗? 我想知道这是什么以及为什么我需要了解它,于是查阅了 一些 pdfs ,越看越一头雾水(里面一些比较学术的 pdfs 看得我昏昏欲睡)。初次入坑失败。 但我的同事 Sebastian 一直 参 考 它们 作为我们在React中做的一些东西的心智模型。(Sebastian 工作于 React 团队并且贡献过很多想法,包括 Hooks 和 Suspense。)在某种意义上,这已经变成了 React 团队的一个梗,很多时候我们的谈话都以此结尾: 事实证明,代数效应是一个很酷的概念,也没有我看了一些 pdfs 之后想的那么可怕。 如果你只是用 React, 其实完全没有必要去了解这些概念 -- 但如果你像我一样对此感到好奇,就请继续读下去吧。 (免责声明:我并不是一个编程语言的研究人员,在解释的过程中可能会搞混一些东西。在这个话题上我不是权威人士,所以如果发现哪里不对请告诉我!) 代数效应是一项研究中的编程语言特性。这意味着 不像 if,functions,甚至比较新的 async / await,你也许还不能在生产中真正的使用它。只有 少数专为研究这些特性而创造的 语言支持它们的使用。OCaml 将这些特性投入生产,虽然取得了一些进展但也......尚在 进行中。换句话来说就是, 触不可及 。 修正:有人指出 LISP 中 有类似的实现 ,所以如果你写
通俗易懂的代数效应
因此一大批基于 Hooks 的状态管理库诞生,包括 React-Redux 也提供了基于 Hooks 的 API 使用,推出了 redux-toolkit
redux-tookit 简化的例子
// counterSlice.js
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

export interface CounterState {
  count: number
}

const initialState: CounterState = {
  count: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.count += 1
    },
    decrement: (state) => {
      state.count -= 1
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.count += action.payload
    },
  },
})

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

// store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch

为什么会有这篇文章

一次偶然的机会我遇见了 unstate-next ,轻巧简洁,借助 useContext 就可以实现基础的状态管理能力。虽然使用上依然会遇到一些小问题,但是具有十足的启发性,那 Hooks 下什么是理想的状态管理方案,这里会介绍下现在主流的几个状态管理仓库

State 👋

什么是 状态?
jQuery 时代,JS 代码混杂 DOM 操作,代码长且难以理解
面条式代码,程序代码就像面条一样扭曲纠结 😫
$("#btn1").click(function(){
	$("p").append(" <b>Appended text</b>.");
	var d = Math.floor(t/1000/60/60/24) <= 0 ? 0 : Math.floor(t/1000/60/60/24)
	$("#t_d").html(d);
	if (d <= 0) {
    $("#day").hide();
    $('#second').show();
  }
});
notion image
随着现代前端框架的来临,它们都有一个重要的理念:数据驱动视图
由原来编写过程的 “命令式代码“ 改为 “操作各种数据”,而这些变化的数据就是 状态(State)

React State

Class Component 时的 state 是 this.state
Function Component 时的 state 是 useState / useReducer
为了避免过于复杂的应用代码,我们会通过“拆分”,形成多个组件,之间通过 props 传递 state 进行通信。同时遵守“单向数据流
React 也引入 Context 解决组件间状态通信导致 props 层层传递复杂的问题

我想用 Context 做状态管理方案

假设现在我们有一个 Foo 组件,组件内部有一个 count state
const Foo = () => {
  const [count, setCount] = React.useState(0);
  return (
    <>
      <h1>{count}</h1>
      <button onClick={() => setCount(v => v + 1)}>Click Me!</button>
    </>
  );
};
现在我们希望其他组件也能访问到这个 count state
const CountContext = React.createContext(null);
const CountProvider = ({ children }) => {
  const [value] = React.useState(0);
  return (
    <Context.Provider value={value}>
      {children}
    </Context.Provider>
  );
};

const Foo = () => {
  const count = React.useContext(CountContext);
  return (
    <>
      <h1>{count}</h1>
    </>
  );
};

const Bar = () => {
  const count = React.useContext(CountContext);
  return <h2>{count % 2 ? 'Even' : 'Odd'}</h2>
};

const Buz = () => (
  <CountProvider>
      <Foo />
      <Bar />
  </CountProvider>
);
进一步,我们更好的方式是将 Provider 提升到顶部
const App = () => (
  <CountProvider>
    <Layout />
  </CountProvider>
);
进一步的我们发现,虽然我们共享了状态,但是没办法修改它?
将修改 state 的方法跟随 state 一起通过 Context 传下去
这时候我们写的代码已经非常接近 unstate-next 了

unstate-next

unstate-next 可以理解为提供了一种使用 context 作为状态管理的范式
源码非常简单,只有不到40行,并且仅 2 个 API(useContainercreateContainer
Example
import React, { useState } from "react"
import { createContainer } from "unstated-next"

function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <span>{counter.count}</span>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {
  return (
    <Counter.Provider>
      <CounterDisplay />
      <Counter.Provider initialState={2}>
        <div>
          <div>
            <CounterDisplay />
          </div>
        </div>
      </Counter.Provider>
    </Counter.Provider>
  )
}
使用的时候我们传入 封装了 state 和修改 state 的自定义 Hooks,返回 container,带有 Provider 和 useContainer

问题

provider 不支持 displayName
优化 DevTools 的显示
notion image
需要特别注意 context 重复渲染导致的性能问题
一个简单的 Context 例子:
Context 下的组件会因为 父组件的 render 而 render,为了避免重复渲染,可以采取的措施
  • 子组件提升为 提升为 props.children
  • W memo”,对 context value 使用 useMemo,对子组件使用 React.memo 包裹
但是如果我们把所有 value 都放在一个 context 中,会导致不管子组件是否用到它,都会导致重渲染
  • Split contexts
    • 拆分 Contexts,对于不同上下文背景的 Contexts 进行拆分
    • 将多变的和不变的 Contexts 分开,让不变的 Contexts 在外层,多变的 Contexts 在内层
借助 Split contexts 依然无法改变的就是 state 和 setState 的单一依赖重复渲染问题
随着你的应用 Provider 越来越多,它会变成这个样子,多美妙的图形【误
const App = () => (
  <Context1Provider>
    <Context2Provider>
      <Context3Provider>
        <Context4Provider>
          <Context5Provider>
            <Context6Provider>
              <Context7Provider>
                <Context8Provider>
                  <Context9Provider>
                    <Context10Provider>
                      <Layout />
                    </Context10Provider>
                  </Context9Provider>
                </Context8Provider>
              </Context7Provider>
            </Context6Provider>
          </Context5Provider>
        </Context4Provider>
      </Context3Provider>
    </Context2Provider>
  </Context1Provider>
);

constate

constate 使用方式和 unstate-next 非常相似,在 unstate-next 的基础上改进了几个问题
  • 提供了 displayName
  • 提供了selectors API,更加精细的使用 context value
Example
import React, { useState, useCallback } from "react";
import constate from "constate";

function useCounter() {
  const [count, setCount] = useState(0);
  // increment's reference identity will never change
  const increment = useCallback(() => setCount(prev => prev + 1), []);
  return { count, increment };
}

const [Provider, useCount, useIncrement] = constate(
  useCounter,
  value => value.count, // becomes useCount
  value => value.increment // becomes useIncrement
);

function Button() {
  // since increment never changes, this will never trigger a re-render
  const increment = useIncrement();
  return <button onClick={increment}>+</button>;
}

function Count() {
  const count = useCount();
  return <span>{count}</span>;
}

核心代码

根据 传入的 selectors 为每个创建 context
if (selectors.length) {
  selectors.forEach((selector) => createContext(selector.name));
} else {
  createContext(useValue.name);
}
根据创建的 contexts 自动生成嵌套的 Provider
const Provider: React.FC<Props> = ({ children, ...props }) => {
    const value = useValue(props as Props);
    let element = children as React.ReactElement;
    for (let i = 0; i < contexts.length; i += 1) {
      const context = contexts[i];
      const selector = selectors[i] || ((v) => v);
      element = (
        <context.Provider value={selector(value)}>{element}</context.Provider>
      );
    }
    return element;
  };

zustand

2021最🔥的状态管理库
zustand 采用的全局的状态管理方案。基于观察者模式 。API 清晰简单,不需要额外处理 Context 的重复渲染的问题,不需要 Provider ,相对的通过 forceUpdate 实现组件的更新。脱离于 React 自身的状态,支持非 React 环境使用。
Example
import create from 'zustand'

const useStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 })
}))

function BearCounter() {
  const bears = useStore(state => state.bears)
  return <h1>{bears} around here ...</h1>
}

function Controls() {
  const increasePopulation = useStore(state => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}
 

核心代码

Vanilla.ts
观察者模式的实现,提供了 setState、subscribe 、getState 、destroy 方法。并且前两者提供了 selector 和 equalityFn 参数
setState 代码
const setState: SetState<TState> = (partial, replace) => {
  const nextState = typeof partial === 'function' ? partial(state) : partial
  if (nextState !== state) {
    const previousState = state
    state = replace ? (nextState as TState) : Object.assign({}, state, nextState)
    listeners.forEach((listener) => listener(state, previousState))
  }
}
getState 代码
const getState: GetState<TState> = () => state
subscribe 代码
const subscribe: Subscribe<TState> = <StateSlice>(
  listener: StateListener<TState> | StateSliceListener<StateSlice>,
  selector?: StateSelector<TState, StateSlice>,
  equalityFn?: EqualityChecker<StateSlice>
) => {
  if (selector || equalityFn) {
    return subscribeWithSelector(
      listener as StateSliceListener<StateSlice>,
      selector,
      equalityFn
    )
  }
  listeners.add(listener as StateListener<TState>)
  // Unsubscribe
  return () => listeners.delete(listener as StateListener<TState>)
}
createStore
function createStore(createState) {
  let state: TState
  const setState = /** ... */
  const getState = /** ... */
  /** ... */
  const api = { setState, getState, subscribe, destroy }
  state = createState(setState, getState, api)
  return api
}
react.ts
基于 React Hooks API 接口的封装,实现状态的注册和更新,通过 forceUpdate 对组件重渲染
组件重渲染的方式
const [, forceUpdate] = useReducer((c) => c + 1, 0) as [never, () => void]
判断状态是否更新
const state = api.getState()
newStateSlice = selector(state)
hasNewStateSlice = !equalityFn(
  currentSliceRef.current as StateSlice,
  newStateSlice
)
useIsomorphicLayoutEffect(() => {
  if (hasNewStateSlice) {
    currentSliceRef.current = newStateSlice as StateSlice
  }
  stateRef.current = state
  selectorRef.current = selector
  equalityFnRef.current = equalityFn
  erroredRef.current = false
})
状态更新通知组件重渲染
useIsomorphicLayoutEffect(() => {
  const listener = () => {
    try {
      const nextState = api.getState()
      const nextStateSlice = selectorRef.current(nextState)
      if (!equalityFnRef.current(currentSliceRef.current as StateSlice, nextStateSlice)) {
        stateRef.current = nextState
        currentSliceRef.current = nextStateSlice
        forceUpdate()
      }
    } catch (error) {
      erroredRef.current = true
      forceUpdate()
    }
  }
  const unsubscribe = api.subscribe(listener)
  if (api.getState() !== stateBeforeSubscriptionRef.current) {
    listener() // state has changed before subscription
  }
  return unsubscribe
}, [])
context.ts
借助 React Context 分发 store ,实现创建多个互不干扰的 store 实例
const useStore: UseContextStore<TState> = <StateSlice>(
  selector?: StateSelector<TState, StateSlice>,
  equalityFn = Object.is
) => {
  const useProviderStore = useContext(ZustandContext)
  return useProviderStore(
    selector as StateSelector<TState, StateSlice>,
    equalityFn
  )
}
middleware.ts
中间件的本质是各种函数根据顺序互相嵌套调用,在 Zustand 中,中间件是将 setState 函数包裹起来,Zustand 天生就是使用这种函数式的设计
以内置的持久化状态中间件为例:

// 使用的例子
export const useStore = create(persist(
  (set, get) => ({
    fishes: 0,
    addAFish: () => set({ fishes: get().fishes + 1 })
  }),
  {
    name: "food-storage", // unique name
    getStorage: () => sessionStorage, // (optional) by default, 'localStorage' is used
  }
))

// middleware.ts 实现缩略代码
// 修改传入 options 的 set 函数
const configResult = config(
	((...args) => {
	  set(...args)
	  void setItem()
	}) as CustomSetState,
	get,
	api
)
 
 

jotai

同样的全局状态管理方案,跟 zustand 是同一个作者。借鉴了 Recoil 的原子化概念
同样 API 也十分简单, atomuseAtom。通过 atom 创建原子化的数据源,调用useAtom 传入 atom 返回对应的状态值和修改方法。
import { atom } from 'jotai'

const countAtom = atom(0)
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])
const mangaAtom = atom({ 'Dragon Ball': 1984, 'One Piece': 1997, Naruto: 1999 })

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  return (
    <h1>
      {count}
      <button onClick={() => setCount(c => c + 1)}>one up</button>
		</h1>
	)
}
原子化的数据有非常强的派生能力
const doubledCountAtom = atom((get) => get(countAtom) * 2)

function DoubleCounter() {
  const [doubledCount] = useAtom(doubledCountAtom)
  return <h2>{doubledCount}</h2>
}
同时可以通过 getset 修改原子状态的值和修改的方法
const decrementCountAtom = atom(
  (get) => get(countAtom),
  (get, set, _arg) => set(countAtom, get(countAtom) - 1),
)

function Counter() {
  const [count, decrement] = useAtom(decrementCountAtom)
  return (
    <h1>
      {count}
      <button onClick={decrement}>Decrease</button>
		</h1>
	)
}
虽然是全局状态管理方案,但同时支持 Provider 使用
notion image
 
方便查看源码的理解:
  • atom 类比 key
  • state 类比 store
  • atomState 类比 value
原理:官方文档给的最简版本
import { useState, useEffect } from "react";

// atom 函数返回一个配置对象,其中包含初始值
export const atom = (initialValue) => ({ init: initialValue });

// 跟踪原子的状态
// 使用 WeakMap 存储 atomState
// 把 atom 当做key, atomState 当做 value 存储
const atomStateMap = new WeakMap();
const getAtomState = (atom) => {
  let atomState = atomStateMap.get(atom);
  if (!atomState) {
    atomState = { value: atom.init, listeners: new Set() };
    atomStateMap.set(atom, atomState);
  }
  return atomState;
};

// useAtom 钩子类似 useState 
export const useAtom = (atom) => {
  const atomState = getAtomState(atom);
  const [value, setValue] = useState(atomState.value);
  useEffect(() => {
    const callback = () => setValue(atomState.value);
    
		// 添加 atom 的更新回调,就是更新当前 Hooks 的 Value
    atomState.listeners.add(callback);
    callback();
    return () => atomState.listeners.delete(callback);
  }, [atomState]);

  const setAtom = (nextValue) => {
    atomState.value = nextValue;

    // 更新时执行所有 listeners
    atomState.listeners.forEach((l) => l());
  };

  return [value, setAtom];
};
支持派生 atom
const atomState = {
  value: atom.init,
  listeners: new Set(),
  dependents: new Set()
};

valtio

借助 Proxy & Reflect API 代理状态
import { proxy, useSnapshot } from 'valtio'

const state = proxy({ count: 0, text: 'hello' })

// This will re-render on `state.count` change but not on `state.text` change
function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )
}
核心代码
const handler = {
  get(target: T, prop: string | symbol, receiver: any) {
    // 省略部分代码
    return Reflect.get(target, prop, receiver)
  },
  deleteProperty(target: T, prop: string | symbol) {
    const prevValue = Reflect.get(target, prop)
    const childListeners = prevValue?.[LISTENERS]
    if (childListeners) {
      childListeners.delete(popPropListener(prop))
    }
    const deleted = Reflect.deleteProperty(target, prop)
    if (deleted) {
      notifyUpdate(['delete', [prop], prevValue])
    }
    return deleted
  },
  is: Object.is,
  canProxy,
  set(target: T, prop: string | symbol, value: any, receiver: any) {
    const prevValue = Reflect.get(target, prop, receiver)
    if (this.is(prevValue, value)) {
      return true
    }
    const childListeners = prevValue?.[LISTENERS]
    if (childListeners) {
      childListeners.delete(popPropListener(prop))
    }
    if (isObject(value)) {
      value = getUntracked(value) || value
    }
    let nextValue: any
    if (Object.getOwnPropertyDescriptor(target, prop)?.set) {
      nextValue = value
    }
		// 省略部分代码
    Reflect.set(target, prop, nextValue, receiver)
    notifyUpdate(['set', [prop], value, prevValue])
    return true
  },
}
const proxyObject = new Proxy(baseObject, handler)

总结

对于一个 状态管理方案 我们知道一定存在两个关键的概念:
  • 数据源
  • 数据消费
对于数据源:
以上两个库 都是完全基于 Context API 实现 value 传递,同时由于 Provider 可以放在应用的任何地方,也可以称作 局部状态管理方案,与之对应的就有 全局状态管理方案
(基于 Context API 自带 state 变化 重新渲染的机制,因此 全局状态管理方案 需要额外处理如何跟踪 state 的变化并重新渲染组件)
对于数据消费:
类似上面基于 context 和 redux 的不可变(immutable)数据消费
还有 Mobx 为代表的可变(mutable)数据
数据消费方式的不同也决定了他们 如何追踪 state 的变化,不可变(immutable)数据基于 发布订阅模式,而可变(mutable)数据基于 Proxy 代理 的方式
 
 
 

Ref

React 状态管理碎碎念
提到状态管理,可能大多数人的第一反应就是 redux、dva、mobx,可是随着时间的推移,React 的能力不断增强,前端的工程化日益成熟,应用的形态渐渐复杂,如今,或许我们应该重新审视一下,什么是状态管理,又如何做好状态管理。 社区中对 React 状态管理方案的讨论从未停息过,特别是自从 Hooks 诞生以来,各种"新颖"的状态管理方案层出不穷,为了能更理性的看待这个问题,我们不妨把那些具体的框架/库都抛在脑后,先来聊一聊,抽象层面上的"状态管理"到底意味着什么。 给一个抽象的东西下定义,是非常难的。例如我们在讨论"书是什么"时,有些人可能会认为书就是纸张构成的一本印刷册,可有的人认为手写的一本稿子也算是书,而又有的人会认为电子产品中的一部文字的集合也算是广义上的书。 空谈概念的定义是没有什么意义的,很多概念的边界是模糊的,与其浪费时间去争出一个大家都认同的"边界",不如先找出一个粗略的定义,毕竟我们只是想借助对定义的探讨,来反观现实。 在我个人的观念里,状态管理主要有两个方面的职能: 数据的共享 在 React 应用中,在组件间优雅的共享一些数据并非易事,而各种状态管理工具都给出了自己对"数据共享"的标准方案 逻辑的组织 状态不单单是数据项的堆砌,更重要的是把各项数据之间的逻辑、数据与其他系统模块之间的互动逻辑进行组织,例如 A 需要在用户点击按钮时发生变化,而 B 又需要在 A 变化时跟着变化(可能是同步的也可能是异步的) 不知道这是不是 redux 或 mobx 的初衷,但这确实是我一直以来对状态管理工具的" 痛点需求 "。 那么接下来,我们不妨在这两个方面分别讨论: 既然要共享数据,那么最简单粗暴的方案就是直接做成 全局性的(redux 也确实是这么做的),于是很多人的都渐渐形成了一种认知:React 应用中的状态有两种,一种是组件内部的(this.state / useState),另一种是全局的(redux)。但是, 内部状态的反义词从来都不是 全局状态,而是 共享状态,全局只是共享的一种途径罢了,并不能成为其本身。 目前来看,在组件间进行状态的共享,有两种比较可靠的途径,其一就是刚刚提到的 全局状态共享,而另一种,也是我个人更加倾向的,是 局部状态共享 。React 提供了非常简洁易用的 Context API,用于在一个组件树中共享数据,局部状态共享的思路大致就是来自于此,在一个组件树(而非整个应用)内进行数据共享。 可是这样做又能有什么好处呢? 自治 一个组件树可以完成逻辑的封闭,减轻对全局的依赖和干扰,甚至还对组件的封装非常有益 多例 局部共享的状态也可以像组件(树)一样拥有多例的能力,可以打破全局单例对很多逻辑场景的限制 生命周期 局部状态跟随组件树创建和销毁,干净而且易于维护,最常见的场景是让共享状态的生命周期和页面的生命周期保持同步 当然,反对的声音也是存在的: 不够规范 局部状态满天飞,容易让逻辑变乱,排查问题也会变得非常困难 需要一定的认知成本 局部状态不如全局状态那么直观,虽然灵活但也增加了使用成本 其实大家不难发现,局部状态中"局部"是可大可小的,不论是小至包裹在一个组件外面(虽然不会有人真的这样写代码),还是包裹到整个应用外面,都可以称之为"局部"。因此准确的讲,全局状态只是局部状态的一个 子集、一个 特例 。局部状态这种模式,只是把枷锁解掉了,并非和全局状态互斥。 我觉得状态管理中真正的痛点在于"和数据相粘连的逻辑"如何组织,而 Hooks 提供了一整套完备的管理逻辑的方案,从状态到副作用到性能优化到异步。 在以前,对于组件内部的状态,逻辑组织能力是非常薄弱的,因此很多人求助于把状态抽到外部(例如 redux),因为外部的状态管理器中可以更好的组织业务逻辑。甚至更极端的,会试图把整个应用状态都放在 redux 中,而 React 组件内不允许保留状态。 但是 Hooks 的诞生让组件内部状态的逻辑组织能力得到了大幅提升,我觉得是完全不输甚至更强于 flux 模式的。如果我们能够把组件内部状态和共享状态全部都用 Hooks 进行组织,那我们就再也不需要对一份数据该存放在哪里而感到纠结了,因为我们只需要关心一点:这份数据被谁需要了?是只有这个组件自己,还是多个组件之间? **Hooks 不是为了状态管理而生的,但却可以深刻地改变整个生态。**就像智能手机不是为了购物而生的,但却可以颠覆人们的消费和生活方式。 状态管理之所以一直以来被人们特意强调,一定程度上恰恰是因为它的格格不入,需要被人们去单独学习和特别注意。如果大胆猜测一下的话,我会觉得在不久的将来,我们之前热衷于讨论的"状态管理"这个词,会更多的意味着"状态共享",甚至于销声匿迹。一个 ...
React 状态管理碎碎念
 
 
 

© Jeekdong 2021 - 2024