读完这篇文章,我希望我可以给大家分享到的知识点
- Vue.js 中那些空间换时间的操作
- cache-loader的实现原理,缓存的应用
- babel-loader,cacheDirectory=true时缓存的应用
- 浏览器缓存
Vue.js 中那些空间换时间的操作
响应式操作
我们知道vue2.x是通过遍历对象的key去创建响应式对象的,如果一个对象足够大,那么递归遍历是非常耗时的
vue2.x中使用Object.defineProperty把对象变成响应式,一旦某个对象经过Object.defineProperty变成响应式对象后,会把响应式结果通过__ob__存储起来,这样一来,同样的对象如果再次执行observe,则从缓存的__ob__中直接拿到对应的响应式值并返回
vue对data进行处理,initState => initData => observe => 创建Observer对象 => def(value, 'ob', this)通过执行 def 函数把自身实例添加到数据对象 value 的 ob 属性上
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果存在__ob__,并且是一个Observer类型,直接从缓存中取,否则创建一个新的Observer对象
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 通过执行 def 函数把自身实例添加到数据对象 value 的 __ob__ 属性上
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
keep-alive
在谈keep-alive前,先聊一个算法LRU,LRU是最近最少使用页面置换算法(Least Recently Used),也就是首先淘汰最长时间未被使用的页面!
- 新数据插入到链表头部
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
- 当链表满的时候,将链表尾部的数据丢弃
keep-alive实现了vue的组件实例缓存,2.x和3.0的实现策略一致,都是对LRU缓存策略的应用,可以去看下源码实现
- vue2.x源码在vue/src/core/components/keep-alive.js
2.x的实现
export default {
created () {
// 创建缓存对象
this.cache = Object.create(null)
// 创建收集key
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
render () {
...省略其余源码...
// 如果命中缓存,返回缓存重的组件实例
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
// 更新keys在队列中的位置
remove(keys, key)
keys.push(key)
} else {
// 缓存vnode
cache[key] = vnode
// keys队列中添加当前组件key
keys.push(key)
// prune oldest entry
// 超出最大max限制,从队头删除key
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
}
}
- vue3.0源码在vue-next-master/packages/runtime-core/src/components/KeepAlive.ts
...省略其余源码...
const KeepAliveImpl = {
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 创建一个map以key=>value的形式创建缓存
const cache: Cache = new Map()
// 创建收集key
const keys: Keys = new Set()
return () => {
...省略其余源码...
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
// clone vnode if it's reused because we are going to mutate it
if (vnode.el) {
vnode = cloneVNode(vnode)
if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
rawVNode.ssContent = vnode
}
}
// #1513 it's possible for the returned vnode to be cloned due to attr
// fallthrough or scopeId, so the vnode here may not be the final vnode
// that is mounted. Instead of caching it directly, we store the pending
// key and cache `instance.subTree` (the normalized vnode) in
// beforeMount/beforeUpdate hooks.
pendingCacheKey = key
// 如果命中缓存,返回缓存重的组件实例
if (cachedVNode) {
// copy over mounted state
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// avoid vnode being mounted as fresh
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
keys.delete(key)
// 更新keys在队列中的位置
keys.add(key)
} else {
keys.add(key)
// prune oldest entry
// 超出最大max限制,从队头删除key
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
}
}
}
编译
我们都知道.vue文件中的template是通过vue-template-compiler进行ast转换的,然而编译是非常耗时的,如果每次都重新分析ast,组装render函数,性能肯定大大降低,因此在设计的时候也用到了缓存,以空间换时间,达到了渲染优化的效果,这里以2.x版本为例
2.x
// vue/src/compiler/to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
...省略其余源码...
const key = options.delimiters
? String(options.delimiters) + template
: template
// 如果命中缓存,直接从缓存中取render函数
if (cache[key]) {
return cache[key]
}
const compiled = compile(template, options)
...省略其余源码...
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
...省略其余源码...
// 编译成功后vue会给每一个模版添加缓存
return (cache[key] = res)
}
}
cache-loader
webpack编译时优化,cache-loader是一个很好的应用,cache-loader作为webpack的loader,会在pitch和loader两个阶段分别做一些事情:
- pitch阶段:校验缓存文件是否可用
- loader阶段:判断当前loader的文件是否需要重新生成缓存
pitch阶段
webpack再次编译时,根据当前正在处理的文件,尝试读取.cache目录中对应的cache文件,处理当前文件以及所以来的文件,比较当前文件与上一次loader的产物
function pitch(remainingRequest, prevRequest, dataInput) {
...省略其余源码...
data.cacheKey = cacheKeyFn(options, data.remainingRequest);
readFn(data.cacheKey, (readErr, cacheData) => {
...省略其余源码...
}
}
function read(key, callback) {
fs.readFile(key, 'utf-8', (err, content) => {
if (err) {
callback(err);
return;
}
try {
const data = BJSON.parse(content);
callback(null, data);
} catch (e) {
callback(e);
}
});
}
loader阶段
判断当前文件是否需要重新生成缓存,判断逻辑:如果pitch阶段的判断当前文件的缓存失效了,那么loader阶段就要去生成缓存
function loader(...args) {
...省略其余源码...
// 默认开启缓存
let cache = true
...省略其余源码...
async.parallel(
[
(cb) => async.mapLimit(dependencies, 20, toDepDetails, cb),
(cb) => async.mapLimit(contextDependencies, 20, toDepDetails, cb),
],
(err, taskResults) => {
if (err) {
callback(null, ...args);
return;
}
if (!cache) {
callback(null, ...args);
return;
}
const [deps, contextDeps] = taskResults;
writeFn(
data.cacheKey,
{
remainingRequest: pathWithCacheContext(
options.cacheContext,
data.remainingRequest
),
dependencies: deps,
contextDependencies: contextDeps,
result: args,
},
() => {
// ignore errors here
callback(null, ...args);
}
);
}
}
function write(key, data, callback) {
const dirname = path.dirname(key);
const content = BJSON.stringify(data);
// const directories = new Set();
if (directories.has(dirname)) {
// for performance skip creating directory
fs.writeFile(key, content, 'utf-8', callback);
} else {
mkdirp(dirname, (mkdirErr) => {
if (mkdirErr) {
callback(mkdirErr);
return;
}
directories.add(dirname);
fs.writeFile(key, content, 'utf-8', callback);
});
}
}
babel-loader缓存的应用
webpack配置中使用babel-loader解析源码时可以配置cacheDirectory=true来开启缓存,执行babel-loader中定义的cache方法
async function loader(source, inputSourceMap, overrides) {
...省略其余源码...
const {
cacheDirectory = null,
cacheIdentifier = JSON.stringify({
options,
"@babel/core": transform.version,
"@babel/loader": version,
}),
cacheCompression = true,
metadataSubscribers = [],
} = loaderOptions;
let result;
if (cacheDirectory) {
result = await cache({
source,
options,
transform,
cacheDirectory,
cacheIdentifier,
cacheCompression,
});
} else {
result = await transform(source, options);
}
}
cache方法的核心实现在handleCache中,实现原理和cache-loader类似,尝试读取缓存,存在就返回,否则继续创建缓存目录,通过babel.transform转译代码,执行gzip压缩并写入缓存
module.exports = async function (params) {
...省略其余源码...
return await handleCache(directory, params);
};
const handleCache = async function (directory, params) {
try {
// No errors mean that the file was previously cached
// we just need to return it
return await read(file, cacheCompression);
} catch (err) {}
// Make sure the directory exists.
try {
await makeDir(directory);
} catch (err) {
if (fallback) {
return handleCache(os.tmpdir(), params);
}
throw err;
}
// Otherwise just transform the file
// return it to the user asap and write it in cache
const result = await transform(source, options);
try {
await write(file, cacheCompression, result);
} catch (err) {
if (fallback) {
// Fallback to tmpdir if node_modules folder not writable
return handleCache(os.tmpdir(), params);
}
throw err;
}
return result;
}
浏览器缓存
缓存位置
- service worker
- memory cache
- disk cache
- push cache
缓存类型
强缓存
- Expires
用来指定资源到期的时间,是服务端的具体时间点,Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效
- Cache-Control
已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,Cache-Control的优先级高于Expires,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求,与Expires的区别就是前者是绝对时间,而后者是相对时间
Cache-control: max-age=86400
| 指令 | 作用 |
|---|---|
| public | 响应可以由任何缓存存储,即使响应通常是不可缓存的 |
| private | 响应可能仅由浏览器的缓存存储,即使响应通常是不可缓存的 |
| no-cache | 响应可以由任何缓存存储,即使响应通常是不可缓存的。但是,在使用存储的响应之前,必须首先通过源服务器的验证,因此,不能将no cache与immutable结合使用 |
| no-store | 响应不能存储在任何缓存中 |
| max-age=<seconds> | 资源被认为是新鲜的最长时间。与Expires不同,此指令与请求的时间有关 |
| s-maxage=<seconds> | 作用同max-age |
| max-stale=<seconds> | 指示客户端将接受过时的响应。以秒为单位的可选值表示客户端将接受的过期上限 |
| min-fresh=<seconds> | 指示客户端希望响应至少在指定的秒数内保持新鲜 |
| stale-while-revalidate=<seconds> | 指示客户端将接受过时响应,同时在后台异步检查新响应 |
| stale-if-error=<seconds> | 指示如果对新响应的检查失败,客户端将接受过时的响应 |
| must-revalidate | 指示一旦资源变为过时,如果未在源服务器上成功验证,缓存就不能使用其过时副本 |
| proxy-revalidate | 必须重新验证,但仅限于共享缓存(例如代理) |
| immutable | 指示响应主体不会随时间而更改 |
| no-transform | 中间缓存或代理无法编辑响应正文、内容编码、内容范围或内容类型 |
| only-if-cached | 由客户端设置,以指示响应的“不使用网络” |
协商缓存
Last-Modified & If-Modified-Since
-
服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例如 Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
-
浏览器将这个值和内容一起记录在缓存数据库中
-
下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段
-
服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据
缺陷:
- 如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。
- 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用
Etag & If-None-Match
Etag 存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的 Etag 字段。之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。
Etag 的优先级高于 Last-Modified
缓存机制
- 强缓存优先于协商缓存
- 协商缓存失效,返回200,重新返回资源和缓存标识
- 协商缓存生效,返回304,继续使用缓存
缓存策略
- 频繁变动的资源
- 不常变动的资源