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

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

一、前言


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


相信大家比较熟悉的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下的

相关文章
|
前端开发 JavaScript Java
RSA加密---前端---后端解密
RSA加密---前端加---后端解密
1612 0
|
前端开发 JavaScript 程序员
基于React Hooks封装的验证码组件
基于React Hooks封装的验证码组件
1542 1
|
3月前
|
Java 测试技术 Linux
生产环境发布管理
在一个大型团队中,生产发布涉及多环境推进(DEV→TEST→PRE→PROD),以及热更新、回滚等问题。本文基于公司自动化部署平台,讲解如何实现多环境部署与发布管理,涵盖各环境职责、分支管理、自动化构建、日志排查等内容,帮助理解大型企业如何通过CI/CD提升发布效率与稳定性。
|
应用服务中间件
IDEA出现问题:idea启动tomcat 很慢解决方案
IDEA出现问题:idea启动tomcat 很慢解决方案
1911 0
IDEA出现问题:idea启动tomcat 很慢解决方案
|
JavaScript 容器 前端开发
js计算元素距离顶部的高度及元素是否在可视区判断
前言:   在业务当中,我们经常要计算元素的大小和元素在页面的位置信息。比如说,在一个滚动区域内,我要知道元素A是在可视区内,还是在隐藏内容区(滚动到外边看不到了)。有时还要进一步知道,元素是全部都显示在可视区,还是有部分在可视区部分在隐藏内容区。
5551 0
|
9月前
|
存储 人工智能 关系型数据库
AnalyticDB PostgreSQL版:Data+AI 时代的企业级数据仓库
AnalyticDB PostgreSQL版是面向Data+AI时代的企业级数据仓库,涵盖产品架构、核心技术、客户案例及功能发布四大部分。产品架构包括数据分析和AI/ML的存储与计算优化;核心技术涉及高性能实时引擎Beam、向量化执行引擎Laser及优化器Orca;客户案例展示了丝芙兰和领跑汽车的应用;新功能如pgsearch全文检索和In-Database AI/ML进一步提升了性能与易用性。
242 0
|
11月前
|
机器学习/深度学习 人工智能 安全
探索AI在软件工程中的最新应用:自动化测试与代码审查
探索AI在软件工程中的最新应用:自动化测试与代码审查
|
弹性计算 分布式计算 运维
迟来的EMR Serverless Spark评测报告
本文是一篇关于阿里云EMR Serverless Spark产品评测的文章,作者分享了使用体验和理解。EMR Serverless Spark是阿里云提供的全托管、一站式的Spark数据计算平台,简化了大数据处理流程,让用户专注于数据分析。文章提到了产品的主要优势,如快速启动、弹性伸缩、高资源利用率和低成本。
474 8
|
机器学习/深度学习 自然语言处理 数据处理
通过深度学习识别情绪
通过深度学习识别情绪(Emotion Recognition using Deep Learning)是一项结合多模态数据的技术,旨在通过分析人类的面部表情、语音语调、文本内容等特征来自动识别情绪状态。情绪识别在人机交互、健康监测、教育、娱乐等领域具有广泛的应用。
1407 8
|
Android开发
Android 获取签名信息
Android 获取签名信息
367 0