react.js 手写响应式 reactive

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

实现 reactive hooks

代理类声明

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

ts 复制代码
export type Listener<T> = (state: T) => any;

export interface ReactiveCtxModel<T = any> {
  value: T;
  subscribe(listener: Listener<T>): () => void;
}

代理类实现

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

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

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

ts 复制代码
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);
  }
}

实现创建代理的钩子函数

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

ts 复制代码
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;

example:

ts 复制代码
const Demo: React.FC = () => {
  let state = reactive(0);
  const num = state.value;

  return (
    <>
      <Button
        onClick={() => {
          state.value = state.value + 1;
        }}
      >
        {num}
      </Button>
    </>
  );
};

实现监听函数

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

ts 复制代码
/**
 * 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;
    },
  };
}

example:

ts 复制代码
  listen(obj).then((newVal) => {
    console.log(`newVal: ${newVal}`);
  });

借助 Context 传递 Reactive

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

创建默认 Context

ts 复制代码
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;

实现 useReactive 钩子

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

ts 复制代码
/**
 * 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>;
};

实现 useReactiveContext 钩子

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

ts 复制代码
export const useReativeContext = <T = any>(context?: React.Context<ReactiveCtxModel<T> | undefined>): Reactive<T> => {
  const reactiveCtxModel = React.useContext(context || ReactiveCtx);
  return reactiveCtxModel as Reactive<T>;
};

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

ts 复制代码
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>
    </>
  );
};

Kid:

ts 复制代码
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>
    </>
  );
}

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

相关推荐
光影少年38 分钟前
react和vue图片懒加载及实现原理
前端·vue.js·react.js
AndyGoWei40 分钟前
react react-router-dom history 实现原理,看这篇就够了
前端·javascript·react.js
小仓桑43 分钟前
深入理解 JavaScript 中的 AbortController
前端·javascript
换个名字不能让人发现我在摸鱼1 小时前
裁剪保存的图片黑边问题
前端·javascript
小牛itbull1 小时前
Mono Repository方案与ReactPress的PNPM实践
开发语言·前端·javascript·reactpress
小宋10211 小时前
实现Excel文件和其他文件导出为压缩包,并导入
java·javascript·excel·etl
码喽哈哈哈1 小时前
day01
前端·javascript·html
mubeibeinv2 小时前
分页/列表分页
java·前端·javascript
林太白2 小时前
js属性-IntersectionObserver
前端·javascript