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

fix #12615
This commit is contained in:
Evan You 2022-07-06 12:49:30 +08:00
parent 94ccca207c
commit 360272bde3
4 changed files with 265 additions and 59 deletions

View File

@ -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)
})
})

View File

@ -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,

View File

@ -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)}`)

View File

@ -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
}