资讯专栏INFORMATION COLUMN

探索vue源码之缓存篇

Forest10 / 488人阅读

摘要:中采用算法来实现缓存的高效管理。通过这两个属性,将缓存中的变量连接起来。以上图举例缓存这个对象中保存了三个变量。如果缓存数组为空,则返回将为传入参数的缓存对象标识为最常使用的,即,并调整双向链表,返回改变后的。

vue.js入坑也有了小半年的时间了,圈子里一直流传着其源码优雅、简洁的传说。
最近的一次技术分享会,同事分享vue.js源码的缓存部分,鄙人将其整理出来,与大家一起学习

一、从链表说起

首先我们来看一下链表的定义:

链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)

其中的双向链表是我们今天的主角:

双向链表也叫双链表。双向链表中不仅有指向后一个节点的指针,还有指向前一个节点的指针。这样可以从任何一个节点访问前一个节点,当然也可以访问后一个节点,以至整个链表。一般是在需要大批量的另外储存数据在链表中的位置的时候用。

图示如下(图片来自维基百科-链表):

想象一群人手拉手站成一排,除了队头跟队尾,可以根据每个人的左手以及右手找到排在其左边或者右边的人,这也可以看成一种双向链表

JavaScript中,我们可以通过对象的属性来实现双向链表。

而在vue.js中,作者正是利用类似双向链表的方式实现缓存的利用

二、LRU算法

在缓存中,利用类似双向链表来管理缓存并不难的。难的是如何更加高效的管理缓存,如何在缓存达到其最大内存空间,删除程序中最不常用的变量,而不是随机删除,造成最常用的变量被误删的情况。

vue.js中采用LRU算法来实现缓存的高效管理。

LRULeast Recently Used的简称,具体内容可以查看GitHub,其有以下优点:

基于双向链表改变缓存对象中entry的排序,复杂度低

缓存对象有一个head(最近最少使用的项)和一个tail(最近最多使用的项)

headtail都是entry,一个entry可能会有一个newer entry以及一个older entry(双向链接,older entry更接近headnewer entry更接近tail

使用一个key就可以遍历这个缓存对象,也就意味着只有o(1)的复杂度,内存消耗非常小

可以通过下面的图来更好的理解LRU算法:

    entry             entry             entry             entry        
    ______            ______            ______            ______       
   | head |.newer => |      |.newer => |      |.newer => | tail |      
   |  A   |          |  B   |          |  C   |          |  D   |      
   |______| <= older.|______| <= older.|______| <= older.|______|      
                                                                       
removed  <--  <--  <--  <--  <--  <--  <--  <--  <--  <--  <--  added

如果缓存达到最大,那么每次只需要将head删除就行了,保证了删除的项是最不常用的项

还是拿站成一排的人来举例。

有两个指示牌,上面分别写着tail以及headhead指向队伍的第一个人,tail指向队伍的最后一个人。

假设队伍有10个人,按照队伍的排列从队首到队尾依次编号a b c d ··· jhead指向atail指向j

下面分成五种情况来说明队伍的变化:

如果叫到a(使用了数组里面第一个变量),就将a放到队尾,再手拉手重新组成一个新的队伍。并将原来指向jtail现在指向a。再让原来指向ahead指向现在队伍的第一个人b

如果叫到b c d ··· i之间任何一个人,则将其从队伍中抽出,放到队尾,重新排队,再改变tail的指向为这个人

如果叫到j,则保持队伍不变

队伍达到最大人数,则去掉head指向的编号a,并改变head指向编号b,再在队尾增加一个人,假定编号为k,最后则将tail指向编号k

队伍没有达到最大人数,需要增加队伍人数。只需要在队尾增加编号为k的人。再将tail指向编号k

三、源码分析

我们可以通过一张图来先简单理解作者的数据结构:

作者在caches对象的_keymap里面保存所需要缓存的变量,通过older以及newer这两个属性来实现双向链表。older指向其前一个对象,newer指向其后一个对象。通过这两个属性,将缓存中的变量连接起来。

以上图举例:
缓存caches这个对象中保存了三个变量:key1key2key3

header指向key1

tail指向key2

指向如下:

         key1              key2              key3       
        ______            ______            ______       
       | head |.newer => |      |.newer => | tail |      
       |      |          |      |          |      |      
       |______| <= older.|______| <= older.|______|  

下面我们来看作者对这些数据的处理所使用的方法

文件位置:src/cache.js

首先export构造函数Cache

export default function Cache (limit) {
  // 标识当前缓存数组的大小
  this.size = 0
  // 标识缓存数组能达到的最大长度
  this.limit = limit
  // head(最不常用的项),tail(最常用的项)全部初始化为undefined
  this.head = this.tail = undefined
  this._keymap = Object.create(null)
}

接下来作者在Cache的原型链上面分别定义了:

put:在缓存中加入一个key-value对象,如果缓存数组已经达到最大值,则返回被删除的entry,即head,否则返回undefined

shift:在缓存数组中移除最少使用的entry,即head,返回被删除的entry。如果缓存数组为空,则返回undefined

get:将key为传入参数的缓存对象标识为最常使用的entry,即tail,并调整双向链表,返回改变后的tail。如果不存在key为传入参数的缓存对象,则返回undefined

a) get:

Cache.prototype.get = function (key, returnEntry) {
  var entry = this._keymap[key]
  // 如果查找不到含有`key`这个属性的缓存对象
  if (entry === undefined) return
  // 如果查找到的缓存对象已经是 tail (最近使用过的)
  if (entry === this.tail) {
    return returnEntry
      ? entry
      : entry.value
  }
  // HEAD--------------TAIL
  //   <.older   .newer>
  //  <--- add direction --
  //   A  B  C    E
  if (entry.newer) {
  // 处理 newer 指向
    if (entry === this.head) {
      // 如果查找到的缓存对象是 head (最近最少使用过的)
      // 则将 head 指向原 head 的 newer 所指向的缓存对象
      this.head = entry.newer
    }
    // 将所查找的缓存对象的下一级的 older 指向所查找的缓存对象的older所指向的值
    // 例如:A B C D E
    // 如果查找到的是D,那么将E指向C,不再指向D
    entry.newer.older = entry.older // C <-- E.
  }
  if (entry.older) {
  // 处理 older 指向
    // 如果查找到的是D,那么C指向E,不再指向D
    entry.older.newer = entry.newer // C. --> E
  }
  // 处理所查找到的对象的 newer 以及 older 指向
  entry.newer = undefined // D --x
  // older指向之前使用过的变量,即D指向E
  entry.older = this.tail // D. --> E
  if (this.tail) {
    // 将E的newer指向D
    this.tail.newer = entry // E. <-- D
  }
  // 改变 tail 为D 
  this.tail = entry
  return returnEntry
    ? entry
    : entry.value
}

b) put:

Cache.prototype.put = function (key, value) {
  var removed

  var entry = this.get(key, true)
  // 如果不存在 key 这样属性的缓存对象,才能调用 put 方法
  if (!entry) {
    if (this.size === this.limit) {
    // 如果缓存数组达到上限,则先删除 head 指向的缓存对象
      removed = this.shift()
    }
    // 初始化赋值
    entry = {
      key: key
    }
    this._keymap[key] = entry
    if (this.tail) {
    // 如果存在tail(缓存数组的长度不为0),将tail指向新的 entry
      this.tail.newer = entry
      entry.older = this.tail
    } else {
    // 如果缓存数组的长度为0,将head指向新的entry
      this.head = entry
    }
    this.tail = entry
    this.size++
  }
  entry.value = value

  return removed
}

c) shift

Cache.prototype.shift = function () {
  var entry = this.head
  if (entry) {
    // 删除 head ,并改变指向
    this.head = this.head.newer
    this.head.older = undefined
    entry.newer = entry.older = undefined
    // 同步更新 _keymap 里面的属性值
    this._keymap[entry.key] = undefined
    // 同步更新 缓存数组的长度
    this.size--
  }
  return entry
}
四、后记

从整个的代码来看,需要学习的不仅仅是LRU算法,作者的对于Object的处理方式也值的我们评味一番。

没有选择去遍历entry,选择通过在Cache内增加一个_keymap属性,通过这个属性来管理entry,实现keynewerolder状态的分离,减少代码的复杂度

五、附

源码版本为v1.0.26

主要内容来自爱屋吉屋FE团队的技术分享会

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80119.html

相关文章

  • 前端资源系列(4)-前端学习资源分享&前端面试资源汇总

    摘要:特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 本以为自己收藏的站点多,可以很快搞定,没想到一入汇总深似海。还有很多不足&遗漏的地方,欢迎补充。有错误的地方,还请斧正... 托管: welcome to git,欢迎交流,感谢star 有好友反应和斧正,会及时更新,平时业务工作时也会不定期更...

    princekin 评论0 收藏0
  • Vue原理】Compile - 源码 从新建实例到 compile结束的主要流程

    摘要:页面这个实例,按理就需要解析两次,但是有缓存之后就不会理清思路也就是说,其实内核就是不过是经过了两波包装的第一波包装在中的内部函数中内部函数的作用是合并公共和自定义,但是相关代码已经省略,另一个就是执行第二波包装在中,目的是进行缓存 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 ...

    CODING 评论0 收藏0
  • javascript闭包不完全探索记录02:闭包?干嘛使!

    摘要:温馨提示作者的爬坑记录,对你等大神完全没有价值,别在我这浪费生命温馨提示续本文将会成为一篇笔记类型的文章,记录闭包具体的应用方式温馨提示再续本文存在错误,会慢慢改进的,请不要把我说的当真在上一篇博文闭包不完全探索记录闭包啥馅的中,对中 温馨提示:作者的爬坑记录,对你等大神完全没有价值,别在我这浪费生命温馨提示-续:本文(maybe)将会成为一篇笔记类型的文章,记录闭包具体的应用方式温馨...

    Render 评论0 收藏0
  • Vue原理】依赖更新 - 源码

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理依赖更新源码版如果对依赖收集完 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于...

    moven_j 评论0 收藏0

发表评论

0条评论

Forest10

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<