浅析 Vue.js 中那些空间换时间的操作

Posted 老黄的前端私房菜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析 Vue.js 中那些空间换时间的操作相关的知识,希望对你有一定的参考价值。

Hello,各位小伙伴,接下来的一段时间里,我会把我的课程《Vue.js 3.0 核心源码解析》中问题的答案陆续在我的公众号发布,由于课程的问题大多数都是开放性的问题,所以我的答案也不一定是标准的,仅供你参考喔。

本期的问题:在 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<stringstring> = 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 中一些空间换时间的操作,我希望你能学会这个优化思想并把它运用到自己平时的工作中。

我出这个题主要是希望你能做到以下两点:

  1. 学习 Vue.js 编译过程中的一些优化操作,并能思考它为什么能起到优化效果。

  2. 了解优化背后可能会造成的成本,学会评估成本和收益。

要记住,分析和思考的过程远比答案重要。

以上是关于浅析 Vue.js 中那些空间换时间的操作的主要内容,如果未能解决你的问题,请参考以下文章

使用带有渲染功能的 Vue.js 3 片段

浅析vue.js原理

浅析 c++ bitset 的用法

vue.js 2 - 将单个文件组件的 html 片段提取到独立文件中

vue.js字符串换行问题

浅析android适配器adapter中的那些坑