wip: setup() tests

This commit is contained in:
Evan You 2022-05-28 20:33:12 +08:00
parent cfc2c59132
commit 6c1d2efd04
7 changed files with 358 additions and 75 deletions

View File

@ -18,7 +18,7 @@ import {
invokeWithErrorHandling
} from '../util/index'
import { currentInstance, setCurrentInstance } from 'v3/currentInstance'
import { syncObject } from 'v3/apiSetup'
import { syncSetupAttrs } from 'v3/apiSetup'
export let activeInstance: any = null
export let isUpdatingChildComponent: boolean = false
@ -253,12 +253,13 @@ export function updateChildComponent(
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
let needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
const prevVNode = vm.$vnode
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
@ -271,10 +272,22 @@ export function updateChildComponent(
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = parentVnode.data.attrs || emptyObject
const attrs = parentVnode.data.attrs || emptyObject
if (vm._attrsProxy) {
syncObject(vm._attrsProxy, vm.$attrs)
// force update if attrs are accessed and has changed since it may be
// passed to a child component.
if (
syncSetupAttrs(
vm._attrsProxy,
attrs,
(prevVNode.data && prevVNode.data.attrs) || emptyObject,
vm
)
) {
needsForceUpdate = true
}
}
vm.$attrs = attrs
vm.$listeners = listeners || emptyObject

View File

@ -16,7 +16,7 @@ import VNode, { createEmptyVNode } from '../vdom/vnode'
import { isUpdatingChildComponent } from './lifecycle'
import type { Component } from 'typescript/component'
import { setCurrentInstance } from 'v3/currentInstance'
import { syncObject } from 'v3/apiSetup'
import { syncSetupSlots } from 'v3/apiSetup'
export function initRender(vm: Component) {
vm._vnode = null // the root of the child tree
@ -97,73 +97,70 @@ export function renderMixin(Vue: Component) {
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
setCurrentInstance(vm)
try {
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data!.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
if (vm._slotsProxy) {
syncObject(vm._slotsProxy, vm.$scopedSlots)
}
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data!.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
if (vm._slotsProxy) {
syncSetupSlots(vm._slotsProxy, vm.$scopedSlots)
}
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode!
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e: any) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (__DEV__ && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
)
} catch (e: any) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode!
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
setCurrentInstance(vm)
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e: any) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (__DEV__ && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
)
} catch (e: any) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
} else {
vnode = vm._vnode
}
// if the returned array contains only a single node, allow it
if (isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (__DEV__ && isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
} finally {
currentRenderingInstance = null
setCurrentInstance()
}
// if the returned array contains only a single node, allow it
if (isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (__DEV__ && isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
}

View File

@ -11,7 +11,8 @@ import {
isTrue,
isObject,
isPrimitive,
resolveAsset
resolveAsset,
isFunction
} from '../util/index'
import { normalizeChildren, simpleNormalizeChildren } from './helpers/index'
@ -76,7 +77,7 @@ export function _createElement(
)
}
// support single function children as default scoped slot
if (isArray(children) && typeof children[0] === 'function') {
if (isArray(children) && isFunction(children[0])) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0

View File

@ -1,6 +1,6 @@
import { isIE, isIE9, isEdge } from 'core/util/env'
import { extend, isDef, isUndef } from 'shared/util'
import { extend, isDef, isUndef, isTrue } from 'shared/util'
import type { VNodeWithData } from 'typescript/vnode'
import {
@ -26,7 +26,7 @@ function updateAttrs(oldVnode: VNodeWithData, vnode: VNodeWithData) {
const oldAttrs = oldVnode.data.attrs || {}
let attrs: any = vnode.data.attrs || {}
// clone observed objects, as the user probably wants to mutate it
if (isDef(attrs.__ob__)) {
if (isDef(attrs.__ob__) || isTrue(attrs._v_attr_proxy)) {
attrs = vnode.data.attrs = extend({}, attrs)
}

View File

@ -1,4 +1,4 @@
import { isDef, isUndef, extend, toNumber } from 'shared/util'
import { isDef, isUndef, extend, toNumber, isTrue } from 'shared/util'
import type { VNodeWithData } from 'typescript/vnode'
import { isSVG } from 'web/util/index'
@ -13,7 +13,7 @@ function updateDOMProps(oldVnode: VNodeWithData, vnode: VNodeWithData) {
const oldProps = oldVnode.data.domProps || {}
let props = vnode.data.domProps || {}
// clone observed objects, as the user probably wants to mutate it
if (isDef(props.__ob__)) {
if (isDef(props.__ob__) || isTrue(props._v_attr_proxy)) {
props = vnode.data.domProps = extend({}, props)
}

View File

@ -1,8 +1,8 @@
import { Component } from 'typescript/component'
import type { SetupContext } from 'typescript/options'
import { invokeWithErrorHandling, isReserved, warn } from '../core/util'
import { def, invokeWithErrorHandling, isReserved, warn } from '../core/util'
import VNode from '../core/vdom/vnode'
import { bind, isFunction, isObject } from '../shared/util'
import { bind, emptyObject, isFunction, isObject } from '../shared/util'
import { currentInstance, setCurrentInstance } from './currentInstance'
import { isRef } from './reactivity/ref'
@ -75,19 +75,55 @@ function proxySetupProperty(
function initAttrsProxy(vm: Component) {
if (!vm._attrsProxy) {
syncObject((vm._attrsProxy = {}), vm.$attrs)
const proxy = (vm._attrsProxy = {})
def(proxy, '_v_attr_proxy', true)
syncSetupAttrs(proxy, vm.$attrs, emptyObject, vm)
}
return vm._attrsProxy
}
export function syncSetupAttrs(
to: any,
from: any,
prev: any,
instance: Component
) {
let changed = false
for (const key in from) {
if (!(key in to)) {
changed = true
defineProxyAttr(to, key, instance)
} else if (from[key] !== prev[key]) {
changed = true
}
}
for (const key in to) {
if (!(key in from)) {
changed = true
delete to[key]
}
}
return changed
}
function defineProxyAttr(proxy: any, key: string, instance: Component) {
Object.defineProperty(proxy, key, {
enumerable: true,
configurable: true,
get() {
return instance.$attrs[key]
}
})
}
function initSlotsProxy(vm: Component) {
if (!vm._slotsProxy) {
syncObject((vm._slotsProxy = {}), vm.$scopedSlots)
syncSetupSlots((vm._slotsProxy = {}), vm.$scopedSlots)
}
return vm._slotsProxy
}
export function syncObject(to: any, from: any) {
export function syncSetupSlots(to: any, from: any) {
for (const key in from) {
to[key] = from[key]
}

View File

@ -0,0 +1,236 @@
import { h, ref, reactive } from 'v3'
import { nextTick } from 'core/util'
import { effect } from 'v3/reactivity/effect'
import Vue from 'vue'
function renderToString(comp: any) {
const vm = new Vue(comp).$mount()
return vm.$el.outerHTML
}
describe('api: setup context', () => {
it('should expose return values to template render context', () => {
const Comp = {
setup() {
return {
// ref should auto-unwrap
ref: ref('foo'),
// object exposed as-is
object: reactive({ msg: 'bar' }),
// primitive value exposed as-is
value: 'baz'
}
},
render() {
return h('div', `${this.ref} ${this.object.msg} ${this.value}`)
}
}
expect(renderToString(Comp)).toMatch(`<div>foo bar baz</div>`)
})
it('should support returning render function', () => {
const Comp = {
setup() {
return () => {
return h('div', 'hello')
}
}
}
expect(renderToString(Comp)).toMatch(`<div>hello</div>`)
})
it('props', async () => {
const count = ref(0)
let dummy
const Parent = {
render: () => h(Child, { props: { count: count.value } })
}
const Child = {
props: { count: Number },
setup(props) {
effect(() => {
dummy = props.count
})
return () => h('div', props.count)
}
}
const vm = new Vue(Parent).$mount()
expect(vm.$el.outerHTML).toMatch(`<div>0</div>`)
expect(dummy).toBe(0)
// props should be reactive
count.value++
await nextTick()
expect(vm.$el.outerHTML).toMatch(`<div>1</div>`)
expect(dummy).toBe(1)
})
it('context.attrs', async () => {
const toggle = ref(true)
const Parent = {
render: () =>
h(Child, { attrs: toggle.value ? { id: 'foo' } : { class: 'baz' } })
}
const Child = {
// explicit empty props declaration
// puts everything received in attrs
// disable implicit fallthrough
inheritAttrs: false,
setup(_props: any, { attrs }: any) {
return () => h('div', { attrs })
}
}
const vm = new Vue(Parent).$mount()
expect(vm.$el.outerHTML).toMatch(`<div id="foo"></div>`)
// should update even though it's not reactive
toggle.value = false
await nextTick()
expect(vm.$el.outerHTML).toMatch(`<div class="baz"></div>`)
})
// vuejs/core #4161
it('context.attrs in child component slots', async () => {
const toggle = ref(true)
const Wrapper = {
template: `<div><slot/></div>`
}
const Child = {
inheritAttrs: false,
setup(_: any, { attrs }: any) {
return () => {
return h(Wrapper, [h('div', { attrs })])
}
}
}
const Parent = {
render: () =>
h(Child, { attrs: toggle.value ? { id: 'foo' } : { class: 'baz' } })
}
const vm = new Vue(Parent).$mount()
expect(vm.$el.outerHTML).toMatch(`<div id="foo"></div>`)
// should update even though it's not reactive
toggle.value = false
await nextTick()
expect(vm.$el.outerHTML).toMatch(`<div class="baz"></div>`)
})
it('context.attrs in child component scoped slots', async () => {
const toggle = ref(true)
const Wrapper = {
template: `<div><slot/></div>`
}
const Child = {
inheritAttrs: false,
setup(_: any, { attrs }: any) {
return () => {
return h(Wrapper, {
scopedSlots: {
default: () => h('div', { attrs })
}
})
}
}
}
const Parent = {
render: () =>
h(Child, { attrs: toggle.value ? { id: 'foo' } : { class: 'baz' } })
}
const vm = new Vue(Parent).$mount()
expect(vm.$el.outerHTML).toMatch(`<div id="foo"></div>`)
// should update even though it's not reactive
toggle.value = false
await nextTick()
expect(vm.$el.outerHTML).toMatch(`<div class="baz"></div>`)
})
it('context.slots', async () => {
const id = ref('foo')
const Child = {
setup(props: any, { slots }: any) {
return () => h('div', [...slots.foo(), ...slots.bar()])
}
}
const Parent = {
components: { Child },
setup() {
return { id }
},
template: `<Child>
<template #foo>{{ id }}</template>
<template #bar>bar</template>
</Child>`
}
const vm = new Vue(Parent).$mount()
expect(vm.$el.outerHTML).toMatch(`<div>foobar</div>`)
// should update even though it's not reactive
id.value = 'baz'
await nextTick()
expect(vm.$el.outerHTML).toMatch(`<div>bazbar</div>`)
})
it('context.emit', async () => {
const count = ref(0)
const spy = vi.fn()
const Child = {
props: {
count: {
type: Number,
default: 1
}
},
setup(props, { emit }) {
return () =>
h(
'div',
{
on: { click: () => emit('inc', props.count + 1) }
},
props.count
)
}
}
const Parent = {
components: { Child },
setup: () => ({
count,
onInc(newVal: number) {
spy()
count.value = newVal
}
}),
template: `<Child :count="count" @inc="onInc" />`
}
const vm = new Vue(Parent).$mount()
expect(vm.$el.outerHTML).toMatch(`<div>0</div>`)
// emit should trigger parent handler
triggerEvent(vm.$el as HTMLElement, 'click')
expect(spy).toHaveBeenCalled()
await nextTick()
expect(vm.$el.outerHTML).toMatch(`<div>1</div>`)
})
})