mirror of
https://gitee.com/vuejs/vue.git
synced 2024-11-30 02:57:43 +08:00
wip: setup() tests
This commit is contained in:
parent
cfc2c59132
commit
6c1d2efd04
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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>`)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user