preface

Vue3 is updated with two new built-in components, Teleport and Suspense. It makes it very convenient to implement certain effects, such as dummy boxes, asynchronous loading, etc., and I’m curious about how it works inside. After two days of research, I found the internal implementation very clever.

Source location:

Suspense vue-next/packages/runtime-core/src/components/Suspense.ts

Teleport vue-next/packages/runtime-core/src/components/Teleport.ts

Suspense

Suspense is defined in the official documentation as making asynchronous requests (which can also be asynchronous tasks, such as promises) before rendering components properly. Components typically handle this logic locally, which is perfectly fine in most cases. The suspense> component provides an alternative that allows the waiting process to be promoted into a component tree rather than in a single component. More detailed please check: v3.cn.vuejs.org/guide/migra…

Front is introduced

attribute

ActiveBranch: Activates branches. Vnodes that have been mounted to the page are unmounted during update and re-assigned as the newly mounted VNode

PendingBranch: Suspends the branch, mostly in #default. Because asynchronous tasks exist, uninstall activeBranch and mount the activeBranch after the asynchronous tasks are processed.

IsInFallback: Indicates whether it is in the #fallback phase

IsHydrating: Whether the server rendering is complete

Timeout: A parameter accepted in Suspense to specify the current timeout or whether to reload #fallback immediately or after timeout milliseconds

Hook function

Suspense has three unique hook functions

OnPending: execute before mounting pendingBranch content

OnResolve: Resolve to the end, execute

OnFallback: executed before mounting the contents of #fallback

Asynchronous components

Vue provides an API, defineAsyncComponent, to create an asynchronous component that is loaded only when needed.

DefineAsyncComponnet (() => import(‘./xxx.vue’))

  1. Advanced usage: Accept an object
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  // Factory function
  loader: () = > import('./Foo.vue'),
  // The component to use when loading asynchronous components
  loadingComponent: LoadingComponent,
  // The component to use when loading fails
  errorComponent: ErrorComponent,
  / / delay before displaying loadingComponent | default: 200 (ms)
  delay: 200.// If timeout is provided and the component takes longer to load than the set value, the error component will be displayed
  // Default: Infinity (that is, never timeout, in ms)
  timeout: 3000./ / defines whether the component can be hang | default value: true
  suspensible: false./ * * * *@param {*} Error Indicates the error information object *@param {*} Retry A function that indicates whether the promise loader should retry * when it loads a reject@param {*} Fail a function that tells the loader to quit@param {*} Maximum number of retries allowed */
  onError(error, retry, fail, attempts) {
    if (error.message.match(/fetch/) && attempts <= 3) {
      // Retry the request when an error occurs, up to three times
      retry()
    } else {
      // Note that retry/fail is like promise's resolve/reject:
      // One of these must be called to continue error handling.
      fail()
    }
  }
})
Copy the code

Use case

Wait for a Promise to return data.

The Children components

<template>
    {{data}}
</template>

<script>
import {ref} from 'vue'

export default {
    name: 'Children'.async setup() {
        const data = ref(null)
        const promise = new Promise((resolve, reject) = > {
            setTimeout(() = > {
                resolve('success')},2000)
        })
        data.value = await promise
        
        return {
            data
        }
    }
}
</script>
Copy the code

App component

<template>
    <div>
        <! Suspense > 'components have two slots. They both accept only one direct child node. Nodes in the 'default' slot are displayed as much as possible. If not, the nodes in the 'fallback' slot are displayed. -->
        <Suspense>
            <template #default>
                <! -- There may be asynchronous requests or asynchronous tasks -->
                <AsyncComp />
            </template>
            <template #fallback>
                <! -- The content here will be rendered during the #default content load and displayed -->
                <div>data Info Loading...</div>
            </template>
        </Suspense>
    </div>
</template>

<script>
import {defineAsyncComponent} from 'vue'
// If we import the component using defineAsyncComponent we don't need to import it like this,
// defineAsyncComponent can load components dynamically
/ / detailed look at: https://v3.cn.vuejs.org/api/global-api.html#defineasynccomponent
// import Children from './components/Children.vue'

const AysncComp = defineAsyncComponent({
    loader: () = > import('/')})export default {
    name: 'App'
    setup() {},
    components: {
        AysncComp
    }
}
</script>
Copy the code

The effect is as follows:

#fallback is loaded before the asynchronous result is returned

After some time, the asynchronous results are returned and the #default content is rendered

Vue provides a lifecycle hook function to capture errors that have captured child components. OnErrorCaptured is an example of how to capture errors that are often captured asynchronously or when the result is returned.

const error = ref(null)
onErrorCaptured(e= > {
    error.value = e
    return false
})
Copy the code

The template structure is changed to the following to handle loading errors

<template>
    <div>
        <div v-if="error">{{error}}</div>
        <Suspense v-else>
            <template #default>
                <! -- There may be asynchronous requests or asynchronous tasks -->
                <AsyncComp />
            </template>
            <template #fallback>
                <! -- The content here will be rendered during the #default content load and displayed -->
                <div>data Info Loading...</div>
            </template>
        </Suspense>
    </div>
</template>
Copy the code

Analysis of implementation principle

We now break down the principles of Suspense into two parts: initialization and updates

SuspenseThe initialization

Suspense when Patch parses Suspense components, it differs from other components in that the type passed in in Suspense is the configuration object generated by Suspense and goes into the Suspense branch to execute process

In Suspense, the process function is implemented internally to resolve Suspense. In Suspense, the process is no different from ordinary components. In Suspense, the old vNode is updated. Suspense vNodes are quite different from the rest of the render function

Suspense Children must be two functions, or small renderers, with two attributes :ssContent: a vNode for #default content, and ssFallback: a vNode for #fallback content

Initialization entry:mountSuspensefunction

Initialize a suspense object and place it in suspense vNode. This object records all information about suspense currently, for example: 1. In Suspense, are you in the fallback stage? 2. Is it server rendering? There are also some methods for operating Suspense.

#default is parsed and set to pendingBranch. HiddenContainer is the #default container, which will not be displayed at first. It may generate asynchronous DEP during parsing #default.

Handling asynchronous DEP:registerDepfunction

SetupStatefulComponent () and mountComponent (); setupStatefulComponent (); setupStatefulComponent ()

The setupStatefulComponent function saves the result and waits for later execution, or if it’s a server rendering, hangs the.then() and.catch()

mountComponentFunction is called on the instanceregisterDepMethod (This method isSuspenseOne of the operation methods of,parentSuspenseIs actuallysuspenseObject), the next step is for later parsing#defaultPlease give him a placeholder

Suspense deps increase by 1 each time an asynchronous DEP comes in! Suspense deps increase by 1 each time an asynchronous DEP comes in! Suspense deps increase by 1 each time an asynchronous DEP comes in! Let’s continue to see what mountSuspense’s next installment does,

In suspense >, resolve() is executed when #default is not unregistered for asynchronous DEps (suspense > deps are not greater than 0). But the asynchronous DEP, which would normally be generated, goes into if, mounts the backup #fallback, executes onPending and onFallback hook functions, and finally sets #fallback to activeBranch

Catch () and.then(). That’s why you can use onErrorCaptured to catch errors..catch() calls handlerError. OnErrorCaptured hooks are lost internally. ErrorCaptured is also compatible with V2,

If there are no errors in the returned results, you can follow normal logic, first making sure that the component is not uninstalled or at least uninstalled and that the asynchronies for waiting and processing are not the same if not.

The rest is not too different from mounting normal components, handling results in the same way, v2-compatible, and rendering dependencies in the same way, except that an end to Suspense needs to be resolved.

At the same time, there can be nesting of asynchronous tasks and multiple asynchronous tasks, so suspense > can be controlled until the last asynchronous end

End stage:resolvefunction

In Suspense, take a series of data out of Suspense for use later.

The following is an operation to mount #default, mainly for client rendering, handling exit and entry transitions, in order to unmount #fallback and mount #default, since transitions may not exist, the unmount process will handle transitions along the way. Therefore, you need to determine whether to perform the mount yourself based on delayEnter later

If it is server-side rendering, nothing will be done, just turn it off because it has been resolved.

The rest of the process is a bit simpler, update activeBranch, restore your other attributes to defaults, deal with nested Suspense cases, keep looking out, merge yourself into Parent Suspense asynchronous tasks, can’t find out how to start all asynchronous tasks, Finally, execute the hook function onResolve and an initialization process is completed.

SuspenseUpdate phase of

Suspense updates are more complex, with a variety of scenarios, such as whether to start suspending pengindBranch, resolving money, or after resolving. Let’s take it branch by branch.

Update entry:patchSuspensefunction

It still starts with updating the new VNodes and containers. The new #default and #fallback are folded up to show that there is an if else, but there is a lot of consideration in it.

pendingBranchThere is also

Go inside and cover the old pendingBranch. If you look down, there are still a lot of if else

  • Same root type, but the content may have changed.

Suspense is similar to the process for initializing in Suspense, except to update the old pendingBranch, but because render or effect has not been executed or set, only props and slots are updated.

  • Different root types inresolveBefore the end will be the wholepending treeReplace.

There are basically two cases: Whether the pending tree is mounted to the page, or not, it is similar to the initialization process, or it needs to update the new #fallback

If it is mounted, it is even easier to update activeBranch with newBranch, and since resolve is restarted, there is no need to load the transition (which has already been loaded once).

Maybe there’s a third one, probably before we go to the second one, before we go to the third one.

pendingBranchA non-existent situation

pendingBranchDoes not exist, represents the whole<Suspense>The update begins when all the flow has been completed and the asynchronous content has been displayed. There are two kinds of this,

The first is just an update triggered inside #default, just plain patch. No asynchronous tasks are created

If the root node changes, onPending will be triggered first. If the root node changes, onPending will be triggered first. If the root node changes, the pendingId will be changed.

Loading #fallback is also a condition that exists, and the old #default is displayed if the condition is not satisfied. Condition 1: timeout(#default maximum loading time) is greater than 0, and the same suspended #default is displayed. Condition 2: Timeout is equal to 0, which means there will be no timeout, waiting for #default to load.

A small summary

Suspense mounts and updates take into account a variety of situations and have many boundary decisions. The actual process is pretty much the same. Both mount and update #default and fallback.

Suspense> makes it easier to handle asynchrony in Vue without having to manually control global variables or isLoading variables. You can use onErrorCapurted in conjunction with handling loading errors.

Teleport

Teleport’s definition in the official documentation: “Vue encourages us to build uIs by encapsulating UI and related behavior into components. We can nest them inside another to build a tree that makes up the application UI.” . This feels similar to the difference between container components and UI components in React.

Use case

Create a component that contains full-screen mode. In most cases, you want the logic of the modal box to exist in the component, but quick positioning of the modal box is difficult to solve with CSS, or requires changing the component composition. Using Teleport, the implementation is as follows

App component

<template>
  <div>Tooltips Vue 3 Teleport</div>
  <ModalButton />
</template>

<script setup lang="ts">
import ModalButton from './components/ModalButton.vue'
</script>

<style scoped>

</style>
Copy the code

ModalButton components

<template>
  <button @click="open">open full screen modal</button>

  <teleport to="body">
    <div v-if="modalOpen" class="modal">
      {{ modalOpen }}
      <div>I'm a teleport modal!!! (My Parent is body)</div>
      <button @click="clone">clone full screen modal</button>
    </div>
  </teleport>
</template>

<script setup lang="ts">
import { ref } from "vue";
const modalOpen = ref(true);
// Close the function
const clone = () = > modalOpen.value = false
// Turn on the function
const open = () = > modalOpen.value = true
</script>

<style scoped>
</style>
Copy the code

props


is controlled primarily with the to and disabled parameters

To: Must be passed, must be, must be a valid query selector or HTMLElement (if used in a browser environment). Specifies the target element in which the

content will be moved. (This element can be SVG and other elements)

Disabled: This optional property can be used to disable the functionality of

, which means that its slot contents will not be moved to any location, but instead rendered where you specified

in surrounding parent components. (True: disabled; false: enabled)

Note that this will move the actual DOM node rather than being destroyed and recreated, and it will also keep any component instances active. All stateful HTML elements (that is, the video played) will remain in their state.

Front knowledge

resloveTarget: Find render<teleport>Content target object, can not find a warning message, the principle is throughselectTake a selector to fetch nodes from the tree, and in the browser environment,selectAre generallyquerySelector(the rendererrendererMethod, which may not be supported by some platforms, may not be availableTeleport).

Class selectors and id selectors are recommended, because you’re using querySelector and you need to pass it with. Or #, the container should be outside the vue tree and should be available before

is rendered into it.

IsTeleport: Is this a teleport?

export const isTeleport = (type: any) :boolean= > type.__isTeleport
Copy the code

IsTeleportDisabled: Indicates whether the teleport capability is disabled

const isTeleportDisabled = (props: VNode['props') :boolean= >
  props && (props.disabled || props.disabled === ' ')
Copy the code

IsTargetSVG: Is the transfer target SVG

const isTargetSVG = (target: RendererElement): boolean= >
  typeofSVGElement ! = ='undefined' && target instanceof SVGElement
Copy the code

Realize the principle of

Initialization Process

In the patch function, type will be the Teleport configuration object

Here you can see the familiar function :process,

is initialized by a process function again. Unlike Suspense, Teleport relies on this function to get through to the end, and basically handles mount and updates in it.

Preparation before mounting

On entry, a series of operation functions are removed from the renderer for easier operation. Disabled is used later to verify that Teleport is disabled. Initialization must be done to mount content, first look at how

mounts content, before looking at the start of the mount.

Inserts a locator in the main view, which can be disabledTeleportFunction in the case of rendering in the default container. That’s rendering<teleport>Inside the parent component.

Target is the target container for rendering

content, which may not exist or be found. TargetAnchor is also a locator that can be inserted directly into the target container after the contents of

are built in memory.

Mount the start

Teleport writes a mount function to verify whether the < Teleport > is array children, but < Teleport > must have array children, otherwise it will not render a lonely bar (blank content).

The parameter anchor of the mount function is used to confirm the container rendered by

, whether it is rendered in the target container or the default location, both of which depend on the previously determined disabled judgment: when disabled, render in the default container, and when enabled, render to the specified container. The initialization process for

ends.

Update the stage

Teleport updates can be triggered in only two cases: 2. The contents of

are updated. Once the update is triggered, all values will be changed

moveTeleportfunction

<teleport>The update is entirely dependent on this function, which is used to move<teleport>To enter the content, you need to confirm the new target container in the target containertargetChange the case to move the locator to the new container. The only code left is two flows.

  1. <teleport>Not because of the reordering effect, but just<teleport>The child nodes in are being reordered. Will enter the movement in the middlechildrenThe logic of
  2. <teleport>Is the child of a node whose child needs to be reordered for some reason,<teleport>Forced to move, move first<teleport>And then there’s the point, because it’s reordering (isReorderfortrue), so only if the transport function is disabled (<teleport>Bytes rendered inTeleportBetween the start and end tags), will move the child node, and finally willTeleportThe end marker moves.

IsReorder is a reorder flag that is passed in by moveType, moveType equals TARGER_CHANGE and isReorder equals true.

Teleport content updated

It is mainly divided into directly updating a block (an element containing multiple element nodes can be called a block, after updating the block, the EL needs to be reconfirmed to ensure that all root nodes are based on previous DOM references, so that they can be moved in future updates), or updating the child nodes within the block.

Props changes update

In this case, either the render target container for the content of

was changed or teleport transport wasDisabled or enabled, now disabled is the new capability state and wasDisabled is the old capability state. Rely on them to determine new render targets.

  • Switch the transfer function from enabled to disabled

Moves the contents of the render target container to the default container

  • Target container changes

<teleport>Repassing a selector,TeleportIt goes back to the target container and moves the content there

  • Switch the transfer function from disabled to enabled

Moves the contents of the default container to the container specified when the

Content analysis of Teleport update phase is complete

Server renderedTeleport

Server rendering is actually not all that different from client rendering, both are updatestoanddisabledTo confirm the location of the render, it may just be a different way of rendering the data. The details will be discussed later when we study VUE SSR.

A small summary

The main principle of Teleport is to perform selective view rendering in that node. This function can be turned on or off with the parameter disabled. Teleport allows us to encapsulate the UI and behavior of one component and embed it in other components easily.

The last

This is the end of the analysis of the implementation principles of Teleport and Suspense, some of the analysis may not be in place, I hope you can supplement and correct.