react 状态代理

308 阅读3分钟

写react项目的时候表单联动状态过多,不想频繁的写setState。想像vue那样直接赋值,感觉美滋滋。 研究了一下,感觉使用代理代理set可以实现差不多的效果,试了下可行,废话不多说直接开干。

1、ES6 proxy

proxy 为目标提供一个代理,可以对外界的访问进行过滤和改写。属于一种'元编程',则对编程语言进行编程。

语法:

const proxy = new Proxy(target, ProxyHandler);

target:

Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。 ProxyHandler: 一个通常以函数为属性的对象,个属性中的函数分别定义了在执行各种操作时代理p的行为。 可代理的行为详细见MDN: Proxy - JavaScript | MDN (mozilla.org)
这次创建响应式的state 只需要代理其get方法,并且使用Proxy.revocable来创建可撤销的代理,尽可能的让我们创建的对象被垃圾回收机制回收掉。

2、尝试一下:

const targetObj = { name: '张三' } 
const proxyObj = new Proxy(targetObj, { 
    set: (target, key ,value, receiver) => {
        console.log(target, key ,value, receiver) 
        return true 
    } 
}) 
proxyObj.name = '李四' 
// console: {"name":"张三"},name,李四,{"name":"张三"}

3、reactive hook

了解了proxy 之后开始正式整个hook

import { useEffect, useState } from "react"; 
const useReactive = (initialState) => {   
    const [state, setState] = useState(initialState);   
    const preProxyState = useRef();
    const proxyHandler = {     
        set: (target, key, value) => { 
        // 不创建新的对象,setState会比较引用地址没有发生变化,不会更新state       
            setState({ ...target, [key]: value });
            return true;     
        },  
    };   
    let stateProxy = Proxy.revocable(state, proxyHandler);   
    useEffect(() => { 
        if (preProxyState.current) {   
            preProxyState.current.revoke();   
        }     
        preProxyState.current = stateProxy;  
        return () => {       
            // 页面卸载的时候取消代理       
            stateProxy.revoke();       
            stateProxy = null;     
        };
    }, [stateProxy]); 
    return [stateProxy.proxy, setState]; 
}; 
export default useReactive;

在页面中引入使用

import useReactive from "./useReactive";
const Test = () => {
    const [state, setState] = useReactive({
        name: "Petter",
    });

    function changeState() {
        // 直接赋值则可以出发页面变化
        state.name = "Mick";
    }

    return (
        <div className="App">
            <div> {state.name}</div>
            <button onClick={changeState}>toggle state</button>
        </div>
    );
};

点击toggle state 按钮页面显示的name成功变化,一个简单的响应式state就完成了,弄完之后会有一个问题,响应式的state是同步的还是异步的呢。

import useReactive from "./useReactive";
const Test = () => {
  const [state, setState] = useReactive({
    name: "Petter",
  });
  function changeState() {
    state.name = "Mick1";
    state.name = "Mick2";
    state.name = "Mick3";
    state.name = "Mick4";
    state.name = "Mick5";
  }
  console.log(state); // 输出了多少次就重新渲染了多少次
  return (
    <div className="App">
      <div> {state.name}</div>
      <button onClick={changeState}>toggle state</button>
    </div>
  );
};

控制台输出一次,可以发现多次赋值,合并成一次执行,可以与useState一样使用。

截图.png

4、支持数组

该hook使用一个对象作为初始化state,好像不能满足数组作为state同学的需求。接着继续改造一下:

import { useEffect, useState } from "react";

// ProxyHandler 对象

class ProxyHandler {
  constructor(state = {}, setState) {
    this.state = state;
    this.setState = setState;
  }

  set(target, key, value) {
    let result = false;
    switch (this.getType()) {
      case "Object":
        result = this.objectSet(target, key, value);
        break;
      case "Array":
        result = this.arraySet(target, key, value);
        break;
    }
    return result;
  }

  arraySet(target, key, value) {
    const newTarget = target.concat();
    newTarget[key] = value;
    this.setState(newTarget);
    return true;
  }

  objectSet(target, key, value) {
    this.setState({ ...target, [key]: value });
    return true;
  }

  getType() {
    return Object.prototype.toString.call(this.state).slice(8, -1);
  }

}

const useReactive = (initialState) => {
  const [state, setState] = useState(initialState);
  const _ProxyHandler = new ProxyHandler(state, setState);
  const preProxyState = useRef();
  const proxyHandler = {
    set: _ProxyHandler.set.bind(_ProxyHandler), // 需要重新绑定this
  };

  let stateProxy = Proxy.revocable(state, proxyHandler);
  
  useEffect(() => { 
      if (preProxyState.current) {   
          preProxyState.current.revoke();   
      }     
      preProxyState.current = stateProxy;  
      return () => {       
          // 页面卸载的时候取消代理       
          stateProxy.revoke();       
          stateProxy = null;     
      };
  }, [stateProxy]);
  
  return [stateProxy.proxy, setState];
};

页面中尝试一下:

const Test = () => {
  // const [state, setState] = useReactive({
  //   name: "Petter",
  // });
  const [state, setState] = useReactive(["zhangsan"]);
  function changeState() {
    // state.name = "Mick";
    state[0] = "mick";
  }
  console.log(state);
  return (
    <div className="App">
      <div> {state[0]}</div>
      <button onClick={changeState}>toggle state</button>
    </div>
  );
};

重新赋值页面变化,看起来还不错。

截图 (2).png

5、一些问题

  1. 深层级对象赋值。
    但是,项目的需求瞬息万变,一个对象下面可能会有多层级的属性。对深层级的对象赋值是不会触发页面的变化的。

    例如: state.people.name = 'Mike'
    解决办法:拷贝变化后的people对象,重新赋值给state.people即可,数组同样。

    const _people = {...state.people,name: 'Mike'} state.people = _people
    
  2. 替换整个对象
    若需要改变整个对象怎么办呢,userReacive同样返回[state, setState],使用setState重新设置state就行了。

  3. 初始值必须为ES6 Proxy 支持的类型。 源代码: Git