用Object.defineProperty和Proxy重写localStorage的方法,从而实现监听值变化(解决火狐浏览器适配问题)

3,266 阅读3分钟

前言

在工作中维护项目时,遇到一个setItem重写问题,如下代码所示(已经简化):

/**
* 主要实现对setItem的重写,实现对localStorage数据变动的监听
*/
 var signSetItem  = localStorage.setItem;
 localStorage.setItem = function (key, newValue) {
   var setItemEvent = new Event("setItemEvent");
   setItemEvent.key = key;
   var oldValue = localStorage.getItem(key);
   if (!isEqual(oldValue, newValue)) { // 新旧值深度判断,派发监听事件
     setItemEvent.newValue = newValue;
     setItemEvent.oldValue = oldValue;
     window.dispatchEvent(setItemEvent);
     signSetItem.apply(this, arguments);
   }
 };
 
// 全局监听localStorage的setItemEvent事件
window.addEventListener('setItemEvent', function (e) {
  console.log(e.oldValue, e.newValue, e.key )
})

但是上面的方法在火狐浏览器上遇到了问题,即监听失效了!

为何出现监听失效?

对比下火狐浏览器和谷歌浏览器的Storage对象,我们就明白了。实践如下:

  1. 先决条件:统一重写 localStorage.setItem = 2 (在命令行输入)
  2. 观察差别:
差异chrome浏览器火狐浏览器
Storage对象在这里插入图片描述在这里插入图片描述
本地存储在这里插入图片描述在这里插入图片描述

以上不难看出,其实谷歌浏览器重写setItem的本质就是给Storage增加实例属性,当我们在调用setItem时会优先搜索到实例中的setItem属性,而其原型属性setItem没有发生变化

但是火狐浏览器不这么认为,他会筛掉setItem,不把它保存在实例属性中,仅仅是会把它做为本地值存储起来。

作者认为是由于不同浏览器,其Storage对象的get和set机制不一样所导致的。

用Object.defineProperty改写Storage的方法

上文提到寻常的赋值方法,是不能改写setItem的,所以这里用了Object.defineProperty进行改写。

注意Storage的方法都重写一遍,不然调用其对象原型方法会报错。

 // javascript,ES5 
 var localStorageMock = (function(win) {
    var storage = win.localStorage; // 用闭包实现局部对象storage 
    return {
        setItem: function(key, value) {
          var setItemEvent = new Event("setItemEvent");
          var oldValue = storage[key];
          setItemEvent.key = key;
          if (!isEqual(oldValue, value)) { // 新旧值深度判断,派发监听事件
              setItemEvent.newValue = value;
              setItemEvent.oldValue = oldValue;
              win.dispatchEvent(setItemEvent);
              storage[key] = value;
              return true;
          }
          return false;
        },
        getItem: function(key) {
          return storage[key];
        },
        removeItem: function(key) {
          storage[key] = null;
          return true;
        },
        clear: function() {
            storage.clear();
            return true;
        },
        key: function (index) {
            return storage.key(index);
        }
    };
}(window));

Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true });

进阶!用Object.defineProperty和Proxy改写Storage的方法

为什么不同浏览器,其Storage对象的get和set机制不一样?

这也是我通过Proxy代理改写Storage的set和get方法所了解的,感兴趣的同志也可以改写下面代码,一起留言讨论和进步。

在这里,作者也是比较推荐代理这个方法。

因为代理更像是中间件,且你不需要改变它原有的方法,所做的只是加个“哨兵”,去观察它的操作。

  // typescript
  interface SEventType {
      newValue: any;
      oldValue: any;
  }

  const setItem = function (key: string, value: any) {
    return Reflect.set(storageProxy, key, value);
  };
  const getItem = function (key: string) {
    return Reflect.get(storageProxy, key);
  };
  const removeItem = function (key: string) {
    return storageProxy.removeItem(key);
  };
  const key = function (key: number) {
    return storageProxy.key(key);
  };
  const clear = function () {
    return storageProxy.clear();
  };
  const storageProxy = new Proxy(window.localStorage, {
    set: function (ls, key, newValue) {
      if (typeof key === "string") {
        var oldValue = storage[key];
        if (!isEqual(oldValue, newValue)) {
          // 判断新旧值,新值更新
          var setItemEvent: CustomEvent<SEventType> = new CustomEvent(
            "setItemEvent",
            {
              detail: {
                newValue,
                oldValue,
              },
            }
          );
          window.dispatchEvent(setItemEvent);
          return Reflect.set(ls, key, newValue);
        }
      }
      return false;
    },
    get: function (ls, prop) {
      // 在执行localstorage.setItem方法时,会出现报错
      // 发现仅仅一步Reflect.get(ls, prop);是不行的
      // 所以单独每个方法拿出来重写了一下
      if (typeof prop === "string" && prop === "setItem") {
        return setItem;
      } else if (prop === "getItem") {
        return getItem;
      } else if (prop === "key") {
        return key;
      } else if (prop === "clear") {
        return clear;
      } else if (prop === "removeItem") {
        return removeItem;
      }
      return Reflect.get(ls, prop);
    },
  });
  Object.defineProperty(window, "localStorage", {
    configurable: true,
    enumerable: true,
    value: storageProxy,
  });