手写valtio中的vanilla代码

211 阅读8分钟

#手写valtio中的vanilla代码

valtio是一个小巧的状态库,支持js和react,本文是手写js部分(valtio中最主要的部分)。手写valtio中的订阅和快照方法,深入理解valtio的实现。通过手写可以熟悉Proxy,Reflect,Object.defineProperty等一系列API的概念和使用方式

第一步拦截修改操作

第一步使用proxy拦截对象的修改操作,完成对修改的监听。包括set的deleteProperty两个handler,该步中需要掌握ProxyReflect两个API的使用

const isObject = (x) => typeof x === 'object' && x !== null;

export function proxyFunction(initialObject) {
  if (!isObject(initialObject)) {
    throw new Error('object required');
  ****}
  function notifyUpdate(op) {
    console.log(`op`, op);
  }
  const handler = {
    deleteProperty(target, prop) {
      const prevValue = Reflect.get(target, prop);
      const deleted = Reflect.deleteProperty(target, prop);
      if (deleted) {
        notifyUpdate(['delete', [prop], prevValue]);
      }
      return deleted;
    },
    set(target, prop, value, reciever) {
      const hasPrevValue = Reflect.has(target, prop);
      const prevValue = Reflect.get(target, prop);
      if (hasPrevValue && prevValue === value) {
        return true;
      }
      let nextValue = value;
      notifyUpdate(['set', [prop], nextValue, prevValue]);
      Reflect.set(target, prop, nextValue, reciever);
      return true;
    },
  };

  const proxyObject = new Proxy(initialObject, handler);
  return proxyObject;
}

const data = {
  count: 0,
  text: 'hello',
  person: {
    name: 'xioahong',
    age: 23,
    box: {
      width: 3,
      heigth: 4,
    },
  },
};
const proData = proxyFunction(data);
proData.count = 1;
delete proData.text;
proData.person.age = 3;

结果,可以看到只有对象的的原始类型的属性修改被拦截了。data得person属性的修改没有拦截到。

op ["set",["count"],1,0]
op ["delete",["text"],"hello"]

如果属性是引用类型的数据没有被拦截,可以想到的办法是通过递归添加监听函数

function proxyFunction(data) {
  Object.keys(data).forEach((key) => {
    const item = data[key];
    if (typeof item === 'object') {
      data[key] = proxyFunction(item);
    }
  });
  return new Proxy(data, hanlder);
}

这种写法存在问题,在初始后在修改应用类型的数据代理就需要重新设置,valtio中的巧妙的使用set代理函数在初始时进行设置,通过在初始时类似于克隆代理对象的方式,代理对象中的每个值,如果属性值是对象则会给子属性添加代理。 将proxyFunction改为下面的代码

const proxyStateMap = new WeakMap();
export function proxyFunction(initialObject) {
  if (!isObject(initialObject)) {
    throw new Error('object required');
  }
  const notifyUpdate = (op) => {
    console.log(`op`, JSON.stringify(op));
  };
  const handler = {
    deleteProperty(target, prop) {
      const prevValue = Reflect.get(target, prop);
      const deleted = Reflect.deleteProperty(target, prop);
      if (deleted) {
        notifyUpdate(['delete', [prop], prevValue]);
      }
      return deleted;
    },
    set(target, prop, value, reciever) {
      const hasPrevValue = Reflect.has(target, prop);
      const prevValue = Reflect.get(target, prop);
      if (hasPrevValue && prevValue === value) {
        return true;
      }
      let nextValue = value;
      if (!proxyStateMap.get(value) && isObject(value)) {
        nextValue = proxyFunction(value);
      }
      Reflect.set(target, prop, nextValue, reciever);
      notifyUpdate(['set', [prop], value, prevValue]);
      return true;
    },
  };

  const baseObject = Array.isArray(initialObject)
    ? []
    : Object.create(Object.getPrototypeOf(initialObject));
  const proxyObject = new Proxy(baseObject, handler);
  const proxyState = [baseObject];
  proxyStateMap.set(proxyObject, proxyState);

  Reflect.ownKeys(initialObject).forEach((key) => {
    const desc = Object.getOwnPropertyDescriptor(initialObject, key);
    if ('value' in desc) {
      proxyObject[key] = initialObject[key];
      delete desc.value;
      delete desc.writable;
    }
    Object.defineProperty(baseObject, key, desc);
  });
  return proxyObject;
}

而且没有改变initialObject,通过对创建bseObject进行代理,进行一次克隆,并在克隆过程中对子属性进行设置。 结果

op ["set",["count"],0,null]
op ["set",["text"],"hello",null]
op ["set",["name"],"xioahong",null]
op ["set",["age"],23,null]
op ["set",["width"],3,null]
op ["set",["heigth"],4,null]
op ["set",["box"],{"width":3,"heigth":4},null]
op ["set",["person"],{"name":"xioahong","age":23,"box":{"width":3,"heigth":4}},null]
op ["set",["count"],1,0]
op ["delete",["text"],"hello"]
op ["set",["age"],3,23]

可以看到最后的三行是操作代理对象出发的,其他行时进行初始时出发的,初始时也会触发通知函数后面会进行修复

到这里已经完成对对象的修改的监听,第二步建立监听函数

第二步添加通知

function isObject(x) {
  return typeof x === 'object' && x !== null;
}
const proxyStateMap = new WeakMap(); // <ProxyObject, ProxySate>

export function proxyFunction(initialObject) {
  if (!isObject(initialObject)) {
    throw new Error('object required');
  }
  const listeners = new Set();
  const notifyUpdate = (op) => {
    if (listeners.size) {
      listeners.forEach((listener) => listener(op));
    }
  };
  const createPropListener = (prop) => (op) => {
    const newOp = [...op];
    newOp[1] = [prop, ...newOp[1]]; // path
    notifyUpdate(newOp);
  };
  const propProxyStates = new Map(); // <prop, [proProxyState]>
  const addPropListener = (prop, propProxyState) => {
    if (import.meta.env?.MODE !== 'production' && propProxyStates.has(prop)) {
      throw new Error('prop listener already exists');
    }
    if (listeners.size) {
      const remove = propProxyState[1](createPropListener(prop));
      propProxyStates.set(prop, [propProxyState, remove]);
    } else {
      propProxyStates.set(prop, [propProxyState]);
    }
  };
  const removePropListener = (prop) => {
    const entry = propProxyStates.get(prop);
    if (entry) {
      propProxyStates.delete(prop);
      entry[1]?.();
    }
  };
  const addListener = (listener) => {
    listeners.add(listener);
    if (listeners.size === 1) {
      propProxyStates.forEach(([proxyState, prevRemove], prop) => {
        if (import.meta.env?.MODE !== 'production' && prevRemove) {
          throw new Error('remove already exists');
        }
        const remove = proxyState[1](createPropListener(prop));
        propProxyStates.set(prop, [proxyState, remove]);
      });
    }
    const removeListener = () => {
      listeners.delete(listener);

      if (listeners.size === 0) {
        propProxyStates.forEach(([propProxyState, remove], prop) => {
          if (remove) {
            remove();
            propProxyStates.set(prop, [propProxyState]);
          }
        });
      }
    };
    return removeListener;
  };

  const handler = {
    deleteProperty(target, prop) {
      const prevValue = Reflect.get(target, prop);
      removePropListener(prop);
      const deleted = Reflect.deleteProperty(target, prop);
      if (deleted) {
        notifyUpdate(['delete', [prop], prevValue]);
      }
      return deleted;
    },
    set(target, prop, value, reciever) {
      const hasPrevValue = Reflect.has(target, prop);
      const prevValue = Reflect.get(target, prop);
      if (hasPrevValue && prevValue === value) {
        return true;
      }
      removePropListener(prop);
      let nextValue = value;
      if (!proxyStateMap.get(value) && isObject(value)) {
        nextValue = proxyFunction(value);
      }
      const childProxyState = proxyStateMap.get(nextValue);
      if (childProxyState) {
        addPropListener(prop, childProxyState);
      }
      Reflect.set(target, prop, nextValue, reciever);
      notifyUpdate(['set', [prop], value, prevValue]);
      return true;
    },
  };

  const baseObject = Array.isArray(initialObject)
    ? []
    : Object.create(Object.getPrototypeOf(initialObject));
  const proxyObject = new Proxy(baseObject, handler);
  const proxyState = [baseObject, addListener];
  proxyStateMap.set(proxyObject, proxyState);

  Reflect.ownKeys(initialObject).forEach((key) => {
    const desc = Object.getOwnPropertyDescriptor(initialObject, key);
    if ('value' in desc) {
      proxyObject[key] = initialObject[key];
      delete desc.value;
      delete desc.writable;
    }
    Object.defineProperty(baseObject, key, desc);
  });
  return proxyObject;
}
export function subscribe(proxyObject, callback) {
  const proxyState = proxyStateMap.get(proxyObject);
  if (import.meta.env?.MODE !== 'production' && !proxyState) {
    console.warn('Please use proxy object');
  }
  const ops = [];
  const addListener = proxyState[1];
  let isListenerActive = false;
  const listener = (op) => {
    ops.push(op);
    callback(ops.splice(0));
  };
  const removeListener = addListener(listener);
  isListenerActive = true;
  return () => {
    isListenerActive = false;
    removeListener();
  };
}

const data = {
  count: 0,
  text: 'hello',
  person: {
    name: 'xioahong',
    age: 23,
    box: {
      width: 3,
      heigth: 4,
    },
  },
};
const proData = proxyFunction(data);

const unscribe = subscribe(proData, (op) => {
  // const snap1 = snapshot(proData);
  // console.log(`snap1`, snap1);
  console.log(`op`, JSON.stringify(op));
});
proData.count = 1;
delete proData.text;
proData.person.age = 3;
// unscribe();
proData.person.box.width = 5;
export default {};

  • 对于一个不包含子属性的对象来说,addListener函数已经足够使用了,通过添加listenr,在delete和set函数中触发。
  • 如果是子属性是引用类型的数据
    • 在addListener函数中,会遍历propProxyStates,即是当前引用类型属性的map,拿到子属性的代理数据,proxyState,获取子代理对象的addListener方法,然后调用,形成了递归添加listener方法。data -> addListener data.person -> addListener data.person.box -> addListener
    • 上面添加的listener都是通过createPropListener产生的,每个listener中都包含了自己的属性名,并且对子属性的改变添加了监听函数。proData.person.box.width = 5语句会触发,box得width修改 此时 op 为['set',['width'],5,3] 由proData.person.box代理对象中的listener触发,该函数会触发父对象中的监听函数 此时 op 为['set',['box','width'],5,3] 由proData.person代理对象中的listener触发 此时 op 为['set',['person','box','width'],5,3] 由proData代理对象中的listener触发

第三步 添加快照

快照就是对当前的数据对象进行复制,具体代码

const createSnapshot = (target, version) => {
  const snap = Array.isArray(target)
    ? []
    : Object.create(Object.getPrototypeOf(target));
  Reflect.ownKeys(target).forEach((key) => {
    if (Object.getOwnPropertyDescriptor(snap, key)) {
      // Only the known case is Array.length so far.
      return;
    }
    const value = Reflect.get(target, key);
    const desc = {
      value,
      enumerable: true,
      // This is intentional to avoid copying with proxy-compare.
      // It's still non-writable, so it avoids assigning a value.
      configurable: true,
    };
    if (proxyStateMap.has(value)) {
      const [target] = proxyStateMap.get(value);
      desc.value = createSnapshot(target, version);
    }
    Object.defineProperty(snap, key, desc);
  });
  return Object.preventExtensions(snap);
};

createSnapshot 返回一个不支持扩展(不支持赋值)的对象,可以认为是不可变数据。 在react的函数中快照方法会被频繁调用,所以在数据没有变化时通过缓存获取。valitio通过添加版本的机制来表示数据是否发生变化。

versionHolder得数据中保存了两个版本,第一个版本表示当前的数据版本,第二个版本表示检查的版本 ensureVersion 函数返回一个当前数据最新的版本version

const versionHolder = [1, 1];
const proxyFunction = (initialObject) => {
  let version = versionHolder[0];
  const listeners = new Set();
  const notifyUpdate = (op, nextVersion = ++versionHolder[0]) => {
    if (version !== nextVersion) {
      version = nextVersion;
      listeners.forEach((listener) => listener(op));
    }
  };
  let checkVersion = versionHolder[1];
  const ensureVersion = (nextCheckVersion = ++versionHolder[1]) => {
    if (checkVersion !== nextCheckVersion && !listeners.size) {
      checkVersion = nextCheckVersion;
      // eslint-disable-next-line @typescript-eslint/no-use-before-define
      propStateMap.forEach(([propProxyState]) => {
        const propVersion = propProxyState[1](nextCheckVersion);
        if (propVersion > version) {
          version = propVersion;
        }
      });
    }
    return version;
  };
  // 其他代码
  // ...
  // 返回的state中添加ensureVersion和createSnapshot方法
  const proxyState = [baseData, ensureVersion, createSnapshot, addListener];
}
  // 原来调用addListener的地方需要修改获取的方式
  export function subscribe(proxyObject, listener) {
  const remove = proxyState[3](newListener);// 原来是 proxyState[1]
  }

在createSnapshot方法中加入缓存功能

const createSnapshot = (target, version) => {
  const cache = snapCache.get(target);
  if (cache?.[0] === version) {
    return cache[1];
  }
  const snap = //...
  snapCache.set(target, [version, snap]);
}

最后导出snapshot方法

export function snapshot(proxyObject) {
  const proxyState = proxyStateMap.get(proxyObject);
  if (import.meta.env?.MODE !== 'production' && !proxyState) {
    console.warn('Please use proxy object');
  }
  const [target, ensureVersion, createSnapshot] = proxyState;
  return createSnapshot(target, ensureVersion());
}

完整的代码

function isObject(data) {
  return typeof data === 'object' && data !== null;
}

const proxyStateMap = new WeakMap(); // <ProxyObject, ProxySate>
const snapCache = new WeakMap();
const createSnapshot = (target, version) => {
  const cache = snapCache.get(target);
  if (cache?.[0] === version) {
    return cache[1];
  }
  const snap = Array.isArray(target)
    ? []
    : Object.create(Object.getPrototypeOf(target));
  snapCache.set(target, [version, snap]);
  Reflect.ownKeys(target).forEach((key) => {
    if (Object.getOwnPropertyDescriptor(snap, key)) {
      // Only the known case is Array.length so far.
      return;
    }
    const value = Reflect.get(target, key);
    const desc = {
      value,
      enumerable: true,
      // This is intentional to avoid copying with proxy-compare.
      // It's still non-writable, so it avoids assigning a value.
      configurable: true,
    };
    if (proxyStateMap.has(value)) {
      const [target] = proxyStateMap.get(value);
      desc.value = createSnapshot(target, version);
    }
    Object.defineProperty(snap, key, desc);
  });
  return Object.preventExtensions(snap);
};

const versionHolder = [1, 1];
export function proxyFunction(initialObject) {
  if (!isObject(initialObject)) {
    throw new Error('need object');
  }
  let version = versionHolder[0];
  const listeners = new Set();
  const notifyUpdate = (op, nextVersion = ++versionHolder[0]) => {
    if (version !== nextVersion) {
      version = nextVersion;
      listeners.forEach((listener) => listener(op));
    }
  };
  let checkVersion = versionHolder[1];
  const ensureVersion = (nextCheckVersion = ++versionHolder[1]) => {
    if (checkVersion !== nextCheckVersion && !listeners.size) {
      checkVersion = nextCheckVersion;
      // eslint-disable-next-line @typescript-eslint/no-use-before-define
      propProxyStates.forEach(([propProxyState]) => {
        const propVersion = propProxyState[1](nextCheckVersion);
        if (propVersion > version) {
          version = propVersion;
        }
      });
    }
    return version;
  };
  const createPropListener = (prop) => (op) => {
    const newOp = [...op];
    newOp[1] = [prop, ...newOp[1]];
    notifyUpdate(newOp);
  };
  const propProxyStates = new Map(); // <prop,[proxyState,remove?]>
  const addPropListener = (prop, propProxyState) => {
    if (import.meta.env?.MODE !== 'production' && propProxyStates.has(prop)) {
      throw new Error('prop listener already exists');
    }
    if (listeners.size) {
      const remove = propProxyState[3](createPropListener(prop));
      propProxyStates.set(prop, [propProxyState, remove]);
    } else {
      propProxyStates.set(prop, [propProxyState]);
    }
  };
  const removePropListener = (prop) => {
    const entry = propProxyStates.get(prop);
    if (entry) {
      propProxyStates.delete(prop);
      entry[1]?.();
    }
  };
  const addListener = (listener) => {
    listeners.add(listener);
    if (listeners.size === 1) {
      propProxyStates.forEach(([proxyState, prevRemove], prop) => {
        if (import.meta.env?.MODE !== 'production' && prevRemove) {
          throw new Error('remove already exists');
        }
        const remove = proxyState[3](createPropListener(prop));
        propProxyStates.set(prop, [proxyState, remove]);
      });
    }
    const removeListener = () => {
      listeners.delete(listener);

      if (listeners.size === 0) {
        propProxyStates.forEach(([propProxyState, remove], prop) => {
          if (remove) {
            remove();
            propProxyStates.set(prop, [propProxyState]);
          }
        });
      }
    };
    return removeListener;
  };

  const handler = {
    deleteProperty(target, prop) {
      const prevValue = Reflect.get(target, prop);
      removePropListener(prop);
      const deleted = Reflect.deleteProperty(target, prop);
      if (deleted) {
        notifyUpdate(['delete', [prop], prevValue]);
      }
      return deleted;
    },
    set(target, prop, value, receiver) {
      const hasPrev = Reflect.has(target, prop);
      const prevValue = Reflect.get(target, prop, receiver);
      if (hasPrev && prevValue === value) {
        return true;
      }
      removePropListener(prop);
      let newValue = value;
      if (!proxyStateMap.get(value) && isObject(value)) {
        // 可以被代理
        newValue = proxyFunction(value);
      }
      const childState = proxyStateMap.get(newValue);
      if (childState) {
        addPropListener(prop, childState);
      }
      let res = Reflect.set(target, prop, newValue, receiver);
      notifyUpdate(['set', [prop], newValue, prevValue]);
      return res;
    },
  };
  const baseObject = Array.isArray(initialObject)
    ? []
    : Object.create(Object.getPrototypeOf(initialObject));
  const proxyObject = new Proxy(baseObject, handler);
  const proxyState = [baseObject, ensureVersion, createSnapshot, addListener];
  proxyStateMap.set(proxyObject, proxyState);

  Reflect.ownKeys(initialObject).forEach((key) => {
    const desc = Object.getOwnPropertyDescriptor(initialObject, key);
    if ('value' in desc) {
      proxyObject[key] = initialObject[key];
      delete desc.value;
      delete desc.writable;
    }
    Object.defineProperty(baseObject, key, desc);
  });
  return proxyObject;
}

export function subscribe(proxyObject, callback) {
  const proxyState = proxyStateMap.get(proxyObject);
  if (import.meta.env?.MODE !== 'production' && !proxyState) {
    console.warn('Please use proxy object');
  }
  const ops = [];
  const addListener = proxyState[3];
  let isListenerActive = false;
  const listener = (op) => {
    ops.push(op);
    callback(ops.splice(0));
  };
  const removeListener = addListener(listener);
  isListenerActive = true;
  return () => {
    isListenerActive = false;
    removeListener();
  };
}

export function snapshot(proxyObject) {
  const proxyState = proxyStateMap.get(proxyObject);
  if (import.meta.env?.MODE !== 'production' && !proxyState) {
    console.warn('Please use proxy object');
  }
  const [target, ensureVersion, createSnapshot] = proxyState;
  return createSnapshot(target, ensureVersion());
}

const data = {
  count: 0,
  text: 'hello',
  person: {
    name: 'xioahong',
    age: 23,
    box: {
      width: 3,
      heigth: 4,
    },
  },
};
const proData = proxyFunction(data);

const unscribe = subscribe(proData, (op) => {
  const snap = snapshot(proData);
  console.log(`op`, JSON.stringify(op));
  console.log(`snap`, JSON.stringify(snap));
});
proData.count = 1;
// delete proData.text;
proData.person.age = 3;
// unscribe();
// proData.person.box.width = 5;
export default {};



结果

op [["set",["count"],1,0]]
snap {"count":1,"text":"hello","person":{"name":"xioahong","age":23,"box":{"width":3,"heigth":4}}}
op [["set",["person","age"],3,23]]
snap {"count":1,"text":"hello","person":{"name":"xioahong","age":3,"box":{"width":3,"heigth":4}}}