lodash源码分析之缓存方式的选择

每个人心里都有一团火,路过的人只看到烟。

——《至爱梵高·星空之谜》

本文为读 lodash 源码的第八篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodash

gitbook也会同步仓库的更新,gitbook地址:pocket-lodash

前言

在《lodash源码分析之Hash缓存》和《lodash源码分析之List缓存》介绍了 lodash 的两种缓存方式,这两种缓存方式都实现了和 一致的数据管理接口,其中 缓存只在不支持 的环境中使用,那何时使用 缓存,何时使用 或者 缓存呢?这就是 类所需要做的事情。

缓存方式的选择

从之前的分析可以看出, 缓存完全可以用 缓存或 来代替,为什么 lodash 不干脆统一用一种缓存方式呢?

原因是在数据量较大时,对象的存取比 或者数组的性能要好。

因此,ladash 在能够用 缓存时,都尽量使用 缓存,而能否使用 缓存的关键是 的类型。

以下便为 lodash 决定使用缓存方式的流程:

首先,判断 的类型,以是否为 类型为成两拨,如果是以上的类型,再判断 是否等于 ,如果不是 ,则使用 缓存。不能为 的原因是,大部分 JS 引擎都以这个属性来保存对象的原型。

如果不是以上的类型,则判断 是否,如果为 ,则依然使用 缓存,其余的则使用 或者 缓存。

从上面的流程图还可以看到,在可以用 来缓存 中,还以是否为 类型分成了两个 对象来缓存数据,为什么要这样呢?

我们都知道,对象的 如果不是字符串或者 类型时,会转换成字符串的形式,因此如果缓存的数据中同时存在像数字 和字符串 时,数据都会储存在字符串 上。这两个不同的键值,最后获取的都是同一份数据,这明显是不行的,因此需要将要字符串的 和其他需要转换类型的 分开两个 对象储存。

作用与用法

所做的事情有点像函数重载,其调用方式和 一致。

new MapCache([

  ['key', 'value'],

  [{key: 'An Object Key'}, 1],

  [Symbol(),2]

])

所返回的结果如下:

{

  size: 3,

  \_\_data\_\_: {

    string: {

      ... 

    },

    hash: {

      ...

    },

    map: {

      ...  

    }

  }

}

可以看到, 里根据 的类型分成了 三种类型来储存数据。其中 都是 的实例,而 则是 或 的实例。

接口设计

同样实现了跟 一致的数据管理接口,如下:

依赖

import Hash from './Hash.js'

import ListCache from './ListCache.js'

lodash源码分析之Hash缓存

lodash源码分析之List缓存

源码分析

function getMapData({ \_\_data\_\_ }, key) {

  const data = \_\_data\_\_

  return isKeyable(key)

    ? data[typeof key == 'string' ? 'string' : 'hash']

    : data.map

}



function isKeyable(value) {

  const type = typeof value

  return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')

    ? (value !== '\_\_proto\_\_')

    : (value === null)

}



class MapCache {



  constructor(entries) {

    let index = -1

    const length = entries == null ? 0 : entries.length



    this.clear()

    while (++index < length) {

      const entry = entries[index]

      this.set(entry[0], entry[1])

    }

  }



  clear() {

    this.size = 0

    this.\_\_data\_\_ = {

      'hash': new Hash,

      'map': new (Map || ListCache),

      'string': new Hash

    }

  }



  delete(key) {

    const result = getMapData(this, key)['delete'](key)

    this.size -= result ? 1 : 0

    return result

  }



  get(key) {

    return getMapData(this, key).get(key)

  }



  has(key) {

    return getMapData(this, key).has(key)

  }



  set(key, value) {

    const data = getMapData(this, key)

    const size = data.size



    data.set(key, value)

    this.size += data.size == size ? 0 : 1

    return this

  }

}

是否使用Hash

function isKeyable(value) {

  const type = typeof value

  return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')

    ? (value !== '\_\_proto\_\_')

  : (value === null)

}

这个函数用来判断是否使用 缓存。返回 表示使 缓存,返回 则使用 或者 缓存。

这个在流程图上已经解释过,不再作详细的解释。

获取对应缓存方式的实例

function getMapData({ \_\_data\_\_ }, key) {

  const data = \_\_data\_\_

  return isKeyable(key)

    ? data[typeof key == 'string' ? 'string' : 'hash']

    : data.map

}

这个函数根据 来获取储存了该 的缓存实例。

即为 实例中的 性的值。

如果使用的是 缓存,则类型为字符串时,返回 中的 属性的值,否则返回 属性的值。这两者都为 实例。

否则返回 属性的值,这个可能是 实例或者 实例。

constructo

constructor(entries) {

  let index = -1

  const length = entries == null ? 0 : entries.length



  this.clear()

  while (++index < length) {

    const entry = entries[index]

    this.set(entry[0], entry[1])

  }

}

构造器跟 一模一样,都是先调用 方法,然后调用 方法,往缓存中加入初始数据。

clea

clear() {

  this.size = 0

  this.\_\_data\_\_ = {

    'hash': new Hash,

    'map': new (Map || ListCache),

    'string': new Hash

  }

}

是为了清空缓存。

这里值得注意的是 属性,使用 来保存不同类型的缓存数据,它们之间的区别上面已经论述清楚。

这里也可以清晰地看到,如果在支持 的环境中,会优先使用 ,而不是

has

has(key) {

  return getMapData(this, key).has(key)

}

用来判断是否已经有缓存数据,如果缓存数据已经存在,则返回

这里调用了 方法,获取到对应的缓存实例( 的实例),然后调用的是对应实例中的 方法

set

set(key, value) {

  const data = getMapData(this, key)

  const size = data.size



  data.set(key, value)

  this.size += data.size == size ? 0 : 1

  return this

}

用来增加或者更新需要缓存的值。 的时候需要同时维护 和缓存的值。

这里除了调用对应的缓存实例的 方法来维护缓存的值外,还需要维护自身的 属性,如果增加值,则加

get

get(key) {

  return getMapData(this, key).get(key)

}

方法是从缓存中取值。

同样是调用对应的缓存实例中的 方法

delete

delete(key) {

  const result = getMapData(this, key)['delete'](key)

  this.size -= result ? 1 : 0

  return result

}

方法用来删除指定 的缓存。成功删除返回 , 否则返回 。 删除操作同样需要维护 属性。

同样是调用对应缓存实例中的 方法,如果删除成功,则需要将自身的 的值减

参考

License

署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)

最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

作者:对角另一面

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏博客园

Redis命令与配置

    slaveof  127.0.0.1 6379(设置Mater的Host以及Port)

1554
来自专栏电光石火

获取URL地址中的GET参数

/*-----------------实现1--------------------*/ function getPar(par){ //获取当前URL...

2279
来自专栏Python小屋

使用Python检查密码安全程度

本文主要演示几种内置用法的用法和代码优化技巧,所以没有使用正则表达式。 import string def check(pwd): #密码必须至少包含6个字符...

3275
来自专栏DannyHoo的专栏

block和代理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

1413
来自专栏好好学java的技术栈

「附数据结构资源」玩转java并发(六):深入线程Thread类的start()方法和run()方法

java的线程是通过java.lang.Thread类来实现的。VM启动时会有一个由主方法所定义的线程。可以通过创建Thread的实例来创建新的线程。每个线程都...

1062
来自专栏python3

习题31:访问列表元素

访问列表中的元素,使用下标的方式,通常以0开始(为什么是0而不是1),这里程序的设计就是如此,个人觉得没有必要纠结,如有兴趣,可自行查看资料

822
来自专栏noteless

-1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),

java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法...

1104
来自专栏desperate633

第7课 创建计算字段拼接字段执行简单的算术运算

什么是计算字段? 就是直接从数据库中检索出转换,计算或者格式化的数据,而不是检索出数据之后,再在客户端应用程序中重新格式化。

732
来自专栏技巅

Thrift之代码生成器Compiler原理及源码详细解析3

2366
来自专栏Linyb极客之路

并发编程之volatile关键字

一、Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的。 Java内存模型规定了所有的变量都存储在主内存中...

3555

扫码关注云+社区

领取腾讯云代金券