mirror of
https://github.com/vuejs/vue.git
synced 2024-11-21 20:28:54 +00:00
parent
94ccca207c
commit
360272bde3
192
packages/server-renderer/test/ssr-reactivity.spec.ts
Normal file
192
packages/server-renderer/test/ssr-reactivity.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
@ -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,
|
||||
|
@ -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,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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user