返回文章列表
·3 分钟阅读

Valtio 原理

前言

下面所说的都是非嵌套对象的情况

分为两个部分,vanilla 和 react

Vanilla

核心:为可变状态添加不变性

检测突变

使用Proxyset来检测突变,使用版本号来标记这个对象已经发生了变化

let version = 0;
const p = new Proxy(
  {},
  {
    set(target, prop, value) {
      ++version;
      target[prop] = value;
    },
  }
);

p.a = 10;
console.log(version); // ---> 1
++p.a;
console.log(version); // ---> 2

快照

当调用snapshot的时候,根据版本号创建快照

let version = 0;
let lastVersion;
let lastSnapshot;
const p = new Proxy(
  {},
  {
    set(target, prop, value) {
      ++version;
      target[prop] = value;
    },
  }
);
const snapshot = () => {
  if (lastVersion !== version) {
    lastVersion = version;
    lastSnapshot = { ...p };
  }
  return lastSnapshot;
};

p.a = 10;
console.log(snapshot()); // ---> { a: 10 }
p.b = 20;
console.log(snapshot()); // ---> { a: 10, b: 20 }
++p.a;
++p.b;
console.log(snapshot()); // ---> { a: 11, b: 21 }

事件订阅

let version = 0;
const listeners = new Set();
const p = new Proxy(
  {},
  {
    set(target, prop, value) {
      ++version;
      target[prop] = value;
      listeners.forEach((listener) => listener());
    },
  }
);
const subscribe = (callback) => {
  listeners.add(callback);
  const unsubscribe = () => listeners.delete(callback);
  return unsubscribe;
};

subscribe(() => {
  console.log("mutated!");
});

p.a = 10; // shows "mutated!"
++p.a; // shows "mutated!"
p.b = 20; // shows "mutated!"

React

核心:useSyncExternalStore and proxy-compare

useSyncExternalStore

// Create a state
const stateFoo = proxy({ count: 0, text: "hello" });

// Define subscribe function for stateFoo
const subscribeFoo = (callback) => subscribe(stateFoo, callback);

// Define snapshot function for stateFoo
const snapshotFoo = () => snapshot(stateFoo);

// Our hook to use stateFoo
const useStateFoo = () => useSyncExternalStore(subscribeFoo, snapshotFoo);

Automatic render optimization

const TextComponent = () => {
  const { text } = useStateFoo();
  return <span>{text}</span>;
};

如果我们更改stateFoo中的count数值(例如++stateFoo.count ,则此TextComponent实际上会重新渲染,但会产生相同的结果,因为它不使用count数值,并且text值不会更改。所以,这是一次额外的重新渲染。

例如,如果我们假设 hook 接受字符串列表,我们将能够告诉如下属性。

const TextComponent = () => {
  const { text } = useStateFoo(["text"]);
  return <span>{text}</span>;
};

那么如何自动化呢

proxy-compare

我们想知道的是,在前面的示例中, text值在TextComponent中使用。

// An array to store accessed properties
const accessedProperties = [];

// Wrap stateFoo with Proxy
const obj = new Proxy(stateFoo, {
  get: (target, property) => {
    accessedProperties.push(property);
    return target[property];
  },
});

// Use it
console.log(obj.text);

// We know what are accessed.
console.log(accessedProperties); // ---> ['text']

Valtio 提供了一个基于 proxy-compare 的钩子来实现自动渲染优化。

useSnapshot

Valtio 提供的hook称为useSnapshot 。它返回一个不可变的快照,但它包含有用于渲染优化的Proxy

使用大概是

import { proxy, useSnapshot } from "valtio";

const state = proxy({ nested: { count: 0, text: "hello" }, others: [] });

const TextComponent = () => {
  const snap = useSnapshot(state);
  return <span>{snap.nested.text}</span>;
};

该组件仅在text值更改时重新呈现。即使countothers发生变化,它也不会重新渲染。

基本上,它只是useSyncExternalStoreproxy-compare的组合。

参考

  1. How valtio works — Valtio, makes proxy-state simple for React and Vanilla (pmnd.rs)
  2. How Valtio Proxy State Works (Vanilla Part) · Daishi Kato's blog (axlight.com)
  3. How Valtio Proxy State Works (React Part) · Daishi Kato's blog (axlight.com)
  4. valtio / proxy-compare 源码解析 | Magicdawn
  5. Valtio 源码解析 - 掘金 (juejin.cn)