编程技术文章分享与教程

网站首页 > 技术文章 正文

看了vue的源码,我用proxy实现了更强大的storage

hmc789 2024-11-14 16:31:02 技术文章 2 ℃

前言

前段时间在看vue关于proxy的源码,也在头条看了很多文章,之前也写了 一篇关于proxy讨论的文章 。为了更好地理解proxy,打算实际应用到项目中。Web Storage的操作是相对比较繁琐的,正好可以作为应用对象,来提升更好的开发体验。

项目的大体功能已经实现,代码已上传至 github ,也提交了npm包。欢迎大家下载使用,也可进行pr,更好的是点个star。你的认可是我不断前进的动力。

正文

项目名为 proxy-web-storage 。主要是使用proxy代理localStorage和sessionStorage,赋值、取值更加便捷,类型不变,并且支持 nullundefinedNaNInfinity 等特殊值,还有 DateRegExpFunction 等类型的存取。同时可以监听数据变化和设置过期时间。

基本功能——存取、删除

使用方法:

import { local, session } from 'proxy-web-storage';

local.test = 'Hello proxy-web-storage'; // works
delete local.test; // works
复制代码

当然,以上用 web storage 也可以实现。在MDN有指出可以像访问对象一样访问这些值。

localStorage.colorSetting = '#a4509b';
localStorage['colorSetting'] = '#a4509b';
delete localStorage.colorSetting;
复制代码

那么,我的实现有什么新的东西吗?是画蛇添足还是画龙点睛?各位看官接着往下看。

类型保持不变

import { local, session } from 'proxy-web-storage';

// number
local.test = 0;
local.test === 0; // true

// boolean
local.test = false;
local.test === false; // true

// undefined
local.test = undefined;
local.test === undefined; // true

// null
local.test = null;
local.test === null; // true
复制代码

正如上所示,你所赋的值是什么类型,访问的也是对应的类型。而 Web Storage 不管是什么类型,获取的必定是字符串。

localStorage.test = false;
localStorage.test // 'false'
复制代码

这会带来一定的麻烦,如果对 localStorage.test 进行判断的话,那么会返回 true ,这跟我们所期待的并不符合。

当然, proxy-web-storage 还支持 DateRegExpfunction 类型。

import { local } from 'proxy-web-storage';

// Date
local.test = new Date('2000-01-01T00:00:00.000Z');
local.test.getTime() === 946684800000; // true

// RegExp
local.test = /d(b+)d/g;
local.test.test("cdbbdbsbz"); // true

// function
local.test = function() {
  return 'Hello proxy-web-storage!';
};
local.test() === 'Hello proxy-web-storage!'; // true
复制代码

如果是用 localStorage 来操作的话,可以正常保存,但是当再次获取来使用的时候,必须对值进行转换。 Date 还好, Date 构造函数支持 dateStringRegExp 会比较麻烦,获取到的值是 '/d(b+)d/g' ,需要拿到 patternflags 才能重新生成 RegExp 类型。而 function 则是借助 eval 重新生成。

不管怎么说, DateRegExpfunction 这几种类型的重新生成,多多少少需要写点逻辑代码,使用 proxy-web-storage 则可以省去不少烦恼,所以确定不点个star吗?

直接操作数据

从上面的 DateRegExp 的例子,也可以看出 proxy-web-storage 可以对数据直接进行操作。对于 ObjectArray ,那么更是方便了,像正常数据一样进行操作,无需进行重复的取值、parse、操作、赋值流程。

proxy-web-storage 让代码更加简洁,舍去重复繁琐逻辑,解放你的双手。

import { local } from 'proxy-web-storage';

// Object
local.test = { hello: 'world' };
local.test.hello = 'proxy-web-storage'; // works

// Array
local.test = ['hello'];
local.test.push('proxy-web-storage'); // works
local.test.length // 2
复制代码

监听数据变化

proxy-web-storage 通过 ononceoff 可以监听数据的变化。

import { local } from 'proxy-web-storage';

local.on('test', function(newVal, oldVal) {
  console.log('test', newVal, oldVal);
});
local.on('test.a', function(newVal, oldVal) {
  console.log('test.a', newVal, oldVal);
});

local.test = {};
// test {} undefined

local.test.a = 1;
// test.a 1 undefined
复制代码

对于 ObjectArray 类型的数据,支持二级监听。 obj.a for Object and list[0] for Array 。还实现了 list.length 的监听,具体实现可滑动至下文查看。

对于 off 方法,如果没有传入key,则移除所有监听对象;如果没有传入callback,则移除指定key的所有回调函数;如果传了callback参数,则只移除callback方法。

设置过期时间

proxy-web-storage 通过 setExpiresgetExpiresremoveExpires 可以设置指定项的过期时间。

import { local } from 'proxy-web-storage';

local.test = 'hello proxy-web-storage';
local.setExpires('test', Date.now() + 10000);

// after 10's
local.test // undefined
复制代码

setExpires 如果传入的时间值小于当前时间,则直接删除指定项。

问题

  1. storage.ts 的 createInstrumentations 方法代码冗余:
(['clear', 'key'] as const).forEach(key => {
  instrumentations[key] = target[key].bind(target);
});

instrumentations.getItem = function(keyName: string) {
  return get(target, keyName, receiver);
};
instrumentations.removeItem = function(keyName: string) {
  return deleteProperty(target, keyName);
};
instrumentations.setItem = function(keyName: string, keyValue: any) {
  return set(target, keyName, keyValue, receiver);
};
...
复制代码

原意是模仿vue重写Array方法的写法,但是typescript没入门,怎么写都不对,所以请各位老哥指点一下。

  1. 关于Array.length的监听

vue是通过 ReactiveEffect 和闭包缓存 oldValue 实现Array.length的监听,可以说watch相关的逻辑都是如此,具体原理可在掘金搜索查看。而我并没有实现 ReactiveEffect 系统,想看通过proxy是否可以进行监听,所以实现上相对会比较繁琐。

length变化的几种情况有:

  • pushpopshiftunshiftsplice
  • let list = []; list.length = 5;
  • let list = []; list[5] = 5;

其中第二种情况会触发handler的set,可以直接拿到length的变化,所以按照set的正常逻辑可以完成监听。而第三种并不会触发set,但先按下不表。

第一种情是通过重写以上方法,记录方法执行前的length,以及执行后的length。

(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
  instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
    lengthAltering = true;
    const oldLength: number = this.length;
    const res = (proxyMap.get(this) as any)[key].apply(this, args)
    if(this.length > oldLength) {
      selfEmit(this, 'length', this.length, oldLength);
    }
    lengthAltering = false;
    return res
  }
});
复制代码

其中有个 lengthAltering 变量,是用来干什么的呢?

这就要结合第三种情况来讨论了。

let list = []; list.push(1, 2); 相当于 list[0] = 1; list[1] = 2; 。也就是说,第一种情况会影响第三种情况的判断,所以加多一个变量辅助。借助这个变量以及 key 是否大于等于 list.length ,来得到第三种情况的变化监听。

let arrayLength: number | undefined;
if(isArray(target) && !lengthAltering) {
  arrayLength = target.length;
}

const result = Reflect.set(target, key, value, receiver);
  
if(isArray(target) && arrayLength !== undefined && Number(key) >= arrayLength) {
  selfEmit(target, 'length', target.length, arrayLength);
}
复制代码

以上是关于Array.length的监听实现,脑袋嗡嗡的,极有可能走了弯路。 想请教有没有更简洁的实现?

Tags:

标签列表
最新留言