SoFunction
Updated on 2025-04-06

Detailed explanation of the use and principle examples of vue3 setup

1. Preface

Why use setup?

When writing a large component, the list of logical concerns is very long, which is not conducive to maintenance and reading; so it would be better to collect the code of logical concerns together, thus giving birth to a combination API, that is, the setup used in vue.

Just like we are going to make braised eggs, the ingredients are originally four piles of spices: star anise, cinnamon, bay leaves, anise (data, method, computed, watch); if it is too troublesome, just pack it into a spice bag, put one bag at a time (setup)

Recently, I am working on vue3-related projects and using a combination API. I appreciate the improvement of vue3's syntax and it is very convenient to use. For students who are already familiar with the writing of vue2, it still requires a certain learning cost to get started. It may be that they are currently at the stage of being able to write and use it. However, what changes are brought about by setup, and what are the internal implementation principles of the two APIs, Ref and Reactive. Let’s summarize it first:

Changes brought by setup:

1. Solved that the data and methods of vue2 are too far apart to be reused between components

2. Provides code blocks that introduce common business logic by script tags, and executes sequentially.

Become a setup function, exposed to the template by default

4. The component is mounted directly without registration

5. Customized instructions can also be automatically obtained in the template.

No longer a reference to this active instance

7. A large number of new APIs brought, such as defineProps, defineEmits, withDefault, toRef, toRefs

Changes brought by ref:

Vue provides aref()Methods to allow us to create a usableAny value typeResponsive data

Ref as TS type annotation

Changes brought by reactive:

Can be usedreactive()Function creates a responsive object or array

reactive can implicitly deduce types from their parameters

Use interface for type annotation

If you need to know the difference between vue2 and vue3, you can check out this article of mine:

Analyze the difference between vue2 and vue3 in an easy-to-understand way

existsetup()It is very cumbersome to manually expose a large number of states and methods in functions. Fortunately, we can simplify that by using build tools. When using single file components (SFC), we can use<script setup>To greatly simplify the code.

<script setup>The import and variable declaration of the top layer in the   can be used directly in the template of the same component. You can understand it as expressions in templates and<script setup>The code in  is in the same scope.

The code inside willThe contents of the component setup() function are compiled. This means that it is with ordinary<script>Only in componentsFirst introducedWhenExecute oncedifferent,<script setup>The code inevery timeComponent instanceCreatedWhen executed.

Official answer:

<script setup>is a compile-time syntax sugar that uses a combined API in a single file component (SFC). This syntax is the default recommendation when using SFC and a combined API. Compared to ordinary<script>Syntax, it has more advantages:

  • Less boilerplate content, cleaner code.
  • Ability to declare props and custom events using pure TypeScript.
  • Better runtime performance (its templates are compiled into rendering functions within the same scope, avoiding rendering context proxy objects).
  • Better IDE type derivation performance (reduces the work of language servers to extract types from code).

The setup execution is beforeCreate execution before creating an instance, so this in the setup function is not an instance of the component, but undefined, and the setup is synchronized.

setup?: (this: void, props: Readonly<LooseRequired<Props & UnionToIntersection<ExtractOptionProp<Mixin>> & UnionToIntersection<ExtractOptionProp<Extends>>>>, ctx: SetupContext<E>) => Promise<RawBindings> | RawBindings | RenderFunction | void;)

In the above code, we learned about the first parameter props and the second parameter context.

props accepts all properties and methods passed by the parent component; context is an object, which is not responsive and can be deconstructed and assigned. The existence attribute is attrs:,slots:,emit: .

setup(props, { attrs, slots, emit, expose }) {
   ...
 }
 or
 setup(props, content) {
   const { attrs, slots, emit, expose } = content
 }

Note here that attrs and slots are stateful objects, and they will always be updated with the update of the component itself. This means you should avoid deconstructing them and always refer to the property in the form of or . Note that unlike props, the properties of attrs and slots are non-responsive. If you intend to apply side effects based on changes to attrs or slots, you should do this in the onBeforeUpdate lifecycle hook.

3. Source code analysis

In the 3.2.3x version of vue, the source code file of the processing setup function is located at:node_moudles/@vue/runtime-core/dist/in the file.

setupStatefulComponent

Let’s start parsing the execution process of setupStatefulComponent:

function setupStatefulComponent(instance, isSSR) {
    var _a;
    const Component = ;
    {
        if () {
            validateComponentName(, );
        }
        if () {
            const names = ();
            for (let i = 0; i < ; i++) {
                validateComponentName(names[i], );
            }
        }
        if () {
            const names = ();
            for (let i = 0; i < ; i++) {
                validateDirectiveName(names[i]);
            }
        }
        if ( && isRuntimeOnly()) {
            warn(`"compilerOptions" is only supported when using a build of Vue that ` +
                `includes the runtime compiler. Since you are using a runtime-only ` +
                `build, the options should be passed via your build tool config instead.`);
        }
    }
    // 0. create render proxy property access cache
     = (null);
    // 1. create public instance / render proxy
    // also mark it raw so it's never observed
     = (new Proxy(, PublicInstanceProxyHandlers));
    {
        exposePropsOnRenderContext(instance);
    }
    // 2. call setup()
    const { setup } = Component;
    if (setup) {
        const setupContext = ( =
             > 1 ? createSetupContext(instance) : null);
        setCurrentInstance(instance);
        ();
        const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [() , setupContext]);
        ();
        unsetCurrentInstance();
        if ((setupResult)) {
            (unsetCurrentInstance, unsetCurrentInstance);
            if (isSSR) {
                // return the promise so server-renderer can wait on it
                return setupResult
                    .then((resolvedResult) => {
                    handleSetupResult(instance, resolvedResult, isSSR);
                })
                    .catch(e => {
                    handleError(e, instance, 0 /* ErrorCodes.SETUP_FUNCTION */);
                });
            }
            else {
                // async setup returned Promise.
                // bail here and wait for re-entry.
                 = setupResult;
                if (!) {
                    const name = (_a = ) !== null && _a !== void 0 ? _a : 'Anonymous';
                    warn(`Component <${name}>: setup function returned a promise, but no ` +
                        `<Suspense> boundary was found in the parent component tree. ` +
                        `A component with async setup() must be nested in a <Suspense> ` +
                        `in order to be rendered.`);
                }
            }
        }
        else {
            handleSetupResult(instance, setupResult, isSSR);
        }
    }
    else {
        finishComponentSetup(instance, isSSR);
    }
}

The function accepts two parameters, one is to form an instance, the other is whether to render ssr, and the next is the verification process. The file here is the development environment file. The DEV environment will start to detect the naming of various options in the component, such as name, components, directives, etc. If there is a problem with the detection, a warning will be issued in the development environment.

After the detection is completed, initialization is performed to generate an accessCached property object, which is used to cache the renderer proxy attributes to reduce the number of reads. Then initialize the property of a proxy, = (new Proxy(, PublicInstanceProxyHandlers); This proxy attribute proxies the component's context and sets it to observe the original value so that the proxy object will not be traced.

Next is the core logic of setup. If there is a setup function on the component, continue to execute. If there is no existence, jump to the end, execute finishComponentSetup(instance, isSSR) to complete the initialization of the component, otherwise it will enter.if (setup)In the following branch conditions. Whether to execute the setup generation context depends on > 1 ? createSetupContext(instance) : null.

Let’s take a look at what the setup execution context is:

function createSetupContext(instance) {
    const expose = exposed => {
        if () {
            warn(`expose() should be called only once per setup().`);
        }
         = exposed || {};
    };
    let attrs;
    {
        // We use getters in dev in case libs like test-utils overwrite instance
        // properties (overwrites should not be done in prod)
        return ({
            get attrs() {
                return attrs || (attrs = createAttrsProxy(instance));
            },
            get slots() {
                return ();
            },
            get emit() {
                return (event, ...args) => (event, ...args);
            },
            expose
        });
    }
}

expose analysis:

This API can be used in setup() to clear control of what is explicitly exposed to component consumers.

When you are encapsulating components, if you think there is too much content exposed in ref, you might as well use expose to constrain the output.

import { ref } from 'vue'
export default {
  setup(_, { expose }) {
    const count = ref(0)
    function increment() {
      ++
    }
    // Expose only increment to parent component    expose({
      increment
    })
    return { increment, count }
  }
}

For example, when you use expose like the code above, the ref object obtained by the parent component will only have an increment property, and the count property will not be exposed.

Execute the setup function

After processing the context of createSetupContext, the component stops dependency collection and starts executing the setup function.

const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [() , setupContext]); 

Vue will call the setup function through callWithErrorHandling, and the component instance instance instance is passed in. Here we can see the last line, which is passed in as the args parameter. As described above, props will always be passed in. If it is <= 1, setupContext will be null.

After calling play setup, the collection status will be reset, (), and the next step is to determine the type of setupResult.

     if ((setupResult)) {
            (unsetCurrentInstance, unsetCurrentInstance);
            if (isSSR) {
                // return the promise so server-renderer can wait on it
                return setupResult
                    .then((resolvedResult) => {
                    handleSetupResult(instance, resolvedResult, isSSR);
                })
                    .catch(e => {
                    handleError(e, instance, 0 /* ErrorCodes.SETUP_FUNCTION */);
                });
            }
            else {
                // async setup returned Promise.
                // bail here and wait for re-entry.
                 = setupResult;
                if (!) {
                    const name = (_a = ) !== null && _a !== void 0 ? _a : 'Anonymous';
                    warn(`Component <${name}>: setup function returned a promise, but no ` +
                        `<Suspense> boundary was found in the parent component tree. ` +
                        `A component with async setup() must be nested in a <Suspense> ` +
                        `in order to be rendered.`);
                }
            }
        }

If the return value of the setup function is of promise type and is rendered on the server, it will wait for execution to continue. Otherwise, an error will be reported, saying that the current version of Vue does not support the setup to return the promise object.

If the return value is not a promise type, the return result will be handled by the handleSetupResult function.

else {
            handleSetupResult(instance, setupResult, isSSR);
        }
function handleSetupResult(instance, setupResult, isSSR) {
    if ((setupResult)) {
        // setup returned an inline render function
        if (.__ssrInlineRender) {
            // when the function's name is `ssrRender` (compiled by SFC inline mode),
            // set it as ssrRender instead.
             = setupResult;
        }
        else {
             = setupResult;
        }
    }
    else if ((setupResult)) {
        if (isVNode(setupResult)) {
            warn(`setup() should not return VNodes directly - ` +
                `return a render function instead.`);
        }
        // setup returned bindings.
        // assuming a render function compiled from template is present.
        {
             = setupResult;
        }
         = (setupResult);
        {
            exposeSetupStateOnRenderContext(instance);
        }
    }
    else if (setupResult !== undefined) {
        warn(`setup() should return an object. Received: ${setupResult === null ? 'null' : typeof setupResult}`);
    }
    finishComponentSetup(instance, isSSR);
}

In the result capture function handleSetupResult, first determine the type of the result returned by the setup. If it is a function and it is an in-line mode rendering function on the server, the result will be used as the ssrRender attribute; and in the case of non-server rendering, it will be directly treated as a render function.

Then it will be judged that the result of the setup returns. If it is an object, the object will be converted into a proxy object and set as the setupState property of the component instance.

In the end, like other components without a setup function, they will call finishComponentSetup to complete the creation of the component.

finishComponentSetup

function finishComponentSetup(instance, isSSR, skipOptions) {
    const Component = ;
    // template / render function normalization
    // could be already set when returned from setup()
    if (!) {
        // only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
        // is done by server-renderer
        if (!isSSR && compile && !) {
            const template = ;
            if (template) {
                {
                    startMeasure(instance, `compile`);
                }
                const { isCustomElement, compilerOptions } = ;
                const { delimiters, compilerOptions: componentCompilerOptions } = Component;
                const finalCompilerOptions = (({
                    isCustomElement,
                    delimiters
                }, compilerOptions), componentCompilerOptions);
                 = compile(template, finalCompilerOptions);
                {
                    endMeasure(instance, `compile`);
                }
            }
        }
         = ( || );
        // for runtime-compiled render functions using `with` blocks, the render
        // proxy used needs a different `has` handler which is more performant and
        // also only allows a whitelist of globals to fallthrough.
        if (installWithProxy) {
            installWithProxy(instance);
        }
    }
    // support for  options
    {
        setCurrentInstance(instance);
        ();
        applyOptions(instance);
        ();
        unsetCurrentInstance();
    }
    // warn missing template/render
    // the runtime compilation of template in SSR is done by server-render
    if (! &&  ===  && !isSSR) {
        /* istanbul ignore if */
        if (!compile && ) {
            warn(`Component provided template option but ` +
                `runtime compilation is not supported in this build of Vue.` +
                (``) /* should not happen */);
        }
        else {
            warn(`Component is missing template or render function.`);
        }
    }
}

The main function of this function is to obtain and set rendering functions for components. There are three standard behaviors for templates and rendering functions to obtain:

1. The rendering function may already exist, and the result is returned through setup. For example, we talked about in the previous section where the return value of setup is a function.

2. If the setup does not return, try to get the component template and compile it,Get the rendering function in

3. If this function still has no rendering function,Set to empty so that it can get the rendering function from mixins/extend etc.

Under the guidance of this standardized behavior, this first judges the server-side rendering situation, and then judges the situation where there is no existence. When making this judgment, it has been stated that the component has not obtained the rendering function from the setup and is trying to make the second behavior. Get the template from the component, set the compilation options and call = compile(template, finalCompilerOptions); for compilation. The compilation process will not be described in detail.

Finally, the compiled render function is assigned to the render property of the component instance, and if not, the value is assigned to the NOOP empty function.

Then determine whether the rendering function is a runtime compiled rendering function with block wrap. If this is the case, the rendering agent will be set to a different one.hashandler proxy trap, it has stronger performance and can avoid detecting some global variables.

At this point, the initialization of the component is completed and the rendering function is also set to end.

4. Summary

In vue3, the new setup function attribute provides us with the convenience of writing. The workload behind it is undoubtedly huge. The initialization process of stateful components. In the initialization part of the setup function, we not only learn the conditions for initialization of the setup context, but also clearly know what attributes the setup context exposes us, and learn a new RFC proposal attribute from it: expose attribute

We learned the process of executing the setup function and how Vue handles capturing the return result of the setup.

Then we explained the finishComponentSetup function that will be executed regardless of whether setup is used or not when component initialization is used. Through the logic inside this function, we understand the rules for the rendering function setting of a component when initialization is completed.