Advanced-Frontend/Daily-Interview-Question

第 104 题:模拟 localStorage 时如何实现过期时间功能

yygmind opened this issue · 23 comments

第 104 题:模拟 localStorage 时如何实现过期时间功能

// 模拟实现一个 localStorage
const localStorage = (function(){
let store = {};
return {
getItem(key){
if(store[key] && store[key+'time']){
const date = new Date().valueOf();
if(date>store[key+'time']){ // 过期了
this.removeItem(key);
return '已经过期了';
}
}
return store[key] || null;
},
setItem(key,value,time){
store[key] = value.toString();
if(time)store[key+'time'] = time; // 设置过期时间
},
removeItem(key){
delete store[key]
},
clear(){
store = {};
}
}
})()

		const localStorageMock = (function() {
			let store = {}
			return {
				getItem: function(key) { return store[key] || null },
				setItem: function(key, value, time) { // time 是毫秒级别,过时时间必须大于0ms
					time = Number(time)?time:0;
					store[key] = value.toString()
					if(time>0){this.timeOut(key,time)}
				},
				timeOut:function(key,time){
					let that = this;
					let timer = setTimeout(function(){ that.removeItem(key);clearTimeout(timer);},time)
				},
				removeItem: function(key) { delete store[key] },
				clear: function() { store = {} },
			}
		})()

		Object.defineProperty(window, 'localStorage2', {
			value: localStorageMock
		})
		
		
		localStorage2.setItem('test',"test",3000)
		console.log(localStorage2.getItem("test"))  //test
		
		setTimeout(function(){
			console.log(localStorage2.getItem("test"))  //null
		},4000);

定时器没有清除呢

用 cookie 模拟 localStorage

参考 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/localStorage

if (!window.localStorage) {
  window.localStorage = {
    getItem: function (sKey) {
      if (!sKey || !this.hasOwnProperty(sKey)) { return null; }
      return unescape(document.cookie.replace(new RegExp("(?:^|.*;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*"), "$1"));
    },
    key: function (nKeyId) {
      return unescape(document.cookie.replace(/\s*\=(?:.(?!;))*$/, "").split(/\s*\=(?:[^;](?!;))*[^;]?;\s*/)[nKeyId]);
    },
    setItem: function (sKey, sValue) {
      if(!sKey) { return; }
      document.cookie = escape(sKey) + "=" + escape(sValue) + "; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/";
      this.length = document.cookie.match(/\=/g).length;
    },
    length: 0,
    removeItem: function (sKey) {
      if (!sKey || !this.hasOwnProperty(sKey)) { return; }
      document.cookie = escape(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
      this.length--;
    },
    hasOwnProperty: function (sKey) {
      return (new RegExp("(?:^|;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
    }
  };
  window.localStorage.length = (document.cookie.match(/\=/g) || window.localStorage).length;
}

扩展 localStorage 支持 expires

(function () {
  var getItem = localStorage.getItem.bind(localStorage)
  var setItem = localStorage.setItem.bind(localStorage)
  var removeItem = localStorage.removeItem.bind(localStorage)
  localStorage.getItem = function (keyName) {
    var expires = getItem(keyName + '_expires')
    if (expires && new Date() > new Date(Number(expires))) {
      removeItem(keyName)
      removeItem(keyName + '_expires')
    }
    return getItem(keyName)
  }
  localStorage.setItem = function (keyName, keyValue, expires) {
    if (typeof expires !== 'undefined') {
      var expiresDate = new Date(expires).valueOf()
      setItem(keyName + '_expires', expiresDate)
    }
    return setItem(keyName, keyValue)
  }
})()

使用

localStorage.setItem('key', 'value', new Date() + 10000) // 10 秒钟后过期
localStorage.getItem('key')

搞不懂啊

localstorage本身没有时间过期的功能,要是自己封装一个简单的localstorage功能的话,可以使用定时器去实现过期时间的功能,值得注意的是执行定时器后,到指定的时间,记得destroy定时器。

export class mockLocalstorage {
  constructor() {
    this.store = new Map(); // 记录存储数据
  }

  getItem(key) {
    const stringKey = String(key);
    if (this.store.has(stringKey)) {
      return String(this.store.get(stringKey));
    } else {
      return null;
    }
  }

  // time单位是小时
  setItem(key, val, time = "undefined") {
    let stime = null;
    if (typeof time !== "number" && time !== "undefined") {
      throw new Error("设置过期时间的基础单位是小时,请不要破坏规则!");
    }

    if (time !== "undefined") {
      time = time * 60 * 60 * 1000; // h ---> ms
      try {
        let _this = this;
        this.store.set(String(key), val);
        // 设置定时器 定时清空垃圾数据
        stime = setTimeout(() => {
          _this.removeItem(key);
          stime = null;
        }, time);
      } catch (e) {
        stime = null;
        throw new Error(e);
      }
    }
  }

  keys() {
    return Object.keys(this.store);
  }

  removeItem(key) {
    this.store.delete(String(key));
  }

  clear() {
    this.store.clear();
  }

  get length() {
    return this.store.size;
  }
}

测试代码(vue代码)

let _this = this;
let time = null;
this.mockLocalstorage.setItem("name", "duya", 0.01);
console.log(this.mockLocalstorage.getItem("name")); // 测试mockLocalstorage duya
  
// 检测数据又没有清空😄
time = setTimeout(() => {
  time = null;
  console.log("name " + _this.mockLocalstorage.getItem("name")); // "name: null"
}, 0.01 * 60 * 60 * 1000);

感觉不能用定时器,因为浏览器会关闭。不知道我感觉的对不对

??存储的时候加个存储时间戳和有效期时长就好了啊。取的时候判断一下不就行了

浏览器切换下一个页签定时器有可能被暂停

使用定时器来做过期时间功能的话,重复设置某个值,如果第二次有效期比第一次长,那么到期之前,第一次已经将它清空了。
加个时间戳和有效期是不是合适一些?基于第 103 题方法的修改

class LocalStorage {
  constructor () {
    this.store = new Map();
    this.expires = new Map();
  }

  getItem(key) {
    const stringKey = String(key);
    if (this.store.has(stringKey)) {
      let time = this.expires.get(stringKey);
      if (!time) {
        return String(this.store.get(stringKey));
      } else if (Date.now() < (time[0] + time[1])) {
        return String(this.store.get(stringKey));
      } else {
        this.store.delete(stringKey);
        this.expires.delete(stringKey);
        return null;
      }
    }
    return null;
  }

  setItem(key, val, time) {
    this.store.set(String(key), String(val));
    time = Number(time);
    if (time && time >= 0) {
      this.expires.set(String(key), [Date.now(), time]);
    }
  }
...

定时器问题,每次set除了要创建新定时器,还要清空已存在的定时器,亦即每次set都会更新定时器,就不必担心旧的定时器清空新set的值了。上面的处理貌似都没在set的时候处理定时器。

更重要的, localStorage要刷新后依然存在。貌似只有cookie模拟的实现了这个特性。
只是可能有以下问题:

localStorage.setItem('myKey', 'value', new Date() + 10000) // 10 秒钟后过期

// 如果再设置以下key,可以让myKey永不过时
localStorage.setItem('myKey_expires', 'abc') 

// 或者设置以下key,可以让myKey立即过时
localStorage.setItem('myKey_expires', '') 

总之使用关键字的形式,可能会因为特殊key的设置导致其他某些key的过期功能失效或不按预期执行。 所以个人还是更支持用定时器的方式。

可能看起来有点吹毛求疵。 只是实际面试中,面试官看到你的答案后,可能会加问一句,“这个方案可能会有什么潜在问题。 ”

参照楼下的做下总结,实现过期时间 两种常见的方案:
1.时间戳 可能会导致过期功能失效
2.定时器 切换页面时浏览器会自动延迟我们的定时器,以便于节约资源,可能会造成定时器时间不准确
这两种方案,各有优缺点,你会选择哪种

		const localStorageMock = (function() {
			let store = {}
			return {
				getItem: function(key) { return store[key] || null },
				setItem: function(key, value, time) { // time 是毫秒级别,过时时间必须大于0ms
					time = Number(time)?time:0;
					store[key] = value.toString()
					if(time>0){this.timeOut(key,time)}
				},
				timeOut:function(key,time){
					let that = this;
					let timer = setTimeout(function(){ that.removeItem(key);clearTimeout(timer);},time)
				},
				removeItem: function(key) { delete store[key] },
				clear: function() { store = {} },
			}
		})()

		Object.defineProperty(window, 'localStorage2', {
			value: localStorageMock
		})
		
		
		localStorage2.setItem('test',"test",3000)
		console.log(localStorage2.getItem("test"))  //test
		
		setTimeout(function(){
			console.log(localStorage2.getItem("test"))  //null
		},4000);

setTimeout不能保证到时间点就执行吧

const localStorage = (function () {
  let store = {}
  return {
    getItem: function (key) {
      return store[key] || null
    },
    setItem: function (key, val, time) {
      time = Number(time) || 0;
      store[key] = val.toString();
      if (time > 0) {
        this.timeOut(key, time);
      }
    },
    timeOut: function (key, time) {
      var timer = setTimeout(() => {
        this.removeItem(key);
        clearTimeout(timer)
      }, time);
    },
    removeItem: function (key) {
      delete store[key]
    },
    clear: function () {
      store = {}
    }
  }
})()
Object.defineProperty(window, 'localStorage2', {
  value: localStorage
})
class miniLocalStorage {
    static unSafeExpires = 2147483647;
    static Store = Object.create(null);
    static storeExpires = Object.create(null);

    constructor() {}

    getItem(key) {
        return miniLocalStorage.Store[key];
    }

    setItem(key, val, expires) {
        console.log('setItem', Date.now())
        try {
            if (typeof expires === 'number' && !Number.isNaN(expires)) {
                if (expires > miniLocalStorage.unSafeExpires) {
                    throw Error('expires overflow');
                }

                this._clearTimeout(key)

                miniLocalStorage.storeExpires[key + '_timer'] = setTimeout(()=>{
                    delete miniLocalStorage.Store[key]
                }
                , expires);
            }

            miniLocalStorage.Store[key] = val
        } catch (e) {
           throw e;
        }
    }

    removeItem(key) {
        this._clearTimeout(key)
        delete miniLocalStorage.Store[key];
    }

    clear() {
        for (const key in miniLocalStorage.Store) {
            this.removeItem(key)
        }
    }

    _clearTimeout(key) {
        const expiresId = key + '_timer'

        if (expiresId in miniLocalStorage.storeExpires) {
            clearTimeout(miniLocalStorage.storeExpires[expiresId])
            delete miniLocalStorage.storeExpires[expiresId];
        }
    }
}

var miniLocalStorage2 = (function() {
    const Store =  Object.create(null);

    function miniLocalStorage2() {}

    miniLocalStorage2.prototype.getItem = function getItem(key) {
        if (Store[key]) {
            if ('expires' in Store[key] && Date.now() > Store[key].expires) { 
                delete Store[key]
            } else {
                return Store[key].value;
            }
        }
    }

    miniLocalStorage2.prototype.setItem = function setItem(key, value, expires) {
        Store[key] = {
            value
        };
        console.log('setItem', Date.now())

        if (typeof expires === 'number' && !Number.isNaN(expires)) {
            Store[key].expires = (Date.now() + expires);
        }
    }

    miniLocalStorage2.prototype.removeItem = function removeItem(key) {
        delete Store[key];
    }

    miniLocalStorage2.prototype.clear = function clear() {
        for (const key in Store) {
            this.removeItem(key)
        }
    }

    return miniLocalStorage2;
}
)()

localStorage最主要的能力是本地存储,即使关闭浏览器还是存在的,因此基于cookie做个模拟

function setCookie(key, value = '', days) {
        if (!key) return
        let date = new Date()
        date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); // 设置过期时间为 days 天之后
        document.cookie = key + "=" + decodeURIComponent(value) + ";expires=" + date.toGMTString();
    }

    function getCookie(key) {
        let result = ''
        let cookie = document.cookie + ";"
        let queryString = key + "="
        let start = cookie.indexOf(queryString)
        if (start > -1) {
            start += queryString.length
            let end = cookie.indexOf(";", start)
            result = (cookie.substring(start, end))
        }
        // return getCookies()[key]
        return result
    }

    function getCookies() {
        let result = {}
        if (!document.cookie) return result
        document.cookie.split(';').forEach((item) => {
            const [key, value] = item.split('=')
            result[key.trim()] = value.trim()
        })
        return result
    }

    window.localStorage = window.localStorage || new LocalStorage()

    function LocalStorage() {
        const cookies = getCookies()
        this.length = Object.keys(cookies).length
        for (let [key, value] of Object.entries(cookies)) {
            this[key] = value
        }
    }

    LocalStorage.prototype = function () {
        function getItem(key) {
            return getCookie(key);
        }

        function setItem(key, value, days) {
            this[key] = value
            this.length++
            setCookie(key, value, days)
        }

        function removeItem(key) {
            this.length--
            delete this[key]
            setItem(key, '', -1);
        }

        function clear() {
            const cookies = getCookies();
            for (let key of Object.keys(cookies)) {
                delete this[key]
                removeItem(key);
            }
            this.length = 0
        }

        return {
            constructor: LocalStorage,
            getItem,
            setItem,
            removeItem,
            clear,
        }
    }()

其实只需要在获取数据的时候判断数据的时间是否已经过期就好了,最多再加一个time interval 去定时遍历数据的过期时间。

在set key的时候 加上一个timestamp
获取的时候拿这个timestampe进行时间判断 看是否过期

随便写了一个,麻烦大家看一下有没有不对。setItem的时候同时保存过期时间戳,同时可以根据需要是否自行定时检查。主要写一下思路,没有做过多的值的合法检查,关于值的合法检查的问题就不要批评我了。

class ExpirableLocalStorage {
  /**
   * 设置localStorage键值对
   * @param {Integer} expires 过期时间戳
   */
  static setItem(key, value, expires) {
    if (expires < Date.now()) {
      this.removeItem(key);
      return;
    }
    window.localStorage.setItem(key, value);
    window.localStorage.setItem(`expires_${key}`, expires);
  }

  /**
   * 获取localStorage值
   */
  static getItem(key) {
    if (parseInt(window.localStorage.getItem(`expires_${key}`)) < Date.now()) {
      this.removeItem(key);
      return null;
    }
    return window.localStorage.getItem(key);
  }

  /**
   * 移除localStorage键值对
   */
  static removeItem(key) {
    window.localStorage.removeItem(key);
    window.localStorage.removeItem(`expires_${key}`);
  }

  /**
   * 检查过期的键值对并移除
   */
  static checkExpired() {
    for (let i = 0, len = window.localStorage.length; i < len; i++) {
      const key = window.localStorage.key(i);
      const value = window.localStorage.getItem(key);
      const expires = window.localStorage.getItem(`expires_${key}`);
      if (value && expires && expires < Date.now()) {
        this.removeItem(key);
      }
    }
  }

  /**
   * 定时检查
   */
  static timingCheck(interval = 1000) {
    if (ExpirableLocalStorage.timer) {
      clearInterval(ExpirableLocalStorage.timer);
    }
    ExpirableLocalStorage.timer = setInter(() => {
      this.checkExpired();
    }, interval);
  }
}

用 cookie 模拟 localStorage

参考 https://developer.mozilla.org/zh-CN/docs/Web/API/Storage/LocalStorage

if (!window.localStorage) {
  window.localStorage = {
    getItem: function (sKey) {
      if (!sKey || !this.hasOwnProperty(sKey)) { return null; }
      return unescape(document.cookie.replace(new RegExp("(?:^|.*;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*"), "$1"));
    },
    key: function (nKeyId) {
      return unescape(document.cookie.replace(/\s*\=(?:.(?!;))*$/, "").split(/\s*\=(?:[^;](?!;))*[^;]?;\s*/)[nKeyId]);
    },
    setItem: function (sKey, sValue) {
      if(!sKey) { return; }
      document.cookie = escape(sKey) + "=" + escape(sValue) + "; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/";
      this.length = document.cookie.match(/\=/g).length;
    },
    length: 0,
    removeItem: function (sKey) {
      if (!sKey || !this.hasOwnProperty(sKey)) { return; }
      document.cookie = escape(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
      this.length--;
    },
    hasOwnProperty: function (sKey) {
      return (new RegExp("(?:^|;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
    }
  };
  window.localStorage.length = (document.cookie.match(/\=/g) || window.localStorage).length;
}

扩展 localStorage 支持 expires

(function () {
  var getItem = localStorage.getItem.bind(localStorage)
  var setItem = localStorage.setItem.bind(localStorage)
  var removeItem = localStorage.removeItem.bind(localStorage)
  localStorage.getItem = function (keyName) {
    var expires = getItem(keyName + '_expires')
    if (expires && new Date() > new Date(Number(expires))) {
      removeItem(keyName)
      removeItem(keyName + '_expires')
    }
    return getItem(keyName)
  }
  localStorage.setItem = function (keyName, keyValue, expires) {
    if (typeof expires !== 'undefined') {
      var expiresDate = new Date(expires).valueOf()
      setItem(keyName + '_expires', expiresDate)
    }
    return setItem(keyName, keyValue)
  }
})()

使用

localStorage.setItem('key', 'value', new Date() + 10000) // 10 秒钟后过期
localStorage.getItem('key')

使用方法好像expire参数需要修改一下:
localStorage.setItem('key', 'value', new Date(Date.now() + 10000)) // 10 秒钟后过期

模拟cookie功能,之前写了个lib: https://github.com/pagemarks/codes/blob/master/js/cache.js

function getStorage(isSession) {
    return isSession ? sessionStorage : localStorage;
}

const cache = {
    get(key, isSession = true, val = null) {
        const storage = getStorage(isSession);
        let ret = storage.getItem(key);
        if (!ret && val !== null) return val; //default val
        const char = ret && ret.slice(0, 1);
        if (char && (char === '{' || char === '[')) {
            ret = JSON.parse(ret);
            if (ret.expires) {
                if (ret.expires >= Date.now()) {
                    if ('value' in ret && Object.keys(ret).length === 2) {
                        ret = ret.value;
                    } else {
                        delete ret.expires;
                    }
                } else {
                    ret = val !== null ? val : null;
                    this.del(key, isSession)
                }
            }
        } else if (ret === 'true' || ret=== 'false') {
            ret = ret === 'true';
        }
        return ret;
    },
    hget(key, hash, isSession = true) {
        return this.get(key, isSession, {})[hash];
    },
    set(key, value, isSession = true, seconds = 0) {
        const storage = getStorage(isSession);
        let val = value;
        if (seconds) {
            const expires = Date.now() + seconds;
            val = Object.assign({}, typeof value === 'object' ? value : { value }, { expires });
        }
        if (typeof value === 'object') val = JSON.stringify(val);
        storage.setItem(key, val);
    },
    hset(key, hash, val, isSession = true) {
        const ob = this.get(key, isSession, {});
        ob[hash] = val;
        this.set(key, ob, isSession);
    },
    del(key, isSession = true) {
        const storage = getStorage(isSession);
        storage.removeItem(key);
    }
};
export default cache;

cookie 只能存那么一点东西, 你有cookie模拟意义在哪里

// Custom storage object
const storage: StateStorage = {
  getItem(key) {
    const itemStr = localStorage.getItem(key)

    // if the item doesn't exist, return null
    if (!itemStr) {
      return null
    }

    const item = JSON.parse(itemStr)
    const now = new Date()

    // compare the expiry time of the item with the current time
    if (now.getTime() > item.expiry) {
      // If the item is expired, delete the item from storage
      // and return null
      localStorage.removeItem(key)
      return null
    }
    return item.value
  },

  setItem(key, value) {
    const now = new Date()

    // `item` is an object which contains the original value
    // as well as the time when it's supposed to expire
    const item = {
      value: value,
      expiry: now.getTime() + 7 * 24 * 60 * 60 * 1000, //  7 days
    }
    localStorage.setItem(key, JSON.stringify(item))
  },

  // remove the entry with the given key
  removeItem(key) {
    window.localStorage.removeItem(key)
  },
}