写了个简单的useReactive

1,142 阅读1分钟

这两天看了ahooks,有一个useReactive,看源码学习了一下,是通过proxy实现的。

今天用Object.defineProperty尝试实现一下,效果如下

应用时代码

import React from "react";
import useReactive from "../../hooks/useReactive";

function Demo() {
  const data: {
    inputVal: string;
    obj: {
      value: string;
    };
    vdata: any;
    count: number;
    arr: any[];
  } = {
    inputVal: "",
    obj: {
      value: ""
    },
    vdata: null,
    count: 1,
    arr: []
  };
  const data2: any[] = ["fasdfs"];
  const state = useReactive(data);
  const state2 = useReactive(data2);

  console.log("state2", state2);

  return (
    <div>
      <p> state.count:{state.count}</p>
      <button
        onClick={() => {
          state.count++;
        }}
      >
        count ++
      </button>
      <button
        onClick={() => {
          state.count--;
        }}
      >
        count --
      </button>

      <p style={{ marginTop: 20 }}> state.inputVal: {state.inputVal}</p>
      <input onChange={e => (state.inputVal = e.target.value)} />

      <p style={{ marginTop: 20 }}> state.obj.value: {state.obj.value}</p>
      <input onChange={e => (state.obj.value = e.target.value)} />

      <p style={{ marginTop: 20 }}>
        state.vdata: {JSON.stringify(state.vdata)}
      </p>
      <button
        onClick={() => {
          state.vdata = {
            p1: 1,
            p2: 2
          };
          setTimeout(() => {
            state.vdata.p1 = "p1";
          }, 3000);
        }}
      >
        vdata本来是null, 测试设置为对象后,是否也有响应能力
      </button>

      <div style={{ marginTop: 20 }}>
        {" "}
        state.arr:{" "}
        {state.arr.map((o, index) => (
          <div key={index}>{o}</div>
        ))}
      </div>
      <button
        style={{ marginTop: "10px" }}
        onClick={() => state.arr.push(Math.floor(Math.random() * 100))}
        role="pushbtn"
      >
        push
      </button>
      <button
        style={{ marginTop: "10px" }}
        onClick={() => state.arr.pop()}
        role="popbtn"
      >
        pop
      </button>
      <button
        style={{ marginTop: "10px" }}
        onClick={() => state.arr.shift()}
        role="shiftbtn"
      >
        shift
      </button>
      <button
        style={{ marginTop: "10px" }}
        role="unshiftbtn"
        onClick={() => state.arr.unshift(Math.floor(Math.random() * 100))}
      >
        unshift
      </button>
      <button
        style={{ marginTop: "10px" }}
        role="reverse"
        onClick={() => state.arr.reverse()}
      >
        reverse
      </button>
      <button
        style={{ marginTop: "10px" }}
        role="sort"
        onClick={() => state.arr.sort()}
      >
        sort
      </button>

      <div style={{ marginTop: 20 }}>
        {" "}
        state2:{" "}
        {state2.map((o, index) => (
          <div key={index}>{JSON.stringify(o)}</div>
        ))}
      </div>
      <button
        style={{ marginTop: "10px" }}
        onClick={() => {
          state2.push({
            aaa: "aaa"
          });
          setTimeout(() => {
            state2.length && (state2[1].aaa = "aaa 哈哈");
          }, 2000);
        }}
        role="pushbtn"
      >
        初始化就是一个数组 push, push后是个对象 ,对象一样是响应式的
      </button>
    </div>
  );
}
export default Demo;

源码

import { useRef, useMemo, useState } from  'react'

const RE_RENDER_ATTR = '__rerender__'
let oldArrayMethods = Array.prototype
const arrayMethods = Object.create(oldArrayMethods)
const ARRAY_METHODS = [
  'push',
  'shift',
  'unshift',
  'pop',
  'sort',
  'splice',
  'reverse'
]
ARRAY_METHODS.forEach(method=>{
  arrayMethods[method] = function (...args: any[]) { 
      const result = oldArrayMethods[method].apply(this,  args)
      let inserted
      let rerender = this[RE_RENDER_ATTR];
      switch (method) {
          case 'push':
          case 'unshift':
              inserted = args;
              break;
          case 'splice': 
              inserted = args.slice(2)
          default:
              break;
      }
      inserted?.forEach((isertItem: any) => {
        observe(isertItem, rerender)
      })
      rerender()
      console.log('got u')
      return result
  }
})

function def<T extends object>(data: T, key: string, value: any){
  Object.defineProperty(data,key,{
      enumerable:false,
      configurable:false,
      value
  })
}

function defineReactive<T extends object>(obj: T, attr: string, value: any, cb: () => void) {
  observe(value, cb)
  Object.defineProperty(obj, attr, {
    enumerable: true,
    configurable: true,
    get: () => {
      return value
    },
    set: (newVal) => {
      value = newVal
      if (Array.isArray(newVal)) {
        observeArray(newVal, cb)
      } else {
        observe(value, cb)
      }
      cb()
    }
  })
}

function observeArray (arr: any[],  cb: () => void) {
  def(arr, RE_RENDER_ATTR, cb)
  // @ts-ignore
  arr.__proto__ = arrayMethods
  for(let i = 0; i < arr.length;i++){
    observe(arr[i], cb)
  }
}

function observe<T extends object> (data:  T, cb: () => void) {
  if (!data || typeof data !== 'object') {
    return
  }
  Object.keys(data).forEach(attr => {
    if (Array.isArray(data[attr])) {
      observeArray(data[attr], cb)
    } else {
      defineReactive(data, attr, data[attr], cb)
    }
  })
}

export default function useReactive<S extends object> (initialState: S): S {
  const stateRef = useRef<S>(initialState)
  const [, setFlag] = useState({});
  useMemo(() => {
    if (Array.isArray(stateRef.current)) {
      observeArray(stateRef.current, () => {
        setFlag({})
      })
    } else {
      observe(stateRef.current, () => {
        setFlag({})
      })
    }
  }, [])
  return stateRef.current
}