diff --git a/packages/server-renderer/test/ssr-reactivity.spec.ts b/packages/server-renderer/test/ssr-reactivity.spec.ts
new file mode 100644
index 00000000000..f9b145c7740
--- /dev/null
+++ b/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('
42
')
+ })
+
+ 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({})
+
+ 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([])
+
+ 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('hello')
+
+ 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({})
+ set(state.value, 'a', {})
+
+ expect(isReactive(state.value.a)).toBe(true)
+ })
+})
diff --git a/src/core/observer/index.ts b/src/core/observer/index.ts
index 6180b8e904f..004a65fe6b5 100644
--- a/src/core/observer/index.ts
+++ b/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) {
- 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) {
- 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,
diff --git a/src/v3/reactivity/reactive.ts b/src/v3/reactivity/reactive.ts
index d750dd2c713..0ff682243a2 100644
--- a/src/v3/reactivity/reactive.ts
+++ b/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)}`)
diff --git a/src/v3/reactivity/ref.ts b/src/v3/reactivity/ref.ts
index 3b8d2fc7d1e..f7026df05a1 100644
--- a/src/v3/reactivity/ref.ts
+++ b/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
}