浅析 Vue.js 中那些空间换时间的操作(好文收藏!)
本期的问题:在 Vue.js 模板的编译过程中,我们已经知道静态提升的好处:针对静态节点不用每次在 render
阶段都执行一次 createVNode
创建 vnode
对象。但它有没有成本呢?为什么?
在回答问题前,我们简单回顾一下什么是静态提升,假设我们有如下模板:
<p>hello {{ msg }}</p>
<p>static</p>
在开启 hoistStatic
编译配置的情况下最终编译结果如下:
import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("p", null, "hello " + _toDisplayString(_ctx.msg), 1 /* TEXT */),
_hoisted_1
], 64 /* STABLE_FRAGMENT */))
}
我们发现静态节点 <p>static</p>
编译生成 vnode
的过程被提取到 render
函数外面了,然后在 render
函数内部就可以直接拿到静态节点的编译结果 _hoisted_1
。
之所以可以这么做,是因为静态节点是不会改变的,所以它编译生成的 vnode
也不会改变,而动态节点是变化的,必须在每次 render
的时候动态创建,它的 vnode
生成过程就不能提取到外面。
显然,这样做的好处是由于 render
函数在每次组件重新渲染的时候都会执行,而针对静态节点,创建 vnode
的过程只执行一次,相当于提升了 render
的性能。
但是这么做也是有成本的,创建的 hoisted_1 vnode
对象始终会在内存中占用,并不会在每次 render
函数执行后释放。
其实,这就是典型的空间换时间的做法,在绝大部分的场景,性能好意味着更好的用户体验,而牺牲那一点内存空间完全是可接受的,对于用户也是无感知的,所以空间换时间是常见的一种优化手段。
在整个 Vue.js 源码内部,经常可以见到这种空间换时间的操作,接下来我们就来看几个 Vue.js 中常见的空间换时间的操作。
Vue.js 中常见的空间换时间操作
reactive API
Vue.js 3.0 使用 Proxy API 把对象变成响应式,一旦某个对象经过 reactive
API 变成响应式对象后,会把响应式结果存储起来,大致如下:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// ...
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// ...
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
在整个响应式模块的内部,使用了 WeakMap
的数据结构存储响应式结果,它的 key
是原始的 Target
对象,值是 Proxy
对象。
export const reactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()
这样一来,同样的对象如果再次执行 reactive
,则从缓存的 proxyMap
中直接拿到对应的响应式值并返回。
KeepAlive 组件
整个 KeepAlive
组件的设计,本质上就是空间换时间。在 KeepAlive
组件内部,在组件渲染挂载和更新前都会缓存组件的渲染子树 subTree
,如下:
const cacheSubtree = () => {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
这个子树一旦被缓存了,在下一次渲染的时候就可以直接从缓存中拿到子树 vnode
以及对应的 DOM 元素来渲染。
KeepAlive
具体实现细节我在课程中有专门的一小节课说明,这里就不多赘述了。
工具函数 cacheStringFunction
Vue.js 源码内部的一些工具函数的实现,也利用了空间换时间的思想,比如 cacheStringFunction
函数,如下:
const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
const cache: Record<string, string> = Object.create(null)
return ((str: string) => {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}) as any
}
cacheStringFunction
函数的实现很简单,内部定义了 cache
变量做缓存,并返回了一个新的函数。
在新函数的内部,先尝试中从缓存中拿数据,如果不存在则执行函数 fn
,并把 fn
的返回结果用 cache
缓存,这样下一次就可以命中缓存了。
我们来看看 cacheStringFunction
的几个应用场景:
const camelizeRE = /-(\w)/g
export const camelize = cacheStringFunction(
(str: string): string => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
}
)
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cacheStringFunction((str: string) =>
str.replace(hyphenateRE, '-$1').toLowerCase()
)
export const capitalize = cacheStringFunction(
(str: string) => str.charAt(0).toUpperCase() + str.slice(1)
)
可以看到,这些字符串变形的相关函数都使用了 cacheStringFunction
,这样就保证了同样的字符串,在调用某个字符串变形函数后会把结果缓存,然后同一字符串再次执行该函数的时候就能从缓存拿结果了。
注意,Vue.js 内部之所以给这些字符串变形函数设计缓存,是因为它们的缓存命中率高,如果缓存命中率低的话,这类空间换时间的缓存设计就可能变成负优化了。
会造成内存泄漏吗
看到这里,你可能会有疑惑,空间换时间的基本操作都是通过缓存的方式,那这会造成内存泄漏吗?
回答这个问题前,你先要明白什么是内存泄漏:
内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
简单点说,内存泄漏就是那些你已经用不到的内存空间,由于没有释放而产生的内存浪费。
而我们空间换时间所设计的缓存,都是需要用到的内存空间,所以算是内存占用,并非内存泄漏。
关于使用 Vue.js 开发工作中可能会造成内存泄漏的场景,我在前几篇文章中提到了,如果你还不了解,建议你去看一看。
总结
综上,我们了解到 Vue.js 在编译过程中使用静态提升并非无成本,但是总体来看收益大于成本。此外,我们也了解 Vue.js 中一些空间换时间的操作,我希望你能学会这个优化思想并把它运用到自己平时的工作中。
我出这个题主要是希望你能做到以下两点:
学习 Vue.js 编译过程中的一些优化操作,并能思考它为什么能起到优化效果。
了解优化背后可能会造成的成本,学会评估成本和收益。
要记住,分析和思考的过程远比答案重要。
最后
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
欢迎加我微信「 sherlocked_93 」拉你进技术群,长期交流学习...
关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。