core icon indicating copy to clipboard operation
core copied to clipboard

feat(compiler-sfc): introduce `defineRender` macro

Open sxzz opened this issue 2 years ago • 13 comments

⚠️ Status: Experimental

Summary

See the RFC for details.

This PR introduces a new macro called defineRender that allows for defining a render function in <script setup> with or without JSX, instead of template syntax.

This is an experimental feature. We introduced a new compiler script option defineRender, and it's disabled by default.

Basic Usage

<script setup lang="jsx">
import { h } from 'vue'

defineRender(<div />)
defineRender(h('div'))
defineRender(() => {
  // ...
  return <div />
})
</script>

Prev Solution: return statement

Redundant return statements can be removed by bundlers (tested on both Rollup and esbuild)

Example: https://deploy-preview-9400--vue-sfc-playground.netlify.app/#eNp9kDFPwzAQhf/K4SWtVIWhW9VWAlQJGAABEouXKLmEFOdsne0QKcp/x3bUwoC6We99vvfuRnFjTN57FBuxtSW3xoFF581eUtsZzQ5GYKxhgpp1B1lAM0mlJuugsw3sorvI7lEpDR+aVXWVLSVxmMEEiyXs9pHL+0J5lLS9nkPCeLESzoZBddvkR6spNBglAUhR6s60CvnZuDYESbGB5ESvCDnfj0lz7HF10stPLL/+0Y92iJoUL4wWuUcpzp4ruEE324e3JxzC+2x2uvIq0BfMV7Ra+dhxxm49VaH2Hy61fUh3bKl5t4fBIdnTUrFoJKfESxFue3dh9d+663yd/kmaxPQDLeaULA==

image

sxzz avatar Oct 13 '23 16:10 sxzz

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 85.9 kB 32.6 kB 29.5 kB
vue.global.prod.js 132 kB 49.3 kB 44.4 kB

Usages

Name Size Gzip Brotli
createApp 47.9 kB 18.8 kB 17.2 kB
createSSRApp 50.6 kB 19.9 kB 18.2 kB
defineCustomElement 50.3 kB 19.6 kB 17.9 kB
overall 61.2 kB 23.7 kB 21.6 kB

github-actions[bot] avatar Oct 13 '23 16:10 github-actions[bot]

Should we warn if there's a return statement inside of if branch?

<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')

if (msg.value !== '')
    return () => msg.value // never return
</script>

The setup function is only called once, so most cases of if + return statement are invalid and likely cause unexpected behaviors.

sxzz avatar Oct 14 '23 02:10 sxzz

The setup function is only called once, so most cases of if + return statement are invalid and likely cause unexpected behaviors.

I wouldn't call "invalid", they are likely to be incorrect, but I think that's a linter's job, not compiler, we don't know how people use it, if you add a warning we need a way to remove the warning I suppose.

Just because this is a perfectly fine component, albeit not very good, but fine

defineComponent({
    props: {
        test: Number
    },

    setup(props) {
        if (props.test > 1) {
            return () => h('div', 'big number')
        }
        return () => h('div', 'small number')
    }
})

pikax avatar Oct 14 '23 07:10 pikax

After discussing with Evan, we added one more macro, defineRender.

Compared to the return statement:

  • Macro is more explicit for defining render. The return statement has unclear semantics.
  • return can be written more than once can could cause losing reactivity like I mentioned before. But in defineRender, it can only called once.
  • For all macros, the order of calling is not concerned. In other words, it's the same effect, calling defineRender whether it's the first line or the last line.
  • Like the above one, all macros can only called in the top level of <script setup> but the return statement is not.

sxzz avatar Oct 17 '23 09:10 sxzz

It should not be shared with template syntax, right?

<script setup lang='jsx'>
  import { ref } from 'vue'
  const msg = ref('Hello World!')
  defineRender(<p>{msg.value}</p>)
</script>
<template>
  <p>{msg.value}</p>
</template>

baiwusanyu-c avatar Oct 17 '23 09:10 baiwusanyu-c

Should it go through RFC first?

I would say if defineRender is used the compiler should throw an error if <template> (the render template) is present

pikax avatar Oct 17 '23 09:10 pikax

It should not be shared with template syntax, right?

<script setup lang='jsx'>
  import { ref } from 'vue'
  const msg = ref('Hello World!')
  defineRender(<p>{msg.value}</p>)
</script>
<template>
  <p>{msg.value}</p>
</template>

It should be the same as the render function, with the template as the main:

https://play.vuejs.org/#eNp9Uk9PgzAU/yq1F2ZC4LDbRBI1S9SDGjXx0guBB+ssbdOWSbLw3X0tg22J7kBof39efn3v7emd1smuA7qimS0N145YcJ3OmeStVsaRPTFQk4HURrUkQmk0Uw+q1Qc8Sf3FV0KaySwdi2EZvDhotSgc4I2QrOK7cMCj9+RZsI5cOpJZOltoTJ0tlax5k2ytkhh076WMlujiAsyrdlxJy+iKBMZzhRDq5zlgznQQT3i5gfL7D3xre48x+mbAgtkBozPnCtOAG+n1xwv0eJ7JVlWdQPUF8h2sEp3POMruO1lh7BNdSPsUespl82nXvQNpp0f5oF45BD2j2GPfsP+efoy7TJbBx+SAXZzmc5z06YwrqLkMdZUE6WKyORv5jZ8j9EGM0qIT4X9qWRwSGJAVmBVZXJPbnGwWkdWFjGISPQI+k3wpI6qr6BpD4XdhT3AbvDOfQNLaBuUeOl+Q4RdCZeze

Alfred-Skyblue avatar Oct 17 '23 09:10 Alfred-Skyblue

It should not be shared with template syntax, right?

<script setup lang='jsx'>
  import { ref } from 'vue'
  const msg = ref('Hello World!')
  defineRender(<p>{msg.value}</p>)
</script>
<template>
  <p>{msg.value}</p>
</template>

It should be the same as the render function, with the template as the main:

https://play.vuejs.org/#eNp9Uk9PgzAU/yq1F2ZC4LDbRBI1S9SDGjXx0guBB+ssbdOWSbLw3X0tg22J7kBof39efn3v7emd1smuA7qimS0N145YcJ3OmeStVsaRPTFQk4HURrUkQmk0Uw+q1Qc8Sf3FV0KaySwdi2EZvDhotSgc4I2QrOK7cMCj9+RZsI5cOpJZOltoTJ0tlax5k2ytkhh076WMlujiAsyrdlxJy+iKBMZzhRDq5zlgznQQT3i5gfL7D3xre48x+mbAgtkBozPnCtOAG+n1xwv0eJ7JVlWdQPUF8h2sEp3POMruO1lh7BNdSPsUespl82nXvQNpp0f5oF45BD2j2GPfsP+efoy7TJbBx+SAXZzmc5z06YwrqLkMdZUE6WKyORv5jZ8j9EGM0qIT4X9qWRwSGJAVmBVZXJPbnGwWkdWFjGISPQI+k3wpI6qr6BpD4XdhT3AbvDOfQNLaBuUeOl+Q4RdCZeze

It should be like this, its behavior should be consistent with the option api situation

baiwusanyu-c avatar Oct 17 '23 09:10 baiwusanyu-c

I disagree if think we should at the very least alert on both cases, there should not be multiple render functions, because one will never be called. That's more likely to be a bug than intentional.

pikax avatar Oct 17 '23 10:10 pikax

Synchronous discussion: The implementation of defineRender is to compile jsx content into the return value of the setup function, which is different from what vue's existing jsx plugin does (for example, @vitejs/plugin-vue-jsx is to convert jsx into a runtime rendering function code). Therefore, when defineRender and template exist at the same time, their priority issues cannot be discussed together with @vitejs/plugin-vue-jsx. @sxzz Setting defineRender to have higher priority than template has its trade-offs. At the same time, I also agree with @pikax’s view. When defineRender and template exist at the same time, the compiler reports an error, which can regulate user behavior and avoid potential problems.

baiwusanyu-c avatar Oct 17 '23 16:10 baiwusanyu-c

Actually, JSX is not within the scope of defineRender. Whatever using or not defineRender, the order of compilation is transforming Vue SFC into JavaScript (or JSX) code (@vitejs/plugin-vue), then transform JSX into JS (@vitejs/plugin-vue-jsx).

sxzz avatar Oct 18 '23 07:10 sxzz

Now an error will be threw when defineRender() used with <template>

sxzz avatar Oct 18 '23 07:10 sxzz

Should it go through RFC first?

I would say if defineRender is used the compiler should throw an error if <template> (the render template) is present

@pikax

  • Now The macro cannot be used with <template>.
  • The RFC has been written, PTAL https://github.com/vuejs/rfcs/discussions/585

sxzz avatar Oct 19 '23 07:10 sxzz