]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(ssr): only cache computed getters during render phase
authorEvan You <yyx990803@gmail.com>
Fri, 21 Jan 2022 04:31:54 +0000 (12:31 +0800)
committerEvan You <yyx990803@gmail.com>
Fri, 21 Jan 2022 04:31:54 +0000 (12:31 +0800)
fix #5300

packages/reactivity/src/computed.ts
packages/reactivity/src/deferredComputed.ts
packages/reactivity/src/effect.ts
packages/runtime-core/src/componentOptions.ts
packages/server-renderer/__tests__/ssrComputed.spec.ts [new file with mode: 0644]
packages/server-renderer/src/render.ts

index c077d84b209a61f3cde110b03c641137d769cc3c..ab0034a4cf1d80d741a4fd64b1108b9b633b582c 100644 (file)
@@ -23,16 +23,18 @@ export interface WritableComputedOptions<T> {
   set: ComputedSetter<T>
 }
 
-class ComputedRefImpl<T> {
+export class ComputedRefImpl<T> {
   public dep?: Dep = undefined
 
   private _value!: T
-  private _dirty = true
   public readonly effect: ReactiveEffect<T>
 
   public readonly __v_isRef = true
   public readonly [ReactiveFlags.IS_READONLY]: boolean
 
+  public _dirty = true
+  public _cacheable: boolean
+
   constructor(
     getter: ComputedGetter<T>,
     private readonly _setter: ComputedSetter<T>,
@@ -45,7 +47,8 @@ class ComputedRefImpl<T> {
         triggerRefValue(this)
       }
     })
-    this.effect.active = !isSSR
+    this.effect.computed = this
+    this.effect.active = this._cacheable = !isSSR
     this[ReactiveFlags.IS_READONLY] = isReadonly
   }
 
@@ -53,7 +56,7 @@ class ComputedRefImpl<T> {
     // the computed ref may get wrapped by other proxies e.g. readonly() #3376
     const self = toRaw(this)
     trackRefValue(self)
-    if (self._dirty) {
+    if (self._dirty || !self._cacheable) {
       self._dirty = false
       self._value = self.effect.run()!
     }
index 1cd97c74edb5fe3d9194436350070631275e29ef..bf19fd42fdf7db4f2ce8a174212ed06286d47ff2 100644 (file)
@@ -58,14 +58,14 @@ class DeferredComputedRefImpl<T> {
         // value invalidation in case of sync access; normal effects are
         // deferred to be triggered in scheduler.
         for (const e of this.dep) {
-          if (e.computed) {
+          if (e.computed instanceof DeferredComputedRefImpl) {
             e.scheduler!(true /* computedTrigger */)
           }
         }
       }
       this._dirty = true
     })
-    this.effect.computed = true
+    this.effect.computed = this as any
   }
 
   private _get() {
index bb4ed09fa32443262acba171a2ea544064132c0e..3c21ab7150f058e9abcd822453903890d99d4c3c 100644 (file)
@@ -9,6 +9,7 @@ import {
   newTracked,
   wasTracked
 } from './dep'
+import { ComputedRefImpl } from './computed'
 
 // The main WeakMap that stores {target -> key -> dep} connections.
 // Conceptually, it's easier to think of a dependency as a Dep class
@@ -54,9 +55,16 @@ export class ReactiveEffect<T = any> {
   active = true
   deps: Dep[] = []
 
-  // can be attached after creation
-  computed?: boolean
+  /**
+   * Can be attached after creation
+   * @internal
+   */
+  computed?: ComputedRefImpl<T>
+  /**
+   * @internal
+   */
   allowRecurse?: boolean
+
   onStop?: () => void
   // dev only
   onTrack?: (event: DebuggerEvent) => void
index 139ef6d396744bb8e23e6e6eeae92dd06c797efb..e8fa676a25cdcd2d34fe78cecce19cc9b64109c8 100644 (file)
@@ -19,7 +19,8 @@ import {
   LooseRequired,
   UnionToIntersection
 } from '@vue/shared'
-import { computed, isRef, Ref } from '@vue/reactivity'
+import { isRef, Ref } from '@vue/reactivity'
+import { computed } from './apiComputed'
 import {
   watch,
   WatchOptions,
diff --git a/packages/server-renderer/__tests__/ssrComputed.spec.ts b/packages/server-renderer/__tests__/ssrComputed.spec.ts
new file mode 100644 (file)
index 0000000..698893f
--- /dev/null
@@ -0,0 +1,48 @@
+import { createSSRApp, defineComponent, h, computed, reactive } from 'vue'
+import { renderToString } from '../src/renderToString'
+
+// #5208 reported memory leak of keeping computed alive during SSR
+// so we made computed properties created during SSR non-reactive in
+// https://github.com/vuejs/core/commit/f4f0966b33863ac0fca6a20cf9e8ddfbb311ae87
+// However, the default caching leads to #5300 which is tested below.
+// In Vue 2, computed properties are simple getters during SSR - this can be
+// inefficient if an expensive computed is accessed multiple times during render,
+// but because of potential mutations, we cannot cache it until we enter the
+// render phase (where no mutations can happen anymore)
+test('computed reactivity during SSR', async () => {
+  const store = {
+    // initial state could be hydrated
+    state: reactive({ items: null }) as any,
+
+    // pretend to fetch some data from an api
+    async fetchData() {
+      this.state.items = ['hello', 'world']
+    }
+  }
+
+  const getterSpy = jest.fn()
+
+  const App = defineComponent(async () => {
+    const msg = computed(() => {
+      getterSpy()
+      return store.state.items?.join(' ')
+    })
+
+    // If msg value is falsy then we are either in ssr context or on the client
+    // and the initial state was not modified/hydrated.
+    // In both cases we need to fetch data.
+    if (!msg.value) await store.fetchData()
+
+    expect(msg.value).toBe('hello world')
+    return () => h('div', null, msg.value + msg.value + msg.value)
+  })
+
+  const app = createSSRApp(App)
+
+  const html = await renderToString(app)
+  expect(html).toMatch('hello world')
+
+  // should only be called twice since access should be cached
+  // during the render phase
+  expect(getterSpy).toHaveBeenCalledTimes(2)
+})
index 3b629be435c0aff3ee94ba2d0a3aa077ce74dc20..3a08163f927c979d91aecacd4ceb26f3c772adf5 100644 (file)
@@ -128,6 +128,12 @@ function renderComponentSubTree(
       comp.ssrRender = ssrCompile(comp.template, instance)
     }
 
+    // perf: enable caching of computed getters during render
+    // since there cannot be state mutations during render.
+    for (const e of instance.scope.effects) {
+      if (e.computed) e.computed._cacheable = true
+    }
+
     const ssrRender = instance.ssrRender || comp.ssrRender
     if (ssrRender) {
       // optimized