实现迷你lru-cache

398 阅读6分钟

前言

抱歉取这个标题是为了博眼球,其实我并没有看过lru-cache的源码,代码实现是根据个人一些知识基础和实践经验而成的.

如果对lru-cache的使用不了解的同学可以先看看这个链接 lru-cache。如果您对事件轮询和Vue数据响应式不了解的话可以先去查阅一些相关的文章了解一些这块知识,这样有利于您阅读理解本文的代码实现。

目录骨架

- cache
    - array.js
    - scheduler.js
    - observe.js
    - index.js

index.js

定义了Cache

// observe的代码先不用管
import observe from './observe'
import run from './scheduler'

const config = {
    maxAge: 1000 * 60, // 默认一分钟
}

class Cache {
    constructor({ maxAge } = {}) {
        // 缓存时间
        this.maxAge = maxAge || config.maxAge
        // 存储缓存数据
        this.cache = {}
    }
    
    // 开启一个定时器
    timing(key, maxAge) {
        return setTimeout(() => {
            // 超过指定时间清除缓存数据
            delete this.cache[key]
        }, maxAge)
    }
    
    // 重置定时器
    retiming(key) {
        if (!this.cache[key]) return
        const { id, maxAge } = this.cache[key]
        clearTimeout(id)
        this.cache[key].id = timing(key, maxAge)
    }
    
    // 当调用get的时候,如果缓存的数据还在,那么会重置定时器
    get(key) {
        if (!this.cache[key]) return null
        run(() => {
            this.retiming(key)
        })
        return this.cache[key].value
    }
    
    // 这里注意option
    // optiion有两个可选属性,maxAge和deep
    // maxAge可以单独设置某个key的数据缓存时间
    // deep默认为false, 也就是调用get的时候才会重置定时器
    // 如果设置为true, 那么会对数据做响应式处理
    // 效果是当我们访问或修改缓存数据的某个属性时也会重置定时器
    set(key, value, option = {}) {
        // 对于基本数据类型和函数是不做缓存的,您也可以按需更改
        if (typeof value === 'object' && value !== null) {
            // 如果key重复,那么先清除定时器和缓存数据
            if (this.cache[key]) {
                const id = this.cache[key].id
                clearTimeout(id)
                delete this.cache[key]
            }
            // 当deep为true时,会对value做响应式处理, 在访问或修改数据时
            // 会触发get或set方法(注意这里的get和set是调用Object.defineProperty
            // 时设置的存取描述符get,set方法),那么就会调用这个handler方法
            // 先记住有这个么handler函数就行,后面代码会用到,到时候就清楚了
            const handler = () => {
                this.retiming(key)
            }
            // 如果没有配置option的maxAge属性,那么使用默认值
            const maxAge = option.maxAge || this.maxAge
            this.cache[key] = {
                maxAge,
                id: this.timing(key, maxAge),
                value: option.deep ? observe(value, handler) : value,
            }
        }
    }
}

export default Cache

observe.js

对缓存数据做响应式处理 在看下面代码之前可以先思考一个问题,如果缓存的数据是Vue实例的data中的数据,我们知道Vue会对data数据做响应式处理,那么我们该如何处理已经是响应式对象的数据?

// 先不用管这部分代码
import run from './scheduler'
import protoAugment from './array'

// 遍历数组元素做响应式处理
function observeArray(array, handler) {
    for (let i = 0; i < array.length; i += 1) {
        observe(array[i], handler)
    }
}

// 对对象做响应式处理
function defineReactive(obj, key, value, handler) {
    // 如果value是对象或者数组那么递归observe
    observe(value)
    // 这里获取obj对应key属性的属性描述符
    // 如果这个obj已经是响应式对象,那么我们要获取它get和set
    // 然后对get和set做一层包装,注入一些逻辑
    const descriptor = Object.getOwnPropertyDescriptor(obj, key)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 这里调用run方法将handler作为参数传入
            // 这个函数的实现逻辑后面讲,这里先不管
            run(handler)
            return descriptor.get ? descriptor.get() : value
        },
        set(newVal) {
            run(handler)
            // 这里需要注意执行的先后顺序,需要先执行descriptor.set,
            // 然后在对newVal进行observe
            // 首先需要明白的是,如果obj已经是响应式对象,那我们的响应式处理
            // 是在原始的get, set上在进行一次包装而已
            // 所以newVal需要先经过原始的set处理后成为一个响应式对象(以Vue为例),然后在这基础上在进行observe.
            // 如果先observe,在调用原始的set,那么observe所做的事情都没有
            // 意义,因为newVal的get, set函数的逻辑只取决于原始的set函数,
            // observe的所做的事情会被覆盖掉
            // 举个例子,有一个树,我们要将它做成一把锤子,那么原始的set逻辑
            // 就是要将树做成锤子的形状,observe的逻辑就是在以有的锤子基础上
            // 加一些着色和雕饰,这样的逻辑就是正常的
            // 如果我们先着色和雕饰,那么是没有意义的,因为莫得了个锤子
            if (descriptor.set) descriptor.set(newVal)
            if (value === newVal) return
            value = observe(newVal)
        },
    })
}

// 注意hanlder这个参数,如果不清楚handler是什么可以看一下index.js那部分
function observe(obj, handler) {
    if (typeof obj === 'object' && obj !== null) {
        // 注意这里我们将handler绑定到obj的__cache__属性上
        // 它会在对数组类型做响应式处理的时候用到,这里先记住有这个东西
        obj.__cache__ = handler
        if (Array.isArray(obj)) {
            // 这个分支就是负责数组的响应式处理, 这里先不用管
            protoAugment(obj, Object.getPrototypeOf(obj))
            observeArray(obj)
        } else {
            Object.keys(obj).forEach((key) => {
                defineReactive(obj, key, obj[key], handler)
            })
        }
    }
    return obj
}

export default observe

array.js

负责处理数组的响应式。

import observe from './observe'
import run from './scheduler'

const methodsToPatch = [
    'pop',
    'push',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse',
]

// 如果obj不是经过vue处理的数组,那么arrayProto一般就是Array.prototype
// 如果是,那么是一个原型指向Array.prototype的对象,这个对象对数组的一些
// 方法进行了重写
function protoAugment(obj, arrayProto) {
    if (protoAugment.proto) {
        Object.setPrototypeOf(obj, protoAugment.proto)
        return
    }
    const proto = Object.create(arrayProto)
    methodsToPatch.forEach((method) => {
        Object.defineProperty(proto, method, {
            configurable: true,
            enumerable: true,
            writable: false,
            value: function (...args) {
                // 重点, 调用数组的push等方法时需要取出handler然后调用run方法
                // run函数的逻辑后面讲,先不管
                const handler = this.__cache__
                run(handler)
                const origin = arrayProto[method]
                const result = origin.apply(this, args)
                // 保存新增的数组元素
                let inserted
                switch (method) {
                    case 'push':
                    case 'unshift':
                        inserted = args
                        break
                    case 'splice':
                        inserted = args.slice(2)
                        break
                }
                // 对于新增元素,需要进行observe
                // 需要注意先调用原型上对应的method方法,然后在执行observe
                // 原因是因为Vue会对新增元素进行响应式处理,所以observe逻辑
                // 要放在其后面
                if (inserted) observe(inserted)
                return result
            }
        })
    })
    Object.setPrototypeOf(obj, proto)
    protoAugment.proto = proto
}

export default protoAugment

scheduler.js

负责handler的执行时机。

// 搜集handler
const queue = []
let wait = false

// 将callback函数的执行时机放到下一个微任务中
function nextTick(callback) {
    Promise.resolve().then(callback)
}

function resetSchedulerState() {
    queue.length = 0
    wait = false
}

function flushSchedulerQueue() {
    for (let i = 0; i < queue.length; i += 1) queue[i]()
    resetSchedulerState()
}

function run(handler) {
    // 如果队列中已经有handler了,那不需要重复添加
    if (!queue.includes(handler)) queue.push(handler)
    if (!wait) {
        wait = true
        nextTick(flushSchedulerQueue)
    }
}

export default run

总结

在这里再次为标题党的行为说声抱歉,同时很感谢您观看我的文章。当我们想在修改或访问缓存数据的时候重置定时器,那么手动调用cache.get方法即可,就没必要对数据进行响应式处理, 虽然可以自动重置定时器,但是代价是更高的性能开销,另外对于缓存Vue实例的data数据,需要修改data的get, set方法,那么改的时候就需要小心处理了,其实代码中响应式处理有没有玩好我也没把握。。。所以还是建议不要这么干。知道可以在触发get, set方法之前或之后是可以注入一些额外的逻辑代码即可,在将来如果有某些功能需要,那么就可以尝试使用这种方式。

github链接

github