淺析 Vue.js 中那些空間換時間的操作(好文收藏?。?/h1>
本期的問題:在 Vue.js 模板的編譯過程中,我們已經知道靜態(tài)提升的好處:針對靜態(tài)節(jié)點不用每次在 render 階段都執(zhí)行一次 createVNode 創(chuàng)建 vnode 對象。但它有沒有成本呢?為什么?
在回答問題前,我們簡單回顧一下什么是靜態(tài)提升,假設我們有如下模板:
<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 */))
}
我們發(fā)現(xiàn)靜態(tài)節(jié)點 <p>static</p> 編譯生成 vnode 的過程被提取到 render 函數(shù)外面了,然后在 render 函數(shù)內部就可以直接拿到靜態(tài)節(jié)點的編譯結果 _hoisted_1。
之所以可以這么做,是因為靜態(tài)節(jié)點是不會改變的,所以它編譯生成的 vnode 也不會改變,而動態(tài)節(jié)點是變化的,必須在每次 render 的時候動態(tài)創(chuàng)建,它的 vnode生成過程就不能提取到外面。
顯然,這樣做的好處是由于 render 函數(shù)在每次組件重新渲染的時候都會執(zhí)行,而針對靜態(tài)節(jié)點,創(chuàng)建 vnode 的過程只執(zhí)行一次,相當于提升了 render 的性能。
但是這么做也是有成本的,創(chuàng)建的 hoisted_1 vnode 對象始終會在內存中占用,并不會在每次 render 函數(shù)執(zhí)行后釋放。
其實,這就是典型的空間換時間的做法,在絕大部分的場景,性能好意味著更好的用戶體驗,而犧牲那一點內存空間完全是可接受的,對于用戶也是無感知的,所以空間換時間是常見的一種優(yōu)化手段。
在整個 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 的數(shù)據結構存儲響應式結果,它的 key 是原始的 Target 對象,值是 Proxy 對象。
export const reactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()
這樣一來,同樣的對象如果再次執(zhí)行 reactive,則從緩存的 proxyMap 中直接拿到對應的響應式值并返回。
KeepAlive 組件
整個 KeepAlive 組件的設計,本質上就是空間換時間。在 KeepAlive 組件內部,在組件渲染掛載和更新前都會緩存組件的渲染子樹 subTree,如下:
const cacheSubtree = () => {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
這個子樹一旦被緩存了,在下一次渲染的時候就可以直接從緩存中拿到子樹 vnode 以及對應的 DOM 元素來渲染。
KeepAlive 具體實現(xiàn)細節(jié)我在課程中有專門的一小節(jié)課說明,這里就不多贅述了。
工具函數(shù) cacheStringFunction
Vue.js 源碼內部的一些工具函數(shù)的實現(xiàn),也利用了空間換時間的思想,比如 cacheStringFunction 函數(shù),如下:
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 函數(shù)的實現(xiàn)很簡單,內部定義了 cache 變量做緩存,并返回了一個新的函數(shù)。
在新函數(shù)的內部,先嘗試中從緩存中拿數(shù)據,如果不存在則執(zhí)行函數(shù) 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)
)
可以看到,這些字符串變形的相關函數(shù)都使用了 cacheStringFunction,這樣就保證了同樣的字符串,在調用某個字符串變形函數(shù)后會把結果緩存,然后同一字符串再次執(zhí)行該函數(shù)的時候就能從緩存拿結果了。
注意,Vue.js 內部之所以給這些字符串變形函數(shù)設計緩存,是因為它們的緩存命中率高,如果緩存命中率低的話,這類空間換時間的緩存設計就可能變成負優(yōu)化了。
會造成內存泄漏嗎
看到這里,你可能會有疑惑,空間換時間的基本操作都是通過緩存的方式,那這會造成內存泄漏嗎?
回答這個問題前,你先要明白什么是內存泄漏:
內存泄漏(Memory leak)是在計算機科學中,由于疏忽或錯誤造成程序未能釋放已經不再使用的內存。內存泄漏并非指內存在物理上的消失,而是應用程序分配某段內存后,由于設計錯誤,導致在釋放該段內存之前就失去了對該段內存的控制,從而造成了內存的浪費。
簡單點說,內存泄漏就是那些你已經用不到的內存空間,由于沒有釋放而產生的內存浪費。
而我們空間換時間所設計的緩存,都是需要用到的內存空間,所以算是內存占用,并非內存泄漏。
關于使用 Vue.js 開發(fā)工作中可能會造成內存泄漏的場景,我在前幾篇文章中提到了,如果你還不了解,建議你去看一看。
總結
綜上,我們了解到 Vue.js 在編譯過程中使用靜態(tài)提升并非無成本,但是總體來看收益大于成本。此外,我們也了解 Vue.js 中一些空間換時間的操作,我希望你能學會這個優(yōu)化思想并把它運用到自己平時的工作中。
我出這個題主要是希望你能做到以下兩點:
學習 Vue.js 編譯過程中的一些優(yōu)化操作,并能思考它為什么能起到優(yōu)化效果。
了解優(yōu)化背后可能會造成的成本,學會評估成本和收益。
要記住,分析和思考的過程遠比答案重要。
最后
如果你覺得這篇內容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「 sherlocked_93 」拉你進技術群,長期交流學習...
關注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。

點個在看支持我吧,轉發(fā)就更好了
瀏覽
22
本期的問題:在 Vue.js 模板的編譯過程中,我們已經知道靜態(tài)提升的好處:針對靜態(tài)節(jié)點不用每次在 render 階段都執(zhí)行一次 createVNode 創(chuàng)建 vnode 對象。但它有沒有成本呢?為什么?
在回答問題前,我們簡單回顧一下什么是靜態(tài)提升,假設我們有如下模板:
<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 */))
}
我們發(fā)現(xiàn)靜態(tài)節(jié)點 <p>static</p> 編譯生成 vnode 的過程被提取到 render 函數(shù)外面了,然后在 render 函數(shù)內部就可以直接拿到靜態(tài)節(jié)點的編譯結果 _hoisted_1。
之所以可以這么做,是因為靜態(tài)節(jié)點是不會改變的,所以它編譯生成的 vnode 也不會改變,而動態(tài)節(jié)點是變化的,必須在每次 render 的時候動態(tài)創(chuàng)建,它的 vnode生成過程就不能提取到外面。
顯然,這樣做的好處是由于 render 函數(shù)在每次組件重新渲染的時候都會執(zhí)行,而針對靜態(tài)節(jié)點,創(chuàng)建 vnode 的過程只執(zhí)行一次,相當于提升了 render 的性能。
但是這么做也是有成本的,創(chuàng)建的 hoisted_1 vnode 對象始終會在內存中占用,并不會在每次 render 函數(shù)執(zhí)行后釋放。
其實,這就是典型的空間換時間的做法,在絕大部分的場景,性能好意味著更好的用戶體驗,而犧牲那一點內存空間完全是可接受的,對于用戶也是無感知的,所以空間換時間是常見的一種優(yōu)化手段。
在整個 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 的數(shù)據結構存儲響應式結果,它的 key 是原始的 Target 對象,值是 Proxy 對象。
export const reactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()
這樣一來,同樣的對象如果再次執(zhí)行 reactive,則從緩存的 proxyMap 中直接拿到對應的響應式值并返回。
KeepAlive 組件
整個 KeepAlive 組件的設計,本質上就是空間換時間。在 KeepAlive 組件內部,在組件渲染掛載和更新前都會緩存組件的渲染子樹 subTree,如下:
const cacheSubtree = () => {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
這個子樹一旦被緩存了,在下一次渲染的時候就可以直接從緩存中拿到子樹 vnode 以及對應的 DOM 元素來渲染。
KeepAlive 具體實現(xiàn)細節(jié)我在課程中有專門的一小節(jié)課說明,這里就不多贅述了。
工具函數(shù) cacheStringFunction
Vue.js 源碼內部的一些工具函數(shù)的實現(xiàn),也利用了空間換時間的思想,比如 cacheStringFunction 函數(shù),如下:
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 函數(shù)的實現(xiàn)很簡單,內部定義了 cache 變量做緩存,并返回了一個新的函數(shù)。
在新函數(shù)的內部,先嘗試中從緩存中拿數(shù)據,如果不存在則執(zhí)行函數(shù) 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)
)
可以看到,這些字符串變形的相關函數(shù)都使用了 cacheStringFunction,這樣就保證了同樣的字符串,在調用某個字符串變形函數(shù)后會把結果緩存,然后同一字符串再次執(zhí)行該函數(shù)的時候就能從緩存拿結果了。
注意,Vue.js 內部之所以給這些字符串變形函數(shù)設計緩存,是因為它們的緩存命中率高,如果緩存命中率低的話,這類空間換時間的緩存設計就可能變成負優(yōu)化了。
會造成內存泄漏嗎
看到這里,你可能會有疑惑,空間換時間的基本操作都是通過緩存的方式,那這會造成內存泄漏嗎?
回答這個問題前,你先要明白什么是內存泄漏:
內存泄漏(Memory leak)是在計算機科學中,由于疏忽或錯誤造成程序未能釋放已經不再使用的內存。內存泄漏并非指內存在物理上的消失,而是應用程序分配某段內存后,由于設計錯誤,導致在釋放該段內存之前就失去了對該段內存的控制,從而造成了內存的浪費。
簡單點說,內存泄漏就是那些你已經用不到的內存空間,由于沒有釋放而產生的內存浪費。
而我們空間換時間所設計的緩存,都是需要用到的內存空間,所以算是內存占用,并非內存泄漏。
關于使用 Vue.js 開發(fā)工作中可能會造成內存泄漏的場景,我在前幾篇文章中提到了,如果你還不了解,建議你去看一看。
總結
綜上,我們了解到 Vue.js 在編譯過程中使用靜態(tài)提升并非無成本,但是總體來看收益大于成本。此外,我們也了解 Vue.js 中一些空間換時間的操作,我希望你能學會這個優(yōu)化思想并把它運用到自己平時的工作中。
我出這個題主要是希望你能做到以下兩點:
學習 Vue.js 編譯過程中的一些優(yōu)化操作,并能思考它為什么能起到優(yōu)化效果。
了解優(yōu)化背后可能會造成的成本,學會評估成本和收益。
要記住,分析和思考的過程遠比答案重要。
最后
如果你覺得這篇內容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「 sherlocked_93 」拉你進技術群,長期交流學習...
關注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。

