• react.js 手写响应式 reactive


    Redux 太繁琐,Mbox 很酷但我们可能没必要引入新的包,那就让我们亲自在 react.js 中通过代理实现一套钩子来达到类似 vue 的响应式状态:

    实现 reactive hooks

    代理类声明

    代理状态的类应当提供可访问的状态,和订阅变化的接口。

    export type Listener<T> = (state: T) => any;
    
    export interface ReactiveCtxModel<T = any> {
      value: T;
      subscribe(listener: Listener<T>): () => void;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    代理类实现

    使用 es6 class 来实现代理类,es6 class 提供了属性的 get/set 访问器,让我们在通过 obj.key 的方式访问时不是直接访问,而是经过了代理,真实的值则通过 private 被设置为私有属性。

    类的构造器,我们传入用 React.useState 获得的返回,没错,想在 react 中让页面响应数据的变化,我们仍然需要 useState,不传 setState 的话,这个 Reactive 将是惰性的,因为他无法触发页面的重渲染。

    私有属性除了要保存的 state,还有 listeners 数组来保存监听变化要触发的函数,这些函数在 state 每次被 set 访问器调用时跟着调用。

    export class Reactive<T = any> implements ReactiveCtxModel {
      private _state: T;
      private _setState: any = (newState: T) => {
        this._state = newState;
      };
      private _listeners: Listener<Readonly<T>>[] = [];
    
      constructor(state: T, setState?: any) {
        this._state = state;
        setState ? (this._setState = setState) : void 0;
      }
    
      get value(): T {
        return this._state;
      }
    
      set value(newState: T) {
        this._setState?.(newState);
        this._listeners.forEach((listener) => listener(newState));
      }
    
      subscribe(listener: Listener<T>) {
        this._listeners.push(listener);
        return () => {
          this._listeners = this._listeners.filter((l) => l !== listener);
        };
      }
    
      static isReactive(obj: any) {
        return Reactive.prototype.isPrototypeOf(obj);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    实现创建代理的钩子函数

    每次在代码里手动创建 useState() 然后还要 new Reactive() 太麻烦了,我们将这几个操作封装成一个 hook Reactify,然后再赋给 reactive,这样我们就可以直接使用 reactive(initialValue) 创建响应式对象。(为什么要先创建 Reactify?因为 react 约定 react 的 use 钩子的顶部空间应当命名为 useXXX 或者是 大写字母 开头,因为我喜欢 reactive 这个名字,所以做了一个交换)

    const Reactify = <T = any>(initialValue: T): Reactive<T> => {
      const [state, setState] = React.useState<T>(initialValue);
      const observer = new Reactive(state, setState);
      return observer;
    };
    /**
     * reactive is same with Reactify
     */
    export const reactive = Reactify;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    example:

    const Demo: React.FC = () => {
      let state = reactive(0);
      const num = state.value;
    
      return (
        <>
          <Button
            onClick={() => {
              state.value = state.value + 1;
            }}
          >
            {num}
          </Button>
        </>
      );
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    实现监听函数

    直接在 Reactive 对象上调用 subscribe 很棒,但有时候我更喜欢这个操作可以抽出来,于是有了下面这个 listen 函数,传入要监听的 Reactive 对象,接着在 then 中链式传入要触发的回调,观感上更优雅。

    /**
     * When store.state changes, call the given function.
     * @param target listened Reactive store
     * @returns unlistener
     */
    export function listen<T = any>(target: Omit<Reactive<T>, "_state" | "_setState">) {
      return {
        then: (...fns: ((value: T) => any)[]) => {
          const fn = (value: T) => fns.forEach((f) => f(value));
          const dispose = target.subscribe(fn);
          return dispose;
        },
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    example:

      listen(obj).then((newVal) => {
        console.log(`newVal: ${newVal}`);
      });
    
    • 1
    • 2
    • 3

    借助 Context 传递 Reactive

    以上的 reactive 只能在单组件局部使用,即使通过 props 传递给子组件,子组件也只有只读的权利。如果需要跨组件共享 Reactive 代理,我们可以借助 React.Context:

    创建默认 Context

    import { createContext } from "react";
    import { Listener, Reactive } from "./model";
    
    export const createReactiveContext = <T = any>(initialValue?: T) => {
      const reactiveObject = new Reactive(initialValue);
      return createContext<ReactiveCtxModel<T> | undefined>(reactiveObject as any);
    };
    
    const ReactiveCtx = createReactiveContext();
    
    export default ReactiveCtx;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    实现 useReactive 钩子

    useReactive 可以接收一个初值,如果得到了初值就开辟一个新的 context 和 Reactive 对象,否则延用上一步创建的 ReactiveCtx。

    /**
     * Accept a value and return a reactive object. When initalValue is valid a new reactive object will be created.
     */
    export const useReactive = <T = any>(initialValue?: T): Reactive<T> => {
      const [state, setState] = React.useState<T>(initialValue ?? (undefined as T));
      const reactiveObj = new Reactive(state, setState);
      const defaultContextModel = React.useContext((initialValue as any) ?? ReactiveCtx);
      if (initialValue !== undefined && initialValue !== null) {
        return reactiveObj as Reactive<T>;
      }
      return defaultContextModel as Reactive<T>;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    实现 useReactiveContext 钩子

    useReactive 接收初值后新建的 context 不能为其它组件获取,要让其它组件共享非默认的 context,我们就需要在外部额外创建并导出新的 context,并实现一个 useReactiveContext 钩子来接收新的context,这样就可以共享新的 context,同样如果没有传入新的 context,我们将沿用默认的 ReactiveCtx。

    export const useReativeContext = <T = any>(context?: React.Context<ReactiveCtxModel<T> | undefined>): Reactive<T> => {
      const reactiveCtxModel = React.useContext(context || ReactiveCtx);
      return reactiveCtxModel as Reactive<T>;
    };
    
    • 1
    • 2
    • 3
    • 4

    现在,我们将原先 demo 中使用的 raective 替换为 useReactive,然后我们即可自由的跨组件共享 Reactive。
    example:

    const Demo: React.FC = () => {
      let state = useReactive(0);
      const num = state.value;
    
      listen(state).then((newVal) => {
        console(`newVal: ${newVal}`);
      });
    
      return (
        <>
          <Button
            $click={() => {
              state.value = state.value + 1;
            }}
          >
            {num}
          </Button>
          <ReactiveCtx.Provider value={state}>
            <Kid />
          </ReactiveCtx.Provider>
        </>
      );
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    Kid:

    function Kid() {
      const state = useReactive<number>();
      return (
        <>
          <Tag
            light
            style={{ cursor: "pointer" }}
            onClick={() => {
              state.value++;
            }}
          >
            state : {state.value}
          </Tag>
          <Tag
            light
            style={{ cursor: "pointer" }}
            onClick={() => {
              state2.value++;
            }}
          >
            state2 : {state2.value}
          </Tag>
          <context.Provider value={state2}>
            <KidKid />
          </context.Provider>
        </>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    Bingo! 到这里我们就基本实现了 reactive 啦,拜拜~

  • 相关阅读:
    设计模式-创建型模式-单例模式
    商务社交中为何电子名片这么火?都在使用哪一款免费的电子名片?
    IOC 的底层原理和Bean管理XML方式、xml注入集合属性
    电阻电路等效变换(Ⅲ)
    4. 堪比JMeter的.Net压测工具 - Crank 进阶篇 - 认识wrk、wrk2
    关于DDOS的几个误区,你知道几个?
    顺序表的(增删查改)实现
    【读书笔记】【Effective Modern C++】型别推导
    SpringCloudAlibaba-Sentinel流量监控
    MySQL数据库索引与事务、存储引擎、MyISAM和lnnoDB
  • 原文地址:https://blog.csdn.net/m0_51810668/article/details/133976009