vueuse的createReusableTemplate函数实现原理

0 阅读3分钟

使用vueuse的时候发现里面有一个createReusableTemplate的工具函数很有意思

根据文档的介绍,它的使用方式如下:

开发中也许会经常遇到这种情况

<template>
  <dialog v-if="showInDialog">
    <!-- something complex -->
  </dialog>
  <div v-else>
    <!-- something complex -->
  </div>
</template>

我们希望代码尽可能的重用,通常情况下我们会将<!-- something complex -->抽取成组件,但是那样做,在抽取出去的组件中,你就没法访问到当前SFC中的各种变量,而只能通过定义propsemits来实现数据传递,这样会有些繁琐

createReuseableTemplate能够在当前组件的template中直接定义一个可重用的片段,就像这样:

<script setup>
import { createReusableTemplate } from '@vueuse/core'

const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
</script>

<template>
  <DefineTemplate>
    <!-- something complex -->
  </DefineTemplate>

  <dialog v-if="showInDialog">
    <ReuseTemplate />
  </dialog>
  <div v-else>
    <ReuseTemplate />
  </div>
</template>

在这个示例中,<DefineTemplate>中写的代码将不会渲染任何东西,而会渲染在<ReuseTemplate />

不禁惊叹这是什么黑科技,迫切想知道是怎么实现的,于是研究了一下源码

其实实现原理很简单,稍微拆分一下它的代码并手动实现如下:

<script setup>
import { defineComponent } from 'vue';
import { ref } from 'vue'

const msg = ref('Hello World!')

// 首先是可重用的template的定义
// 其实它就是一个Component
const DefineTmpl = defineComponent({
  setup(_, { slots }) {
    // 拿到默认的插槽,console.log打印一下就能看出来,这玩意实际上就是一个渲染函数
    // 而setup方法,是可以返回一个渲染函数的(也就是一个能返回vnode的函数),具体可看vue官方文档:
    // https://cn.vuejs.org/guide/extras/render-function.html#declaring-render-function
    return () => slots.default()
    // 这里甚至可以直接return默认插槽,因为它本身就是一个函数
    // return slots.default
  }
})
</script>

<template>
  <DefineTmpl>
    <h1>{{ msg }}</h1>
    <input v-model="msg" />
  </DefineTmpl>
</template>

以上代码的结果就是:你在DefineTmpl写的任何东西(其实就是默认插槽的内容)都会被原样渲染出来

1.png

其实也就是跟不存在DefineTmpl几乎没有区别:

<script setup>
import { defineComponent } from 'vue';
import { ref } from 'vue'

const msg = ref('Hello World!')
</script>

<template>
  <h1>{{ msg }}</h1>
  <input v-model="msg" />
</template>

那么DefineTmpl存在的意义其实就是拿到默认插槽的内容(也就是你在DefineTmpl里写的任何html),它是一个渲染函数,那就用一个变量来接收它

然后该怎么重用它呢??也就是再定义一个Component,返回DefineTmpl里获取到的默认插槽内容就行了:

<script setup>
import { defineComponent } from 'vue';
import { ref } from 'vue'

const msg = ref('Hello World!')

let renderFn;
const DefineTmpl = defineComponent({
  setup(_, { slots }) {
    renderFn = slots.default
  }
})

const Reuse = defineComponent({
  setup() {
    if (!renderFn) return console.warn('未定义模板!')
    return renderFn
    // 或者 return () => renderFn()
  }
})
</script>

<template>
  <p>DefineTmpl部分(不会渲染任何东西)</p>
  <DefineTmpl>
    <h1>{{ msg }}</h1>
    <input v-model="msg" />
  </DefineTmpl>
  <h2>================分割线================</h2>
  <Reuse />
</template>

然后在页面任意地方写上就可以了,渲染结果:

2.png

如果想传入一些自定义的东西怎么办呢,稍微改造一下Reuse:

const Reuse = defineComponent({
  setup(_, { attrs }) {
    if (!renderFn) {
      console.warn('未定义模板')
    }
    return () => renderFn({ ...attrs }) // 将attrs作为渲染函数的props传入
  }
})

然后在DefineTmpl部分可以这样写:

<template>
  <p>DefineTmpl部分(不会渲染任何东西)</p>
  <DefineTmpl #default="{ data }">
    <h1>{{ msg }}</h1>
    <input v-model="msg" />
    <p>自定义内容:{{ data }}</p>
  </DefineTmpl>

  <h2>================分割线================</h2>

  <Reuse :data="'我是自定义内容'" />
</template>

渲染结果:

3.png

整个封装一下,就是一个简陋版本的createReusableTemplate了(当然比vueuse中简陋很多,不过差不多核心实现原理也就是这样了)

function createMyReusableTemplate() {
  let renderFn;
  const DefineTmpl = defineComponent({
    setup(_, { slots }) {
      renderFn = slots.default
    }
  })

  const Reuse = defineComponent({
    setup(_, { attrs }) {
      if (!renderFn) {
        console.warn('未定义模板')
      }
      return () => renderFn({ ...attrs })
    }
  })
  
  return [DefineTmpl, Reuse]
}