请教一个 vue3 自定义组件传 h 函数问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
weixiaoD
V2EX    Vue.js

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

  •  
  •   weixiaoD 2024-04-29 20:13:43 +08:00 2380 次点击

    我是用 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 条附言    2024-04-30 00:16:58 +08:00

    可能是我表达不够清晰,主要的想法就是想让父组件传递一个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> 
    renmu
        1
    renmu  
       2024-04-29 20:17:05 +08:00 via Android
    warning 不是 error ,能跑就行
    weixiaoD
        2
    weixiaoD  
    OP
       2024-04-29 21:01:03 +08:00
    @renmu 不可以,因为有几千条数据,有多少条他就报多少个,然后整个页面就卡死了
    alvinbone88
        3
    alvinbone88  
       2024-04-29 21:38:53 +08:00
    报错的地方和 icon 无关,是 NText 有问题,传 slot 的时候用的不是函数
    还有,都用 h 函数了,为什么不直接用 jsx 呢
    RabbitDR
        4
    RabbitDR  
       2024-04-29 21:46:41 +08:00   1
    有点怪,为啥要自己写 vnode ,完全可以弄个 jsx 。
    关于你的问题:
    首先,你定义的 icon 不是一个函数,h 函数返回一个 vnode ,所以你的 icon 是一个 vnode (一个对象)。
    其次,h 函数可以接受一个 string ,或者一个 Children ,而 Slot 必须是一个返回 Children 的函数。
    综上,你可以在 suffix slot 里这样写
    suffix: () => h(NIcon, { default: () => item.icon })
    lisongeee
        5
    lisongeee  
       2024-04-29 21:57:33 +08:00
    看起来用的 naiveui ,框架挺好用的,就是它那个 B 文档示例有 jsx 不用偏要用 h 函数

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

    我刚学的时候搞得我以为只能传递手写 h 函数整的代码密密麻麻的看得累死我了
    leokun
        6
    leokun  
       2024-04-29 23:45:45 +08:00
    weixiaoD
        7
    weixiaoD  
    OP
       2024-04-30 00:19:33 +08:00
    @lisongeee 哈哈哈, 我就是看他那个示例代码里, 都是用 h 函数实现的,刚好我又能理解这个逻辑,所以就也跟着他去写了,你看下我附加的代码内容, 浏览器 console 有警告, 但我找不出问题在哪里
    weixiaoD
        8
    weixiaoD  
    OP
       2024-04-30 00:20:43 +08:00
    @alvinbone88
    @RabbitDR
    @lisongeee 我附加里的代码内容可以帮忙看看是哪里的问题吗?
    alvinbone88
        9
    alvinbone88  
       2024-04-30 00:44:51 +08:00
    weixiaoD
        10
    weixiaoD  
    OP
       2024-04-30 10:07:11 +08:00
    @alvinbone88 好像确实是这个问题,现在我改了函数返回,但是还有有一个 warning, 我找不出是哪部分代码发出的
    ![6cedc03957a7eedee9bc5.png]( https://i.9m.pw/file/6cedc03957a7eedee9bc5.png)
    ![80ca7e1bf21bc359e13e3.png]( https://i.9m.pw/file/80ca7e1bf21bc359e13e3.png)
    alvinbone88
        11
    alvinbone88  
       2024-04-30 10:31:38 +08:00
    NList 的 slot 还是数组
    weixiaoD
        12
    weixiaoD  
    OP
       2024-04-30 12:13:13 +08:00
    @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  
       2024-04-30 15:05:33 +08:00   1
    computed 的使用方式不对
    https://vuejs.org/api/reactivity-core.html#computed
    这里应该是 isSelected.value(item)
    weixiaoD
        14
    weixiaoD  
    OP
       2024-04-30 16:02:59 +08:00
    @alvinbone88 这么尴尬,忘记+value 了,哈哈,粗心了,已经搞定,感谢感谢
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1130 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 23:24 PVG 07:24 LAX 16:24 JFK 19:24
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86