V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
weixiaoD
V2EX  ›  Vue.js

请教一个 vue3 自定义组件传 h 函数问题

  •  
  •   weixiaoD · 210 天前 · 1679 次点击
    这是一个创建于 210 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我是用 naiveUI 的 datatable 时自己写一个过滤菜单样式,我目前可以按照预期运行,传入 title 还有 value 还有图标的 src 链接. 格式

    [{title:'标签名', value:'test', imgSrc='1.jpg'}]
    

    现在我要允许传递一个 h 函数进去,像这样

    [{title:'标签名', value:'test', imgSrc:'1.jpg', icon:h('div',null,'我是图标')}]
    

    但是我不知道这个子组件接收到之后怎么把这个 icon 函数放进去 li 标签里面,直接在 h 函数里把 NList 标签用 h 函数来写,像下面的方法,但是他会报错,浏览器全都是下面的报错,我想问下这个要怎么处理才比较好一点

    chunk-6SSRW7KQ.js?v=5c5b974b:1543 [Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance. 
      at <Text> 
      at <ListItem key="peersGettingFromUs" onClick=fn<onClick> class="padding" > 
      at <List hoverable=true clickable=true showDivider=false > 
      at <List> 
      at <ResizeObserver onResize=fn<handleContentResize> > 
      at <ResizeObserver onResize=fn<handleContainerResize> > 
      at <Scrollbar ref="scrollbarInstRef" xScrollable=false theme=undefined  ... > 
      at <Scrollbar style= Object > 
      at <Menu options= Array(20) selected= Array(4) onUpdate:modelValue=fn<onUpdate:modelValue> > 
      at <DropdownRenderOption tmNode= Object key="header" > 
      at <DropdownMenu ref=fn class="n-popover-shared n-dropdown" clsPrefix="n"  ... > 
      at <BaseTransition onEnter=fn onAfterLeave=fn<onAfterLeave> appear=true  ... > 
      at <Transition name="popover-transition" appear=true onEnter=fn<onEnter>  ... > 
      at <LazyTeleport show=true to="body" disabled=false > 
      at <Follower ref="followerRef" zIndex=undefined show=true  ... > 
      at <PopoverBody theme= Object themeOverrides=undefined builtinThemeOverrides=undefined  ... > 
      at <Binder ref="binderInstRef" syncTarget=true syncTargetWithParent=false > 
      at <Popover show=true defaultShow=false showArrow=false  ... > 
      at <Dropdown trigger="hover" options= Array(1) > 
      at <Flex> 
      at <HelloWorld> 
      at <App>
    
    const mylist = () => {
      return h(NList, {
        hoverable: true,
        clickable: true,
        showDivider: false,
      },
        props.options.map((item) => {
          return h(NListItem,
            {
              key: item.title,
              onClick: () => itemClick(item),
              class: 'padding',
            }, {
            prefix: () => {
              return h(NAvatar, {
                size: 22,
                src: item.imgSrc,
                color: 'white',
                bordered: true
              })
            },
            default: () => {
              return h(NText, null, item.title)
            },
            suffix: () => {
              return h(NIcon,
                {
                  // color: isSelected(item) ? '#1abc9c' : '#bdc3c7'
                  size: 20
                }, {
                default: () => {
                  return h(CheckmarkDoneCircle, null)
                }
              }
              )
            }
          })
        })
      )
    }
    

    6af9793f76eb2517d42cd.png

    <template>
      <n-scrollbar style="max-height: 600px">
        <!-- <mylist /> -->
        <n-list hoverable clickable :show-divider="false">
          <n-list-item v-for="item in options" @click="itemClick(item)" :class="padding">
            <template #prefix v-if="item.imgSrc">
              <n-avatar :size="22" :src="item.imgSrc" color="white" :bordered="true" />
            </template>
            {{ item.title ? item.title : empty }}
            <template #suffix>
              <n-icon :color="isSelected(item) ? '#1abc9c' : '#bdc3c7'" size="20">
                <CheckmarkDoneCircle />
              </n-icon>
            </template>
          </n-list-item>
        </n-list>
      </n-scrollbar>
      <n-button-group style="width:100%" size="large" :class="btn_width">
        <n-button secondary @click="contrary" :disabled="isAll || isEmpty" v-if="contrary_btn">
          <template #icon>
            <n-icon :color="isEmpty ? 'black' : '#00b894'" class="rotate"
              :style="{ transform: 'rotate(' + target.contrary + 'deg)' }">
              <RepeatSharp />
            </n-icon>
          </template>
          反选
        </n-button>
        <n-button secondary @click="_submit" :style="'width:' + 1 / 3">
          <template #icon>
            <n-icon>
              <SaveOutline />
            </n-icon>
          </template>
          保存
        </n-button>
        <n-button secondary @click="clear" :disabled="isEmpty" :style="'width:' + 1 / 3">
          <template #icon>
            <n-icon class="rotate" :color="isEmpty ? 'black' : '#e74c3c'">
              <TrashBinOutline />
            </n-icon>
          </template>
          清空
        </n-button>
      </n-button-group>
    </template>
    <script lang="ts" setup>
    import { computed, h, reactive, ref } from 'vue'
    import { CheckmarkDoneCircle, TrashBinOutline, RepeatSharp, SaveOutline } from '@vicons/ionicons5'
    import { NAvatar, NIcon, NList, NListItem, NText } from 'naive-ui';
    const props = defineProps({
      selected: { type: Array<string | boolean>, required: true },
      options: { type: Array<menu>, required: true },
      padding: { type: String, required: false, default: 'small' },
      contrary_btn: { type: Boolean, required: false, default: true },
      // 当 title 为空字符串时显示的文本
      empty: { type: String, required: false, default: '(empty)' },
      renderIcon: { type: Function, required: false },
    })
    // 临时数组,用于保存用户已选中但是还未点击提交按钮的勾选值
    const tmp_selected = ref<Array<string | boolean>>([...props.selected])
    let btn_width = 'two'
    if (props.contrary_btn) {
      btn_width = 'there'
    }
    // 从传入的选项菜单里提取所有 value
    const all = props.options.map(item => item.value)
    const emit = defineEmits(['update:modelValue']);
    interface menu {
      title: string
      value: string | boolean
      desc?: string
      imgSrc?: string
    }
    const itemClick = (item: menu) => {
      if (isSelected.value(item)) {
        tmp_selected.value = tmp_selected.value.filter(o => o !== item.value)
      } else {
        tmp_selected.value.push(item.value)
      }
    }
    const _submit = () => {
      emit('update:modelValue', [...tmp_selected.value]);
    }
    const target = reactive({
      contrary: 0
    })
    // 反选函数
    const contrary = () => {
      target.contrary += 180;
      tmp_selected.value = all.filter(item => !tmp_selected.value.includes(item))
      console.log(tmp_selected.value)
    }
    // 清空函数
    const clear = () => {
      target.contrary = 0;
      tmp_selected.value = []
      emit('update:modelValue', [])
    }
    const isSelected = computed(() => {
      return (item: menu) => {
        return tmp_selected.value.some(o => o === item.value);
      };
    });
    const isAll = computed(() => {
      return tmp_selected.value.length === props.options.length
    })
    const isEmpty = computed(() => {
      return tmp_selected.value.length === 0
    })
    
    </script>
    <style scoped>
    .two button {
      width: 50%;
    }
    
    .there button {
      width: 33.33%;
    }
    
    .n-list.n-list--hoverable .n-list-item.small {
      padding: 7px 20px;
    }
    
    .n-list.n-list--hoverable .n-list-item.medium {
      padding: 10px 20px;
    }
    
    .n-list.n-list--hoverable .n-list-item.large {
      padding: 15px 20px;
    }
    
    .n-avatar {
      vertical-align: middle;
    }
    
    .select {
      background-color: aquamarine;
    }
    
    .rotate {
      transition: all 0.5s ease;
    }
    
    .rotated {
      transform: rotate(180deg);
    }
    </style>
    
    第 1 条附言  ·  210 天前

    可能是我表达不够清晰,主要的想法就是想让父组件传递一个Vnode进去给子组件去渲染, 但是他会在console里出现警告,而且会非常卡顿, 下面是我简化后的代码,大伙帮忙看看是哪里的问题.

    至于为啥不用不用jsx, 因为我也是刚接触vue,而且需要自己定制的不多,就像自己写一个菜单, 所以就没有去学jsx,直接h函数就好了.

    a62ed5f59ce9fbd4d1c7c.png 1e77a5eec4f00e9bfb0f9.png

    父组件:

    <template>
      <test1 :options="data" />
    </template>
    
    <script lang="ts" setup>
    import test1 from './test.vue'
    import { NButton } from 'naive-ui';
    import { h,ref } from 'vue'
    const data = ref([
      { title: 'title1', value: 'value1', icon: () => h(NButton, null, '我是按钮1') },
      { title: 'title2', value: 'value2', icon: () => h(NButton, null, '我是按钮2')},
      { title: 'title3', value: 'value3', icon: () => h(NButton, null, '我是按钮3')},
    ])
    </script>
    

    子组件

    <template>
      <VNode />
    </template>
    <script setup lang="ts">
    import { h, } from 'vue';
    
    import { NAvatar, NList, NListItem, NText } from 'naive-ui';
    const props = defineProps({
      options: { type: Array<menu>, required: true },
    })
    interface menu {
      title: string
      value: string | boolean
      icon: Function
      imgSrc?: string
    }
    const VNode = () => {
      return h(NList, {
        hoverable: true,
        clickable: true,
        showDivider: false,
      },
        props.options.map((item) => {
          return h(NListItem,
            {
              key: item.title,
              class: 'padding',
            }, {
            prefix: () => {
              return h(NAvatar, {
                size: 22,
                src: item.imgSrc,
                color: 'white',
                bordered: true
              })
            },
            default: () => {
              return h(NText, null, item.title)
            },
            suffix: () => {
              return item.icon()
            }
          })
        })
      )
    }
    </script>
    
    14 条回复
    renmu
        1
    renmu  
       210 天前 via Android
    warning 不是 error ,能跑就行🐶
    weixiaoD
        2
    weixiaoD  
    OP
       210 天前
    @renmu 不可以,因为有几千条数据,有多少条他就报多少个,然后整个页面就卡死了
    alvinbone88
        3
    alvinbone88  
       210 天前
    报错的地方和 icon 无关,是 NText 有问题,传 slot 的时候用的不是函数
    还有,都用 h 函数了,为什么不直接用 jsx 呢
    RabbitDR
        4
    RabbitDR  
       210 天前   ❤️ 1
    有点怪,为啥要自己写 vnode ,完全可以弄个 jsx 。
    关于你的问题:
    首先,你定义的 icon 不是一个函数,h 函数返回一个 vnode ,所以你的 icon 是一个 vnode (一个对象)。
    其次,h 函数可以接受一个 string ,或者一个 Children ,而 Slot 必须是一个返回 Children 的函数。
    综上,你可以在 suffix slot 里这样写
    suffix: () => h(NIcon, { default: () => item.icon })
    lisongeee
        5
    lisongeee  
       210 天前
    看起来用的 naiveui ,框架挺好用的,就是它那个 B 文档示例有 jsx 不用偏要用 h 函数

    所以这个框架的初学者基本都会傻乎乎地去手写 h 函数

    我刚学的时候搞得我以为只能传递手写 h 函数整的代码密密麻麻的看得累死我了
    leokun
        6
    leokun  
       210 天前
    weixiaoD
        7
    weixiaoD  
    OP
       210 天前
    @lisongeee 哈哈哈, 我就是看他那个示例代码里, 都是用 h 函数实现的,刚好我又能理解这个逻辑,所以就也跟着他去写了,你看下我附加的代码内容, 浏览器 console 有警告, 但我找不出问题在哪里
    weixiaoD
        8
    weixiaoD  
    OP
       210 天前
    @alvinbone88
    @RabbitDR
    @lisongeee 我附加里的代码内容可以帮忙看看是哪里的问题吗?
    alvinbone88
        9
    alvinbone88  
       210 天前
    weixiaoD
        10
    weixiaoD  
    OP
       210 天前
    @alvinbone88 好像确实是这个问题,现在我改了函数返回,但是还有有一个 warning, 我找不出是哪部分代码发出的
    ![6cedc03957a7eedee9bc5.png]( https://i.9m.pw/file/6cedc03957a7eedee9bc5.png)
    ![80ca7e1bf21bc359e13e3.png]( https://i.9m.pw/file/80ca7e1bf21bc359e13e3.png)
    alvinbone88
        11
    alvinbone88  
       210 天前
    NList 的 slot 还是数组
    weixiaoD
        12
    weixiaoD  
    OP
       210 天前
    @alvinbone88 喔,可以了,谢谢; 还有一个疑问,就是怎么在 h 函数里使用计算属性呢? 我的组件逻辑是这样的, 用 NLi 标签列出按钮,然后通过点击这个按钮把 value 加到数组里, 如果这个 value 已经在数组里的就把他移除掉, 说白了就是一个多选组件, 然后我想他的 icon 颜色 选中就为绿色, 没选就黑色, 这里我用了计算属性来表达
    ```
    const isSelected = computed(() => {
    return (item: menu) => {
    return tmp_selected.value.some(o => o === item.value);
    };
    });

    color: isSelected(item) ? '#1abc9c' : '#bdc3c7',
    ```
    我在 template 里是可以这样表达的
    ```
    <n-icon :color="isEmpty ? 'black' : '#00b894'"></n-icon>
    但是现在我写进去 h 函数里,他不给我这样表达了,有报错,这种有啥好的处理逻辑吗?
    我的想法是先给每一个 item 一个默认 color 属性,然后 icon 的 color:item.color, 最后通过 button click 事件去控制这个 item.color 的值, 不过我还是想学一下计算属性的方法, 不知道可以实现不?

    ![62ed599d648f00bcc97c3.png]( https://i.9m.pw/file/62ed599d648f00bcc97c3.png)
    alvinbone88
        13
    alvinbone88  
       210 天前   ❤️ 1
    computed 的使用方式不对
    https://vuejs.org/api/reactivity-core.html#computed
    这里应该是 isSelected.value(item)
    weixiaoD
        14
    weixiaoD  
    OP
       210 天前
    @alvinbone88 这么尴尬,忘记+value 了,哈哈,粗心了,已经搞定,感谢感谢
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5320 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 08:32 · PVG 16:32 · LAX 00:32 · JFK 03:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.