一个很简单理解的轻量级状态管理

简介: 本文适合对状态管理感兴趣的小伙伴阅读。

一、前言


本文来自广东靓仔的同事--杰哥的投稿,千呼万唤始出来~


相信大家比较熟悉的React状态管理方案有以下几个:

1. dva,umi巨无霸开箱即送,公司项目大爱

2. redux,典中典,当代年轻人不会这么好精力搞了,个人认为缺点比优点多

3.mobx,基本不存在,不如直接去玩vue

4. props传递,一个顶部组件管理value和setValue,作为props下沉到所需子孙组件,层级太深,会造成props drilling(直译不知什么意思,以前网上看的这个词,应该是很深的意思),而且没必要的中介组件也得接力传递,莫名其妙

5. context,被同一个上下文包裹下的组件,都可以互相传递数据。他是最有React的味道,因为他就是React内置的方法😆。但是我相信,很少人会用。因为他太React了,什么都给使用者去实现!你用了他肯定要面对这个问题


  • 有的组件明明没有订阅数据缺无辜重复渲染

那什么时候会使用context呢?

答案是:做第三方(组件)且通信复杂的时候。这样可以保持组件纯粹,越少依赖第三方拓展性越好、越安全。


二、Context用起


image.png


三、unstated-next源码


源码大概有40行,精简下做个demo分析,如下:


小型unstated-next

export function createContainer(useHook) {
  const Context = createContext(null);
  const Provider = (props) => {
    const value = useHook(props.initialState);
    return <Context.Provider value={value}>{props.children}</Context.Provider>;
  };
  const useContainer = () => {
    return useContext(Context);
  };
  return {
    Provider,
    useContainer
  };
}


在导出的createContainer函数中不难看出,当调用它时会返回一个根据context产出的Provider和useContext(这里命名为useContainer)


用起来是什么样子的呢?

跟传统Context一样,也得有"Provider"包裹。

在这里首先得创建一个"Container"


unstated-next的使用0

import { createContainer } from "./unstated-next";
const useHook = (initialState) => {
  const [color, setColor] = useState(initialState);
  return {
    color,
    setColor
  };
};
const ColorContainer = createContainer(useHook); // 传入自定义hook,把逻辑卷里面


跟原始context的Provider有个小区别,就是value改成initialState,同时你不再需要在App搞useState的getter和setter(干净),App也就没re-render(性能),这里只传初始值(聚焦)


unstated-next的使用1

// ...
export default function App() {
  return (
    <div className="App">
      <ColorContainer.Provider initialState="red">
        <Title />
        <div>
          <div>--------</div>
          <div>
            <Content />
            <Pure />
          </div>
        </div>
        <Button />
      </ColorContainer.Provider>
    </div>
  );
}

里面的组件怎么消费呢,ColorContainer.useContainer()返回自定义hook里面的数据,跟contentd的useContext用法类似,区别就是不用传上下文,因为刚开始创建ColorContainer已经绑定了感受一下变化:useContext(context)ColorContainer.useContainer()


unstated-next的使用2

// ...
const Title = () => {
  console.count("Title");
  const { color } = ColorContainer.useContainer();
  return <div style={{ color }}>我是标题</div>;
};
// ...
const Button = () => {
  console.count("button");
  const { setColor } = ColorContainer.useContainer();
  return (
    <button onClick={() => setColor(getRandomColor())>
      点我改颜色
    </button>
  );
};


优点:

  • 上手简单、理解容易,抄下来到自己项目都可以,没有其他依赖
  • 不用再频繁import createContext, useContextcontext,因为都放在一个"Container"里面了
  • 让你逻辑和UI分离,逻辑靠自定义hook传进创建阶段,不再像普通context那样一窝蜂逻辑和UI的数据都传入Provider


缺点:

  • 跟context一样会误伤没有订阅相应数据的组件

四、 重写


context重复渲染现象

从上一节尾demo里看到,useHook的导出的成员包括color和count相关的值

image.png

Provider里有一个Count,他仅仅跟count这个变量”有关“

image.png

但是当我点击改变颜色的按钮时,点几次就多渲染几次

image.png


这时候加memo有用吗?没用,memo只是能根据props解决问题,能引起组件render的途径不单单因为oldProps !== newProps,还有context的变化、fiber.type的变化等,这里正是context发生变化引起render,不论是哪个值


思考方案

面对React context的无脑更新,我们能采取什么措施?


减少更新

尽量减少,常见的方法是context的读写分离、多变放里,少变放外。


控制更新

就是夺取更新的权力,做到没有订阅变动数据的组件,不会随便重复渲染一般我们怎么让组件更新

  • 自己的state用useState(useReducer)
  • 自己的父组件改变传进来的props
  • forceUpdate,在类组件可以this.forceUpdate(),在函数组件可以利用hook来实现


上面的方法可以看出,forceUpdate的可控性是最好的,我什么时候想渲染就什么时候调用,我名为手动更新


那手动更新的时机是什么-相应依赖变化,那就要有一个新旧值比较的流程,值相同的时候不更新,先网上随便抄一抄shallowEqual


shallowEqual

const shallowEqual = <T extends object>(oldValue: T, newValue: T) => {
  if (Object.is(oldValue, newValue)) {
    return true;
  }
  if (
    typeof oldValue !== 'object' ||
    oldValue === null ||
    typeof newValue !== 'object' ||
    newValue === null
  ) {
    return false;
  }
  const keysOldValue = Object.keys(oldValue);
  const keysNewValue = Object.keys(newValue);
  if (keysOldValue.length !== keysNewValue.length) {
    return false;
  }
  for (let i = 0, len = keysOldValue.length; i < len; i++) {
    const currentKey = keysOldValue[i];
    if (
      !keysNewValue.hasOwnProperty(currentKey) ||
      !Object.is(oldValue[currentKey], newValue[currentKey])
    ) {
      return false;
    }
  }
  return true;
};

控制一下Provider的value,让他的值不是useState(useReducer)控制,由useRef控制,改写后发现怎么点击也没再渲染,证明劫持渲染成功


ref接管value


function Provider(props) {
    const value = useHook(props.initialState)
    const contextRef = useRef(value)
    return <Provider value={contextRef}>{props.children}</Provider>
}


接着是看看消费数据的组件怎么设计,用多了dva直接就命名一个hook叫useSelector,用法一样,看样子是有精确订阅"count"这个数据的能力


消费组件新增hook-useSelector

const Count = (props) => {
  console.count("Count");
  //const { count } = ColorContainer.useContainer();
  const count = useSelector(_ => _.count)
  return <div>{count}</div>;
};

接着实现这个useSelector,当然它是存在createContainer里面,作为一个成员导出思路:入参selector是一个函数,value是整个state,selector(value)期望获得最新的值,即上面例子最新的count值


useSelector初步实现

// ...
const Context = createContext()
function useSelector(selector) {
  const value = useContext(Context).current
  const selected = selector(value)
}
return {
    useSelector,
    Provider,
}

改造到现在,点击是没有重新渲染,我们回到关键的Provider。因为contextValue 由useRef控制,只要我们不改变,它永远都不会使内容更新。接下来在Provider继续入手,将contextRef开放,对它赋值,记得别将contextRef传给Provider的value,因为contextRef由始至终能保持是同一个引用useEffect监听走起,useEffect内部需要执行的是【执行监听”需要“的值】


Provider监听

function Provider(props) {
    const value = useHook(props.initialState)
    const contextRef = useRef(value)
    contextRef.current = value
    useEffect(() => {
        // 监听逻辑
    })
    return <Provider value={contextRef}>{props.children}</Provider>
}

如何实现监听逻辑,我这里是想到靠一个单例来通信,简单实现一个添加状态事件和清除事件


Singleton

class Singleton {
  constructor(listeners) {
    this.listeners = listeners;
  }
  add(states) {
    this.listeners.push(states);
  }
  clean() {
    this.listeners = [];
  }
  // 执行
  exc(value) {
    this.listeners.forEach(cb => {
        cb(value)
    })
  }
}

接着,在创建(createContainer)时获得一个实例watcher,在Provider里监听执行callback


添加监听逻辑

function createContainer(useHook){
    const Context = createContext(null);
    const watcher = new Singleton([]); // 新加代码
    function Provider(props) {
        const value = useHook(props.initialState)
        const contextRef = useRef(value)
        contextRef.current = value
        useEffect(() => {
            // 在监听逻辑添加执行回调队列
            watcher.exc(value) // 新加代码
        })
        return <Provider value={contextRef}>{props.children}</Provider>
   }
}

先看看上面的callback的真身是什么样子的

在节点挂载完成阶段,将callback即下面的watch函数添加到watcher单例


watch函数

一句话,各种比较,值相同就return,不相同就触发forceUpdate,强制更新ref控制的值。下面用到的shallowEqual在上面有实现


useSeletor修改

const useSelector = (selector) => {
    const value = useContext(Context).current;
    const selected = selector(value);
    const forceUpdate = useForceUpdate();
    const ref = useRef({
      selected,
      value
    });
    ref.current = {
      selected,
      value
    };
    useEffect(() => {
      const watch = (newValue) => {
        if (!ref.current) {
          return;
        }
        if (newValue === ref.current.value) {
          return;
        }
        const newSelected = selector(newValue);
        if (shallowEqual(newSelected, ref.current.selected)) {
          return;
        }
        forceUpdate();
      };
      watcher.add(watch);
      return () => {
        watcher.clean();
      };
    }, []);
    return selected;
  };

image.png

效果图

Count没有重复渲染!因为它没有订阅color的数据。


增加更多API

要让使用方代码进一步减少,usePicker只关注需要什么属性,不用再像useSelector传一个回调函数。


usePicker

const pick = (obj, arr) =>
  arr.reduce((iter, val) => (val in obj && (iter[val] = obj[val]), iter), {});
// ...
 const usePicker = (target) => {
    return useSelector((_) => {
      // 数组
      if (Array.isArray(target)) {
        return pick(_, target);
      }
      // 字符串
      if (typeof target === "string") {
        return pick(_, [target]);
      }
      return _;
    });
  };


写法支持

image.png


五、最后


其他优秀方案

heo

本文的封装灵感源自于此,但比它更易理解

hox

umijs下的

相关文章
|
6月前
|
JavaScript API 容器
第三十五章 多个组件状态数据共享
第三十五章 多个组件状态数据共享
|
2月前
|
存储
Pinia 是如何实现状态共享的?
Pinia 是如何实现状态共享的?
56 4
|
1月前
|
存储 前端开发 JavaScript
深入理解前端状态管理
【10月更文挑战第7天】深入理解前端状态管理
24 0
|
3月前
|
Shell
轻量级状态管理库 Zustand 的基本使用
轻量级状态管理库 Zustand 的基本使用
62 0
|
6月前
|
JavaScript 测试技术
状态管理:集成 Vuex 进行全局状态管理
【4月更文挑战第22天】Vuex 是 Vue.js 的状态管理库,通过状态、mutations、actions 和 modules 等核心概念集中管理应用状态。创建 store,划分模块以增强代码维护性。mutations 同步改变状态,actions 处理异步逻辑。遵循 Vuex 规范,在组件中使用辅助函数访问状态。有效更新和处理错误,实现与其它工具集成,提升应用性能和可靠性。注意根据项目需求灵活使用,防止状态管理过度复杂。
50 2
|
6月前
|
存储 调度
进程的奥德赛:并发世界中的核心概念与动态管理
进程的奥德赛:并发世界中的核心概念与动态管理
70 2
|
6月前
|
并行计算 Java API
深入理解Java多线程编程:创建、状态管理、同步与通信
深入理解Java多线程编程:创建、状态管理、同步与通信
|
11月前
|
存储 Kubernetes Cloud Native
有状态的应用如何部署 1?
有状态的应用如何部署 1?
有状态的应用如何部署 1?
|
缓存 Java 数据库连接
架构系列——线程实现方式到底是4种还是2种?(附带线程生命周期)
架构系列——线程实现方式到底是4种还是2种?(附带线程生命周期)
|
缓存 前端开发 JavaScript
谈谈复杂应用的状态管理(下):基于 Zustand 的渐进式状态管理实践
谈谈复杂应用的状态管理(下):基于 Zustand 的渐进式状态管理实践
386 0