Ein wenig Übung mit JS Proxy, um das Neuzeichnen von React-Komponenten bei Verwendung von useContext zu optimieren

Das Problem, das wir lösen

Der Kontext in Reaktion kann viele Werte enthalten und verschiedene Konsumenten des Kontexts können nur einen Teil der Werte verwenden. Wenn sich jedoch ein Wert aus dem Kontext ändert, werden alle Verbraucher (insbesondere alle verwendeten Komponenten useContext



) erneut gerendert , auch wenn sie nicht vom geänderten Teil der Daten abhängen. Das Problem ist ziemlich diskutiert und hat viele verschiedene Lösungen. Hier sind einige davon. Ich habe dieses Beispiel erstellt , um das Problem zu demonstrieren. Öffnen Sie einfach die Konsole und drücken Sie die Tasten.





Zweck

Unsere Lösung sollte die vorhandenen Codebasen auf ein Minimum beschränken. Ich möchte einen eigenen benutzerdefinierten Hook useSmartContext



 mit derselben Signatur wie die von erstellen , der useContext



die Komponente jedoch nur dann neu rendert, wenn sich der verwendete Teil des Kontexts ändert.





Idee

Finden Sie heraus, was von der Komponente verwendet wird, indem Sie den Rückgabewert useSmartContext



in einen Proxy einschließen.





Implementierung

Schritt 1.





Wir kreieren unseren eigenen Haken.





const useSmartContext(context) {
  const usedFieldsRef = useRef(new Set());

  const proxyRef = useRef(
    new Proxy(
      {},
      {
        get(target, prop) {
          usedPropsRef.current.add(prop);
          return context._currentValue[prop];
        }
      }
    )
  );

  return proxyRef.current;
}
      
      



Wir haben eine Liste erstellt, in der wir die verwendeten Kontextfelder speichern. Wir haben einen Proxy mit einer get



 Falle erstellt, in der wir diese Liste ausfüllen. Target



es ist uns egal, also habe ich als erstes Argument ein leeres Objekt übergeben {}



.





Schritt 2.





Sie müssen den Wert des Kontexts abrufen, wenn er aktualisiert wird, und den Wert der Felder aus der Liste usedPropsRef



mit den vorherigen Werten vergleichen. Wenn sich etwas geändert hat, lösen Sie ein erneutes Rendern aus. useContext



Wir können nicht es verwenden in unseren Haken, sonst unser Haken beginnt auch erneutes Rendern für alle Änderungen verursachen. Hier beginnen Tänze mit einem Tamburin. Ich hatte ursprünglich gehofft, Kontextänderungen mit zu abonnieren context.Consumer



. Nämlich so:





React.createElement(context.Consumer, {}, (newContextVakue) => {/* handle */})
      
      



. . - , , , .





React



, useContext



. , , , . - . _currentValue



. , undefined



. ! Proxy , . Object.defineProperty



.






  let val = context._currentValue;
  let notEmptyVal = context._currentValue;
  Object.defineProperty(context, "_currentValue", {
    get() {
      return val;
    },
    set(newVal) {
      if (newVal) {
        //     !
      }
      val = newVal;
    }
  });
      
      



! : useSmartContext



  Object.defineProperty



  . useSmartContext



  createContext



.





export const createListenableContext = () => {
  const context = createContext();

  const listeners = [];
  let val = context._currentValue;
  let notEmptyVal = context._currentValue;
  Object.defineProperty(context, "_currentValue", {
    get() {
      return val;
    },
    set(newVal) {
      if (newVal) {
        listeners.forEach((cb) => cb(notEmptyVal, newVal));
        notEmptyVal = newVal;
      }
      val = newVal;
    }
  });

  context.addListener = (cb) => {
    listeners.push(cb);

    return () => listeners.splice(listeners.indexOf(cb), 1);
  };

  return context;
};
      
      



, . ,





const useSmartContext = (context) => {
  const usedFieldsRef = useRef(new Set());
  useEffect(() => {
    const clear = context.addListener((prevValue, newValue) => {
      let isChanged = false;
      usedFieldsRef.current.forEach((usedProp) => {
        if (!prevValue || newValue[usedProp] !== prevValue[usedProp]) {
          isChanged = true;
        }
      });

      if (isChanged) {
        //  
      }
    });

    return clear;
  }, [context]);

  const proxyRef = useRef(
    new Proxy(
      {},
      {
        get(target, prop) {
          usedFieldsRef.current.add(prop);
          return context._currentValue[prop];
        }
      }
    )
  );

  return proxyRef.current;
};

      
      



3.





. useState



, . , . - ?





// ...
const [, rerender] = useState();
const renderTriggerRef = useRef(true);
// ...  
if (isChanged) {
  renderTriggerRef.current = !renderTriggerRef.current;
  rerender(renderTriggerRef.current);
}
      
      



, . . useContext



->useSmartContext



createContext



->createListenableContext



.





, !





  • ,





  • Monkey patch





















, . .





Beim Schreiben dieses Artikels bin ich auf eine andere Bibliothek gestoßen, die das gleiche Problem bei der Optimierung von Neuzeichnungen bei Verwendung des Kontexts löst. Die Lösung dieser Bibliothek ist meiner Meinung nach die richtigste, die ich je gesehen habe. Die Quellen sind viel besser lesbar und sie gaben mir einige Ideen, wie wir unsere Beispielproduktion fertigstellen können, ohne die Art der Verwendung zu ändern. Wenn ich eine positive Antwort von Ihnen bekomme, werde ich über die neue Implementierung schreiben.





Vielen Dank für Ihre Aufmerksamkeit.








All Articles