1
头图

In the previous articles, we have learned the novel Composition API in Vue3 together, and today I want to take you to take a look at another fresh way of writing in Vue3-setup.

In most cases, the components we write are stateful components, and such components will be marked as stateful comonents during the initialization process. When Vue3 detects that we are dealing with such stateful components, it will call Function setupStatefulComponent to initialize a stateful component. The source code location of the processing component is: @vue/runtime-core/src/component.ts .

setupStatefulComponent

Next, the author will take everyone to analyze the process of setupStatefulComponent:

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions

  if (__DEV__) { /* 检测组件名称、指令、编译选项等等,有错误则报警 */ }
    
  // 0. 创建一个渲染代理的属性的访问缓存
  instance.accessCache = Object.create(null)
  // 1. 创建一个公共的示例或渲染器代理
  // 它将被标记为 raw,所以它不会被追踪
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)

  // 2. 调用 setup()
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    currentInstance = instance
    pauseTracking()
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    resetTracking()
    currentInstance = null

    if (isPromise(setupResult)) {
      if (isSSR) {
        // 返回一个 promise,因此服务端渲染可以等待它执行。
        return setupResult
          .then((resolvedResult: unknown) => {
            handleSetupResult(instance, resolvedResult, isSSR)
          })
          .catch(e => {
            handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
          })
      }
    } else {
      // 捕获 Setup 执行结果
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    // 完成组件初始化
    finishComponentSetup(instance, isSSR)
  }
}

The component will initialize a Component variable at the beginning, which holds the options of the component. Next, if it is a DEV environment, it 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 reported in the development environment.

After the detection is completed, a serious initialization process will begin. First, an accessCache attribute will be created on the instance, which is used to cache the renderer proxy attributes to reduce the number of reads. After that, a proxy property will be initialized on the component instance. This proxy property proxy the context of the component and set it to observe the original value so that the proxy object will not be tracked.

After that, we started to deal with the setup logic that we care about in this article. First take out the setup function from the component, here it is judged whether there is a setup function, if it does not exist, jump directly to the bottom logic, execute finishComponentSetup, complete the component initialization. Otherwise, it will enter the branch condition after if (setup)

Whether to execute createSetupContext to generate the context object of setup depends on whether the number of formal parameters in the setup function is greater than 1.

One point of knowledge that needs to be noted here is: when length is called on the function object, the return value is the amount of shape parameter of the function.

for example:

setup() // setup.length === 0

setup(props) // setup.length === 1

setup(props, { emit, attrs }) // setup.length === 2

By default, props is a parameter that must be passed when calling setup, so the condition for generating the context of setup is setup.length> 1.

So following the code logic, let's take a look at what exactly is in the setup context.

export function createSetupContext(
  instance: ComponentInternalInstance
): SetupContext {
  const expose: SetupContext['expose'] = exposed => {
    instance.exposed = proxyRefs(exposed)
  }

  if (__DEV__) {
    /* DEV 逻辑忽略,对上下文选项设置 getter */
  } else {
    return {
      attrs: instance.attrs,
      slots: instance.slots,
      emit: instance.emit,
      expose
    }
  }
}

The magical effect of expose

Seeing the logic of this createSetupContext function, we found that in the setup context, as described in the document, there are three familiar attributes: attrs, slots, and emit, and we are surprised to find that there is even one that is not explained in the document. The expose property is returned.

Expose is an earlier proposal in the Vue RFC. The idea of expose is to provide a expose({ ...publicMembers }) , so that component authors can use the API in setup() to clearly control what content will be explicitly exposed to Component user.

When you are encapsulating components, if too much content is exposed in ref, you may wish to use expose to restrict the output. Of course, this is only an RFC proposal, and interested friends can secretly try it.

import { ref } from 'vue'
export default {
  setup(_, { expose }) {
    const count = ref(0)

    function increment() {
      count.value++
    }
    
    // 仅仅暴露 increment 给父组件
    expose({
      increment
    })

    return { increment, count }
  }
}

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

Execute the setup function

After processing the context of the setupContext, the component will stop dependency collection and start executing the setup function.

const setupResult = callWithErrorHandling(
  setup,
  instance,
  ErrorCodes.SETUP_FUNCTION,
  [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)

Vue will call the setup function through callWithErrorHandling. 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 setup.length <= 1, setupContext will be null.

After calling setup, the dependency collection state will be reset. Next, determine the return value type of setupResult.

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

If it is not a promise type return value, the return result will be processed through the handleSetupResult function.

export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
    // setup 返回了一个行内渲染函数
    if (__NODE_JS__ && (instance.type as ComponentOptions).__ssrInlineRender) {
      // 当这个函数的名字是 ssrRender (通过 SFC 的行内模式编译)
      // 将函数作为服务端渲染函数
      instance.ssrRender = setupResult
    } else {
      // 否则将函数作为渲染函数
      instance.render = setupResult as InternalRenderFunction
    }
  } else if (isObject(setupResult)) {
    // 将返回对象转换为响应式对象,并设置为实例的 setupState 属性
    instance.setupState = proxyRefs(setupResult)
  }
  finishComponentSetup(instance, isSSR)
}

In the result capture function of handleSetupResult, first determine the type of the result returned by setup. If it is a function and the inline mode rendering function of the server, then the result is used as the ssrRender property; and in the case of non-server rendering, it will Treat it as a render function directly.

Then it will determine if the setup return result is an object, it will convert this object into a proxy object and set it to the setupState property of the component instance.

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

finishComponentSetup

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

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

2. If the setup does not return, try to get the component template and compile, get the rendering function Component.render

3. If this function still has no rendering function, instance.render to empty so that it can get the rendering function from mixins/extend and other methods.

Under the guidance of this normative behavior, first judged the server-side rendering situation, and then judged that there is no instance.render. When this judgment is made, it has been explained that the component has not obtained the rendering function from the setup. Attempt of two behaviors. Get the template from the component, call Component.render = compile(template, finalCompilerOptions) to compile after setting the compilation options. The knowledge of this part of compilation is described in first article compilation process of my 160d09b6bcc06e.

Finally, assign the compiled rendering function to the render attribute of the component instance, if not, assign it to the NOOP empty function.

Then determine whether the rendering function is a rendering function compiled at runtime using the with block package. If this is the case, the rendering proxy will be set to a different has handler proxy trap, which has stronger performance and can avoid some detection Global variables.

At this point, the initialization of the component is complete, and the rendering function is also set.

export function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean,
  skipOptions?: boolean
) {
  const Component = instance.type as ComponentOptions

  // 模板 / 渲染函数的规范行为
  // 1、渲染函数可能已经存在,通过 setup 返回
  // 2、除此之外尝试使用 `Component.render` 当做渲染函数
  // 3、如果这个函数没有渲染函数,设置 `instance.render` 为空函数,以便它能从 mixins/extend 中获得渲染函数
  if (__NODE_JS__ && isSSR) {
    instance.render = (instance.render ||
      Component.render ||
      NOOP) as InternalRenderFunction
  } else if (!instance.render) {
    // 可以在 setup() 中设置
    if (compile && !Component.render) {
      const template = Component.template
      if (template) {
        const { isCustomElement, compilerOptions } = instance.appContext.config
        const {
          delimiters,
          compilerOptions: componentCompilerOptions
        } = Component
        const finalCompilerOptions: CompilerOptions = extend(
          extend(
            {
              isCustomElement,
              delimiters
            },
            compilerOptions
          ),
          componentCompilerOptions
        )
        Component.render = compile(template, finalCompilerOptions)
      }
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction

    // 对于使用 `with` 块的运行时编译的渲染函数,这个渲染代理需要不一样的 `has` handler 陷阱,它有更好的
    // 性能表现并且只允许白名单内的 globals 属性通过。
    if (instance.render._rc) {
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      )
    }
  }
}

to sum up

Today, the author introduced the initialization process of a stateful component and explained it carefully in the initialization part of the setup function. We not only learned the conditions of setup context initialization, but also clearly understood which attributes the setup context exposes to us. And learned a new RFC proposal: expose attribute.

We learned about the execution process of the setup function and how Vue handles capturing the returned result of the setup.

Finally, we explained the finishComponentSetup function that will be executed when the component is initialized regardless of whether the setup is used. Through the internal logic of this function, we understand the rules of the rendering function setting when a component is initialized.

Finally, if this article can help you understand the small details of the setup in Vue3, I hope to give this article a little like ❤️. If you want to continue to follow up the follow-up articles, you can also follow my account or follow my github , thank you again for the lovely officials.


Originalix
165 声望63 粉丝

前端工程师,欢迎来 github 互相 follow, originlaix