فهرست منبع

fix(ssr/reactivity): fix composition api behavior in SSR

fix #12615
Evan You 3 سال پیش
والد
کامیت
360272bde3
4فایلهای تغییر یافته به همراه265 افزوده شده و 59 حذف شده
  1. 192 0
      packages/server-renderer/test/ssr-reactivity.spec.ts
  2. 54 55
      src/core/observer/index.ts
  3. 13 2
      src/v3/reactivity/reactive.ts
  4. 6 2
      src/v3/reactivity/ref.ts

+ 192 - 0
packages/server-renderer/test/ssr-reactivity.spec.ts

@@ -0,0 +1,192 @@
+// @vitest-environment node
+
+import Vue from 'vue'
+import {
+  reactive,
+  ref,
+  isReactive,
+  shallowRef,
+  isRef,
+  set,
+  nextTick,
+  getCurrentInstance
+} from 'v3'
+import { createRenderer } from '../src'
+
+describe('SSR Reactive', () => {
+  beforeEach(() => {
+    // force SSR env
+    global.process.env.VUE_ENV = 'server'
+  })
+
+  it('should not affect non reactive APIs', () => {
+    expect(typeof window).toBe('undefined')
+    expect((Vue.observable({}) as any).__ob__).toBeUndefined()
+  })
+
+  it('reactive behavior should be consistent in SSR', () => {
+    const obj = reactive({
+      foo: ref(1),
+      bar: {
+        baz: ref(2)
+      },
+      arr: [{ foo: ref(3) }]
+    })
+    expect(isReactive(obj)).toBe(true)
+    expect(obj.foo).toBe(1)
+
+    expect(isReactive(obj.bar)).toBe(true)
+    expect(obj.bar.baz).toBe(2)
+
+    expect(isReactive(obj.arr)).toBe(true)
+    expect(isReactive(obj.arr[0])).toBe(true)
+    expect(obj.arr[0].foo).toBe(3)
+  })
+
+  it('ref value', () => {
+    const r = ref({})
+    expect(isReactive(r.value)).toBe(true)
+  })
+
+  it('should render', async () => {
+    const app = new Vue({
+      setup() {
+        return {
+          count: ref(42)
+        }
+      },
+      render(this: any, h) {
+        return h('div', this.count)
+      }
+    })
+
+    const serverRenderer = createRenderer()
+    const html = await serverRenderer.renderToString(app)
+    expect(html).toBe('<div data-server-rendered="true">42</div>')
+  })
+
+  it('reactive + isReactive', () => {
+    const state = reactive({})
+    expect(isReactive(state)).toBe(true)
+  })
+
+  it('shallowRef + isRef', () => {
+    const state = shallowRef({})
+    expect(isRef(state)).toBe(true)
+  })
+
+  it('should work on objects sets with set()', () => {
+    const state = ref<any>({})
+
+    set(state.value, 'a', {})
+    expect(isReactive(state.value.a)).toBe(true)
+
+    set(state.value, 'a', {})
+    expect(isReactive(state.value.a)).toBe(true)
+  })
+
+  it('should work on arrays sets with set()', () => {
+    const state = ref<any>([])
+
+    set(state.value, 1, {})
+    expect(isReactive(state.value[1])).toBe(true)
+
+    set(state.value, 1, {})
+    expect(isReactive(state.value[1])).toBe(true)
+  })
+
+  // #550
+  it('props should work with set', async done => {
+    let props: any
+
+    const app = new Vue({
+      render(this: any, h) {
+        return h('child', { attrs: { msg: this.msg } })
+      },
+      setup() {
+        return { msg: ref('hello') }
+      },
+      components: {
+        child: {
+          render(this: any, h: any) {
+            return h('span', this.data.msg)
+          },
+          props: ['msg'],
+          setup(_props) {
+            props = _props
+
+            return { data: _props }
+          }
+        }
+      }
+    })
+
+    const serverRenderer = createRenderer()
+    const html = await serverRenderer.renderToString(app)
+
+    expect(html).toBe('<span data-server-rendered="true">hello</span>')
+
+    expect(props.bar).toBeUndefined()
+    set(props, 'bar', 'bar')
+    expect(props.bar).toBe('bar')
+
+    done()
+  })
+
+  // #721
+  it('should behave correctly', () => {
+    const state = ref({ old: ref(false) })
+    set(state.value, 'new', ref(true))
+    // console.log(process.server, 'state.value', JSON.stringify(state.value))
+
+    expect(state.value).toMatchObject({
+      old: false,
+      new: true
+    })
+  })
+
+  // #721
+  it('should behave correctly for the nested ref in the object', () => {
+    const state = { old: ref(false) }
+    set(state, 'new', ref(true))
+    expect(JSON.stringify(state)).toBe(
+      '{"old":{"value":false},"new":{"value":true}}'
+    )
+  })
+
+  // #721
+  it('should behave correctly for ref of object', () => {
+    const state = ref({ old: ref(false) })
+    set(state.value, 'new', ref(true))
+    expect(JSON.stringify(state.value)).toBe('{"old":false,"new":true}')
+  })
+
+  it('ssr should not RangeError: Maximum call stack size exceeded', async () => {
+    new Vue({
+      setup() {
+        // @ts-expect-error
+        const app = getCurrentInstance().proxy
+        let mockNt: any = []
+        mockNt.__ob__ = {}
+        const test = reactive({
+          app,
+          mockNt
+        })
+        return {
+          test
+        }
+      }
+    })
+    await nextTick()
+    expect(
+      `"RangeError: Maximum call stack size exceeded"`
+    ).not.toHaveBeenWarned()
+  })
+
+  it('should work on objects sets with set()', () => {
+    const state = ref<any>({})
+    set(state.value, 'a', {})
+
+    expect(isReactive(state.value.a)).toBe(true)
+  })
+})

+ 54 - 55
src/core/observer/index.ts

@@ -13,7 +13,8 @@ import {
   isUndef,
   isValidArrayIndex,
   isServerRendering,
-  hasChanged
+  hasChanged,
+  noop
 } from '../util/index'
 import { isReadonly, isRef, TrackOpTypes, TriggerOpTypes } from '../../v3'
 
@@ -31,6 +32,14 @@ export function toggleObserving(value: boolean) {
   shouldObserve = value
 }
 
+// ssr mock dep
+const mockDep = {
+  notify: noop,
+  depend: noop,
+  addSub: noop,
+  removeSub: noop
+} as Dep
+
 /**
  * Observer class that is attached to each observed
  * object. Once attached, the observer converts the target
@@ -41,78 +50,63 @@ export class Observer {
   dep: Dep
   vmCount: number // number of vms that have this object as root $data
 
-  constructor(public value: any, public shallow = false) {
+  constructor(public value: any, public shallow = false, public mock = false) {
     // this.value = value
-    this.dep = new Dep()
+    this.dep = mock ? mockDep : new Dep()
     this.vmCount = 0
     def(value, '__ob__', this)
     if (isArray(value)) {
-      if (hasProto) {
-        protoAugment(value, arrayMethods)
-      } else {
-        copyAugment(value, arrayMethods, arrayKeys)
+      if (!mock) {
+        if (hasProto) {
+          /* eslint-disable no-proto */
+          ;(value as any).__proto__ = arrayMethods
+          /* eslint-enable no-proto */
+        } else {
+          for (let i = 0, l = arrayKeys.length; i < l; i++) {
+            const key = arrayKeys[i]
+            def(value, key, arrayMethods[key])
+          }
+        }
       }
       if (!shallow) {
         this.observeArray(value)
       }
     } else {
-      this.walk(value, shallow)
-    }
-  }
-
-  /**
-   * Walk through all properties and convert them into
-   * getter/setters. This method should only be called when
-   * value type is Object.
-   */
-  walk(obj: object, shallow: boolean) {
-    const keys = Object.keys(obj)
-    for (let i = 0; i < keys.length; i++) {
-      const key = keys[i]
-      defineReactive(obj, key, NO_INIITIAL_VALUE, undefined, shallow)
+      /**
+       * Walk through all properties and convert them into
+       * getter/setters. This method should only be called when
+       * value type is Object.
+       */
+      const keys = Object.keys(value)
+      for (let i = 0; i < keys.length; i++) {
+        const key = keys[i]
+        defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock)
+      }
     }
   }
 
   /**
    * Observe a list of Array items.
    */
-  observeArray(items: Array<any>) {
-    for (let i = 0, l = items.length; i < l; i++) {
-      observe(items[i])
+  observeArray(value: any[]) {
+    for (let i = 0, l = value.length; i < l; i++) {
+      observe(value[i], false, this.mock)
     }
   }
 }
 
 // helpers
 
-/**
- * Augment a target Object or Array by intercepting
- * the prototype chain using __proto__
- */
-function protoAugment(target, src: Object) {
-  /* eslint-disable no-proto */
-  target.__proto__ = src
-  /* eslint-enable no-proto */
-}
-
-/**
- * Augment a target Object or Array by defining
- * hidden properties.
- */
-/* istanbul ignore next */
-function copyAugment(target: Object, src: Object, keys: Array<string>) {
-  for (let i = 0, l = keys.length; i < l; i++) {
-    const key = keys[i]
-    def(target, key, src[key])
-  }
-}
-
 /**
  * Attempt to create an observer instance for a value,
  * returns the new observer if successfully observed,
  * or the existing observer if the value already has one.
  */
-export function observe(value: any, shallow?: boolean): Observer | void {
+export function observe(
+  value: any,
+  shallow?: boolean,
+  ssrMockReactivity?: boolean
+): Observer | void {
   if (!isObject(value) || isRef(value) || value instanceof VNode) {
     return
   }
@@ -121,12 +115,12 @@ export function observe(value: any, shallow?: boolean): Observer | void {
     ob = value.__ob__
   } else if (
     shouldObserve &&
-    !isServerRendering() &&
+    (ssrMockReactivity || !isServerRendering()) &&
     (isArray(value) || isPlainObject(value)) &&
     Object.isExtensible(value) &&
-    !value.__v_skip
+    !value.__v_skip /* ReactiveFlags.SKIP */
   ) {
-    ob = new Observer(value, shallow)
+    ob = new Observer(value, shallow, ssrMockReactivity)
   }
   return ob
 }
@@ -139,7 +133,8 @@ export function defineReactive(
   key: string,
   val?: any,
   customSetter?: Function | null,
-  shallow?: boolean
+  shallow?: boolean,
+  mock?: boolean
 ) {
   const dep = new Dep()
 
@@ -158,7 +153,7 @@ export function defineReactive(
     val = obj[key]
   }
 
-  let childOb = !shallow && observe(val)
+  let childOb = !shallow && observe(val, false, mock)
   Object.defineProperty(obj, key, {
     enumerable: true,
     configurable: true,
@@ -202,7 +197,7 @@ export function defineReactive(
       } else {
         val = newVal
       }
-      childOb = !shallow && observe(newVal)
+      childOb = !shallow && observe(newVal, false, mock)
       if (__DEV__) {
         dep.notify({
           type: TriggerOpTypes.SET,
@@ -241,16 +236,20 @@ export function set(
     __DEV__ && warn(`Set operation on key "${key}" failed: target is readonly.`)
     return
   }
+  const ob = (target as any).__ob__
   if (isArray(target) && isValidArrayIndex(key)) {
     target.length = Math.max(target.length, key)
     target.splice(key, 1, val)
+    // when mocking for SSR, array methods are not hijacked
+    if (!ob.shallow && ob.mock) {
+      observe(val, false, true)
+    }
     return val
   }
   if (key in target && !(key in Object.prototype)) {
     target[key] = val
     return val
   }
-  const ob = (target as any).__ob__
   if ((target as any)._isVue || (ob && ob.vmCount)) {
     __DEV__ &&
       warn(
@@ -263,7 +262,7 @@ export function set(
     target[key] = val
     return val
   }
-  defineReactive(ob.value, key, val)
+  defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock)
   if (__DEV__) {
     ob.dep.notify({
       type: TriggerOpTypes.ADD,

+ 13 - 2
src/v3/reactivity/reactive.ts

@@ -1,5 +1,12 @@
 import { observe, Observer } from 'core/observer'
-import { def, isArray, isPrimitive, warn, toRawType } from 'core/util'
+import {
+  def,
+  isArray,
+  isPrimitive,
+  warn,
+  toRawType,
+  isServerRendering
+} from 'core/util'
 import type { Ref, UnwrapRefSimple, RawSymbol } from './ref'
 
 export const enum ReactiveFlags {
@@ -67,7 +74,11 @@ function makeReactive(target: any, shallow: boolean) {
         )
       }
     }
-    const ob = observe(target, shallow)
+    const ob = observe(
+      target,
+      shallow,
+      isServerRendering() /* ssr mock reactivity */
+    )
     if (__DEV__ && !ob) {
       if (target == null || isPrimitive(target)) {
         warn(`value cannot be made reactive: ${String(target)}`)

+ 6 - 2
src/v3/reactivity/ref.ts

@@ -6,7 +6,7 @@ import {
 } from './reactive'
 import type { IfAny } from 'types/utils'
 import Dep from 'core/observer/dep'
-import { warn, isArray, def } from 'core/util'
+import { warn, isArray, def, isServerRendering } from 'core/util'
 import { TrackOpTypes, TriggerOpTypes } from './operations'
 
 declare const RefSymbol: unique symbol
@@ -69,7 +69,11 @@ function createRef(rawValue: unknown, shallow: boolean) {
   const ref: any = {}
   def(ref, RefFlag, true)
   def(ref, ReactiveFlags.IS_SHALLOW, true)
-  ref.dep = defineReactive(ref, 'value', rawValue, null, shallow)
+  def(
+    ref,
+    'dep',
+    defineReactive(ref, 'value', rawValue, null, shallow, isServerRendering())
+  )
   return ref
 }