feat(runtime-core): async component support
This commit is contained in:
		
							parent
							
								
									d425818901
								
							
						
					
					
						commit
						c3bb3169f4
					
				
							
								
								
									
										464
									
								
								packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										464
									
								
								packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,464 @@
 | 
			
		||||
import {
 | 
			
		||||
  createAsyncComponent,
 | 
			
		||||
  h,
 | 
			
		||||
  Component,
 | 
			
		||||
  ref,
 | 
			
		||||
  nextTick,
 | 
			
		||||
  Suspense
 | 
			
		||||
} from '../src'
 | 
			
		||||
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
 | 
			
		||||
 | 
			
		||||
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
 | 
			
		||||
 | 
			
		||||
describe('api: createAsyncComponent', () => {
 | 
			
		||||
  test('simple usage', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    const Foo = createAsyncComponent(
 | 
			
		||||
      () =>
 | 
			
		||||
        new Promise(r => {
 | 
			
		||||
          resolve = r as any
 | 
			
		||||
        })
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const toggle = ref(true)
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => (toggle.value ? h(Foo) : null)
 | 
			
		||||
    }).mount(root)
 | 
			
		||||
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    // first time resolve, wait for macro task since there are multiple
 | 
			
		||||
    // microtasks / .then() calls
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
 | 
			
		||||
    toggle.value = false
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // already resolved component should update on nextTick
 | 
			
		||||
    toggle.value = true
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('with loading component', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    const Foo = createAsyncComponent({
 | 
			
		||||
      loader: () =>
 | 
			
		||||
        new Promise(r => {
 | 
			
		||||
          resolve = r as any
 | 
			
		||||
        }),
 | 
			
		||||
      loading: () => 'loading',
 | 
			
		||||
      delay: 1 // defaults to 200
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const toggle = ref(true)
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => (toggle.value ? h(Foo) : null)
 | 
			
		||||
    }).mount(root)
 | 
			
		||||
 | 
			
		||||
    // due to the delay, initial mount should be empty
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // loading show up after delay
 | 
			
		||||
    await timeout(1)
 | 
			
		||||
    expect(serializeInner(root)).toBe('loading')
 | 
			
		||||
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
 | 
			
		||||
    toggle.value = false
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // already resolved component should update on nextTick without loading
 | 
			
		||||
    // state
 | 
			
		||||
    toggle.value = true
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('with loading component + explicit delay (0)', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    const Foo = createAsyncComponent({
 | 
			
		||||
      loader: () =>
 | 
			
		||||
        new Promise(r => {
 | 
			
		||||
          resolve = r as any
 | 
			
		||||
        }),
 | 
			
		||||
      loading: () => 'loading',
 | 
			
		||||
      delay: 0
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const toggle = ref(true)
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => (toggle.value ? h(Foo) : null)
 | 
			
		||||
    }).mount(root)
 | 
			
		||||
 | 
			
		||||
    // with delay: 0, should show loading immediately
 | 
			
		||||
    expect(serializeInner(root)).toBe('loading')
 | 
			
		||||
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
 | 
			
		||||
    toggle.value = false
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // already resolved component should update on nextTick without loading
 | 
			
		||||
    // state
 | 
			
		||||
    toggle.value = true
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('error without error component', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    let reject: (e: Error) => void
 | 
			
		||||
    const Foo = createAsyncComponent(
 | 
			
		||||
      () =>
 | 
			
		||||
        new Promise((_resolve, _reject) => {
 | 
			
		||||
          resolve = _resolve as any
 | 
			
		||||
          reject = _reject
 | 
			
		||||
        })
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const toggle = ref(true)
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => (toggle.value ? h(Foo) : null)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const handler = (app.config.errorHandler = jest.fn())
 | 
			
		||||
 | 
			
		||||
    app.mount(root)
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    const err = new Error('foo')
 | 
			
		||||
    reject!(err)
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(handler).toHaveBeenCalled()
 | 
			
		||||
    expect(handler.mock.calls[0][0]).toBe(err)
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    toggle.value = false
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // errored out on previous load, toggle and mock success this time
 | 
			
		||||
    toggle.value = true
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // should render this time
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('error with error component', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    let reject: (e: Error) => void
 | 
			
		||||
    const Foo = createAsyncComponent({
 | 
			
		||||
      loader: () =>
 | 
			
		||||
        new Promise((_resolve, _reject) => {
 | 
			
		||||
          resolve = _resolve as any
 | 
			
		||||
          reject = _reject
 | 
			
		||||
        }),
 | 
			
		||||
      error: (props: { error: Error }) => props.error.message
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const toggle = ref(true)
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => (toggle.value ? h(Foo) : null)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const handler = (app.config.errorHandler = jest.fn())
 | 
			
		||||
 | 
			
		||||
    app.mount(root)
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    const err = new Error('errored out')
 | 
			
		||||
    reject!(err)
 | 
			
		||||
    await timeout()
 | 
			
		||||
    // error handler will not be called if error component is present
 | 
			
		||||
    expect(handler).not.toHaveBeenCalled()
 | 
			
		||||
    expect(serializeInner(root)).toBe('errored out')
 | 
			
		||||
 | 
			
		||||
    toggle.value = false
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // errored out on previous load, toggle and mock success this time
 | 
			
		||||
    toggle.value = true
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // should render this time
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('error with error + loading components', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    let reject: (e: Error) => void
 | 
			
		||||
    const Foo = createAsyncComponent({
 | 
			
		||||
      loader: () =>
 | 
			
		||||
        new Promise((_resolve, _reject) => {
 | 
			
		||||
          resolve = _resolve as any
 | 
			
		||||
          reject = _reject
 | 
			
		||||
        }),
 | 
			
		||||
      error: (props: { error: Error }) => props.error.message,
 | 
			
		||||
      loading: () => 'loading',
 | 
			
		||||
      delay: 1
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const toggle = ref(true)
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => (toggle.value ? h(Foo) : null)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const handler = (app.config.errorHandler = jest.fn())
 | 
			
		||||
 | 
			
		||||
    app.mount(root)
 | 
			
		||||
 | 
			
		||||
    // due to the delay, initial mount should be empty
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // loading show up after delay
 | 
			
		||||
    await timeout(1)
 | 
			
		||||
    expect(serializeInner(root)).toBe('loading')
 | 
			
		||||
 | 
			
		||||
    const err = new Error('errored out')
 | 
			
		||||
    reject!(err)
 | 
			
		||||
    await timeout()
 | 
			
		||||
    // error handler will not be called if error component is present
 | 
			
		||||
    expect(handler).not.toHaveBeenCalled()
 | 
			
		||||
    expect(serializeInner(root)).toBe('errored out')
 | 
			
		||||
 | 
			
		||||
    toggle.value = false
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // errored out on previous load, toggle and mock success this time
 | 
			
		||||
    toggle.value = true
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // loading show up after delay
 | 
			
		||||
    await timeout(1)
 | 
			
		||||
    expect(serializeInner(root)).toBe('loading')
 | 
			
		||||
 | 
			
		||||
    // should render this time
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('timeout without error component', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    const Foo = createAsyncComponent({
 | 
			
		||||
      loader: () =>
 | 
			
		||||
        new Promise(_resolve => {
 | 
			
		||||
          resolve = _resolve as any
 | 
			
		||||
        }),
 | 
			
		||||
      timeout: 1
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => h(Foo)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const handler = (app.config.errorHandler = jest.fn())
 | 
			
		||||
 | 
			
		||||
    app.mount(root)
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    await timeout(1)
 | 
			
		||||
    expect(handler).toHaveBeenCalled()
 | 
			
		||||
    expect(handler.mock.calls[0][0].message).toMatch(
 | 
			
		||||
      `Async component timed out after 1ms.`
 | 
			
		||||
    )
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    // if it resolved after timeout, should still work
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('timeout with error component', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    const Foo = createAsyncComponent({
 | 
			
		||||
      loader: () =>
 | 
			
		||||
        new Promise(_resolve => {
 | 
			
		||||
          resolve = _resolve as any
 | 
			
		||||
        }),
 | 
			
		||||
      timeout: 1,
 | 
			
		||||
      error: () => 'timed out'
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => h(Foo)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const handler = (app.config.errorHandler = jest.fn())
 | 
			
		||||
 | 
			
		||||
    app.mount(root)
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
 | 
			
		||||
    await timeout(1)
 | 
			
		||||
    expect(handler).not.toHaveBeenCalled()
 | 
			
		||||
    expect(serializeInner(root)).toBe('timed out')
 | 
			
		||||
 | 
			
		||||
    // if it resolved after timeout, should still work
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('timeout with error + loading components', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    const Foo = createAsyncComponent({
 | 
			
		||||
      loader: () =>
 | 
			
		||||
        new Promise(_resolve => {
 | 
			
		||||
          resolve = _resolve as any
 | 
			
		||||
        }),
 | 
			
		||||
      delay: 1,
 | 
			
		||||
      timeout: 16,
 | 
			
		||||
      error: () => 'timed out',
 | 
			
		||||
      loading: () => 'loading'
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => h(Foo)
 | 
			
		||||
    })
 | 
			
		||||
    app.mount(root)
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
    await timeout(1)
 | 
			
		||||
    expect(serializeInner(root)).toBe('loading')
 | 
			
		||||
 | 
			
		||||
    await timeout(16)
 | 
			
		||||
    expect(serializeInner(root)).toBe('timed out')
 | 
			
		||||
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('timeout without error component, but with loading component', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    const Foo = createAsyncComponent({
 | 
			
		||||
      loader: () =>
 | 
			
		||||
        new Promise(_resolve => {
 | 
			
		||||
          resolve = _resolve as any
 | 
			
		||||
        }),
 | 
			
		||||
      delay: 1,
 | 
			
		||||
      timeout: 16,
 | 
			
		||||
      loading: () => 'loading'
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () => h(Foo)
 | 
			
		||||
    })
 | 
			
		||||
    const handler = (app.config.errorHandler = jest.fn())
 | 
			
		||||
    app.mount(root)
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!---->')
 | 
			
		||||
    await timeout(1)
 | 
			
		||||
    expect(serializeInner(root)).toBe('loading')
 | 
			
		||||
 | 
			
		||||
    await timeout(16)
 | 
			
		||||
    expect(handler).toHaveBeenCalled()
 | 
			
		||||
    expect(handler.mock.calls[0][0].message).toMatch(
 | 
			
		||||
      `Async component timed out after 16ms.`
 | 
			
		||||
    )
 | 
			
		||||
    // should still display loading
 | 
			
		||||
    expect(serializeInner(root)).toBe('loading')
 | 
			
		||||
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('with suspense', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    const Foo = createAsyncComponent(
 | 
			
		||||
      () =>
 | 
			
		||||
        new Promise(_resolve => {
 | 
			
		||||
          resolve = _resolve as any
 | 
			
		||||
        })
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () =>
 | 
			
		||||
        h(Suspense, null, {
 | 
			
		||||
          default: () => [h(Foo), ' & ', h(Foo)],
 | 
			
		||||
          fallback: () => 'loading'
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    app.mount(root)
 | 
			
		||||
    expect(serializeInner(root)).toBe('loading')
 | 
			
		||||
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved & resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('suspensible: false', async () => {
 | 
			
		||||
    let resolve: (comp: Component) => void
 | 
			
		||||
    const Foo = createAsyncComponent({
 | 
			
		||||
      loader: () =>
 | 
			
		||||
        new Promise(_resolve => {
 | 
			
		||||
          resolve = _resolve as any
 | 
			
		||||
        }),
 | 
			
		||||
      suspensible: false
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      components: { Foo },
 | 
			
		||||
      render: () =>
 | 
			
		||||
        h(Suspense, null, {
 | 
			
		||||
          default: () => [h(Foo), ' & ', h(Foo)],
 | 
			
		||||
          fallback: () => 'loading'
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    app.mount(root)
 | 
			
		||||
    // should not show suspense fallback
 | 
			
		||||
    expect(serializeInner(root)).toBe('<!----> & <!---->')
 | 
			
		||||
 | 
			
		||||
    resolve!(() => 'resolved')
 | 
			
		||||
    await timeout()
 | 
			
		||||
    expect(serializeInner(root)).toBe('resolved & resolved')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // TODO
 | 
			
		||||
  test.todo('suspense with error handling')
 | 
			
		||||
})
 | 
			
		||||
@ -379,6 +379,9 @@ describe('SSR hydration', () => {
 | 
			
		||||
    expect(container.innerHTML).toMatch(`<span>2</span><span>3</span>`)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // TODO
 | 
			
		||||
  test.todo('async component')
 | 
			
		||||
 | 
			
		||||
  describe('mismatch handling', () => {
 | 
			
		||||
    test('text node', () => {
 | 
			
		||||
      const { container } = mountWithHydration(`foo`, () => 'bar')
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										155
									
								
								packages/runtime-core/src/apiAsyncComponent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								packages/runtime-core/src/apiAsyncComponent.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,155 @@
 | 
			
		||||
import {
 | 
			
		||||
  PublicAPIComponent,
 | 
			
		||||
  Component,
 | 
			
		||||
  currentSuspense,
 | 
			
		||||
  currentInstance,
 | 
			
		||||
  ComponentInternalInstance
 | 
			
		||||
} from './component'
 | 
			
		||||
import { isFunction, isObject, EMPTY_OBJ } from '@vue/shared'
 | 
			
		||||
import { ComponentPublicInstance } from './componentProxy'
 | 
			
		||||
import { createVNode } from './vnode'
 | 
			
		||||
import { defineComponent } from './apiDefineComponent'
 | 
			
		||||
import { warn } from './warning'
 | 
			
		||||
import { ref } from '@vue/reactivity'
 | 
			
		||||
import { handleError, ErrorCodes } from './errorHandling'
 | 
			
		||||
 | 
			
		||||
export type AsyncComponentResolveResult<T = PublicAPIComponent> =
 | 
			
		||||
  | T
 | 
			
		||||
  | { default: T } // es modules
 | 
			
		||||
 | 
			
		||||
export type AsyncComponentLoader<T = any> = () => Promise<
 | 
			
		||||
  AsyncComponentResolveResult<T>
 | 
			
		||||
>
 | 
			
		||||
 | 
			
		||||
export interface AsyncComponentOptions<T = any> {
 | 
			
		||||
  loader: AsyncComponentLoader<T>
 | 
			
		||||
  loading?: PublicAPIComponent
 | 
			
		||||
  error?: PublicAPIComponent
 | 
			
		||||
  delay?: number
 | 
			
		||||
  timeout?: number
 | 
			
		||||
  suspensible?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createAsyncComponent<
 | 
			
		||||
  T extends PublicAPIComponent = { new (): ComponentPublicInstance }
 | 
			
		||||
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
 | 
			
		||||
  if (isFunction(source)) {
 | 
			
		||||
    source = { loader: source }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    suspensible = true,
 | 
			
		||||
    loader,
 | 
			
		||||
    loading: loadingComponent,
 | 
			
		||||
    error: errorComponent,
 | 
			
		||||
    delay = 200,
 | 
			
		||||
    timeout // undefined = never times out
 | 
			
		||||
  } = source
 | 
			
		||||
 | 
			
		||||
  let pendingRequest: Promise<Component> | null = null
 | 
			
		||||
  let resolvedComp: Component | undefined
 | 
			
		||||
 | 
			
		||||
  const load = (): Promise<Component> => {
 | 
			
		||||
    return (
 | 
			
		||||
      pendingRequest ||
 | 
			
		||||
      (pendingRequest = loader().then((comp: any) => {
 | 
			
		||||
        // interop module default
 | 
			
		||||
        if (comp.__esModule || comp[Symbol.toStringTag] === 'Module') {
 | 
			
		||||
          comp = comp.default
 | 
			
		||||
        }
 | 
			
		||||
        if (__DEV__ && !isObject(comp) && !isFunction(comp)) {
 | 
			
		||||
          warn(`Invalid async component load result: `, comp)
 | 
			
		||||
        }
 | 
			
		||||
        resolvedComp = comp
 | 
			
		||||
        return comp
 | 
			
		||||
      }))
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return defineComponent({
 | 
			
		||||
    name: 'AsyncComponentWrapper',
 | 
			
		||||
    setup() {
 | 
			
		||||
      const instance = currentInstance!
 | 
			
		||||
 | 
			
		||||
      // already resolved
 | 
			
		||||
      if (resolvedComp) {
 | 
			
		||||
        return () => createInnerComp(resolvedComp!, instance)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // suspense-controlled
 | 
			
		||||
      if (__FEATURE_SUSPENSE__ && suspensible && currentSuspense) {
 | 
			
		||||
        return load().then(comp => {
 | 
			
		||||
          return () => createInnerComp(comp, instance)
 | 
			
		||||
        })
 | 
			
		||||
        // TODO suspense error handling
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // self-controlled
 | 
			
		||||
      if (__NODE_JS__) {
 | 
			
		||||
        // TODO SSR
 | 
			
		||||
      }
 | 
			
		||||
      // TODO hydration
 | 
			
		||||
 | 
			
		||||
      const loaded = ref(false)
 | 
			
		||||
      const error = ref()
 | 
			
		||||
      const delayed = ref(!!delay)
 | 
			
		||||
 | 
			
		||||
      if (delay) {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          delayed.value = false
 | 
			
		||||
        }, delay)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (timeout != null) {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          if (!loaded.value) {
 | 
			
		||||
            const err = new Error(
 | 
			
		||||
              `Async component timed out after ${timeout}ms.`
 | 
			
		||||
            )
 | 
			
		||||
            if (errorComponent) {
 | 
			
		||||
              error.value = err
 | 
			
		||||
            } else {
 | 
			
		||||
              handleError(err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }, timeout)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      load()
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          loaded.value = true
 | 
			
		||||
        })
 | 
			
		||||
        .catch(err => {
 | 
			
		||||
          pendingRequest = null
 | 
			
		||||
          if (errorComponent) {
 | 
			
		||||
            error.value = err
 | 
			
		||||
          } else {
 | 
			
		||||
            handleError(err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
      return () => {
 | 
			
		||||
        if (loaded.value && resolvedComp) {
 | 
			
		||||
          return createInnerComp(resolvedComp, instance)
 | 
			
		||||
        } else if (error.value && errorComponent) {
 | 
			
		||||
          return createVNode(errorComponent as Component, {
 | 
			
		||||
            error: error.value
 | 
			
		||||
          })
 | 
			
		||||
        } else if (loadingComponent && !delayed.value) {
 | 
			
		||||
          return createVNode(loadingComponent as Component)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }) as any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createInnerComp(
 | 
			
		||||
  comp: Component,
 | 
			
		||||
  { props, slots }: ComponentInternalInstance
 | 
			
		||||
) {
 | 
			
		||||
  return createVNode(
 | 
			
		||||
    comp,
 | 
			
		||||
    props === EMPTY_OBJ ? null : props,
 | 
			
		||||
    slots === EMPTY_OBJ ? null : slots
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -19,6 +19,7 @@ export const enum ErrorCodes {
 | 
			
		||||
  APP_ERROR_HANDLER,
 | 
			
		||||
  APP_WARN_HANDLER,
 | 
			
		||||
  FUNCTION_REF,
 | 
			
		||||
  ASYNC_COMPONENT_LOADER,
 | 
			
		||||
  SCHEDULER
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -49,6 +50,7 @@ export const ErrorTypeStrings: Record<number | string, string> = {
 | 
			
		||||
  [ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
 | 
			
		||||
  [ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
 | 
			
		||||
  [ErrorCodes.FUNCTION_REF]: 'ref function',
 | 
			
		||||
  [ErrorCodes.ASYNC_COMPONENT_LOADER]: 'async component loader',
 | 
			
		||||
  [ErrorCodes.SCHEDULER]:
 | 
			
		||||
    'scheduler flush. This is likely a Vue internals bug. ' +
 | 
			
		||||
    'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-next'
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ export {
 | 
			
		||||
export { provide, inject } from './apiInject'
 | 
			
		||||
export { nextTick } from './scheduler'
 | 
			
		||||
export { defineComponent } from './apiDefineComponent'
 | 
			
		||||
export { createAsyncComponent } from './apiAsyncComponent'
 | 
			
		||||
 | 
			
		||||
// Advanced API ----------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
@ -204,4 +205,8 @@ export {
 | 
			
		||||
} from './directives'
 | 
			
		||||
export { SuspenseBoundary } from './components/Suspense'
 | 
			
		||||
export { TransitionState, TransitionHooks } from './components/BaseTransition'
 | 
			
		||||
export {
 | 
			
		||||
  AsyncComponentOptions,
 | 
			
		||||
  AsyncComponentLoader
 | 
			
		||||
} from './apiAsyncComponent'
 | 
			
		||||
export { HMRRuntime } from './hmr'
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user