一、Vue2里JSX和template的slot核心区别是啥?
p>咱做Vue2项目时,用JSX写组件插槽(slot)总容易懵?要么插槽不显示,要么作用域传值不对,甚至具名插槽混得乱七八糟…其实JSX里的slot和template写法差异不小,今天从区别、基础用法、实战场景、避坑点四个维度唠明白,以后写JSX插槽再也不踩雷~
先看template的slot:靠<slot>
标签、name
属性、v-slot
指令实现,是「声明式写法」——你告诉Vue“这里要插内容”,Vue解析模板时自动处理插槽分发,比如子组件写<slot name="header"></slot>
,父组件用<template #header>...</template>
,逻辑全藏在Vue的模板解析规则里。
再看JSX的slot:JSX本质是h
函数的语法糖,插槽得靠this.$slots
(匿名/具名插槽)、this.$scopedSlots
(作用域插槽)主动“抓取”内容,父组件传插槽时要通过编程式逻辑控制(比如传函数、加slot
属性),可以理解为:JSX把“声明式插内容”变成了“用JS逻辑主动传、主动渲染”。
举个直观对比:
子组件(template vs JSX)
-
template版本:
<template> <div> <slot name="header"></slot> <!-- 具名插槽 --> <slot :msg="msg"></slot> <!-- 作用域插槽 --> </div> </template> <script> export default { data() { return { msg: 'hi' } } } </script>
-
JSX版本(用render函数写JSX):
export default { data() { return { msg: 'hi' } }, render(h) { return ( <div> {this.$slots.header} {/* 对应template的<slot name="header"> */} {this.$scopedSlots.default({ msg: this.msg })} {/* 对应template的<slot :msg="msg"> */} </div> ); } };
父组件(template vs JSX)
-
template版本:
<MyComponent> <template #header>头部</template> <!-- 传具名插槽 --> <template #default="scope">{{ scope.msg }}</template> <!-- 传作用域插槽 --> </MyComponent>
-
JSX版本(用render函数写JSX):
render(h) { return ( <MyComponent> <div slot="header">头部</div> {/* 传具名插槽,用slot属性 */} {({ msg }) => <div>{msg}</div>} {/* 传作用域插槽,用函数接收数据 */} </MyComponent> ); }
总结区别:template是“声明规则让Vue自动处理”,JSX是“用JS主动控制插槽的传和渲染”,语法和逻辑更偏向JavaScript。
JSX里写slot分几步?匿名、具名、作用域全拆解
JSX写插槽,核心是「子组件主动渲染插槽 + 父组件主动传插槽内容」,下面分三种插槽类型拆解步骤:
匿名插槽(默认插槽)怎么玩?
子组件:在JSX里用this.$slots.default
渲染默认插槽内容,如果没写这行,父组件传的默认内容不会显示(和template逻辑一致,得主动渲染)。
父组件:直接把内容当“子元素”传给子组件,不需要额外标记。
举个例子:
-
子组件
DefaultSlot.jsx
:export default { render(h) { return ( <div class="card"> <h2>组件自身标题</h2> {this.$slots.default} {/* 渲染父组件传的默认内容 */} </div> ); } };
-
父组件调用:
<DefaultSlot> <p>这是父组件传给默认插槽的内容</p> <button>还能传任意HTML结构</button> </DefaultSlot>
渲染后结构:
<div class="card"> <h2>组件自身标题</h2> <p>这是父组件传给默认插槽的内容</p> <button>还能传任意HTML结构</button> </div>
具名插槽咋搞?
子组件:用this.$slots['插槽名']
渲染指定具名插槽(比如this.$slots.header
)。
父组件:给要传的内容加slot="插槽名"
属性,告诉子组件“这段内容对应哪个插槽”。
举个完整例子:
-
子组件
HeaderSlot.jsx
(拆分头部、主体插槽):export default { render(h) { return ( <div class="card"> <header>{this.$slots.header}</header> {/* 渲染header插槽 */} <main>{this.$slots.default}</main> {/* 渲染默认插槽 */} </div> ); } };
-
父组件调用:
<HeaderSlot> <div slot="header"> <h1>这是页面大标题</h1> <p>副标题也能塞这儿</p> </div> <p>这是主体内容,属于默认插槽</p> <button>甚至可以混着传</button> </HeaderSlot>
渲染后结构:
<div class="card"> <header> <h1>这是页面大标题</h1> <p>副标题也能塞这儿</p> </header> <main> <p>这是主体内容,属于默认插槽</p> <button>甚至可以混着传</button> </main> </div>
作用域插槽(带数据的插槽)咋实现?
子组件:用this.$scopedSlots['插槽名'](要传的数据)
,把数据包成对象传给父组件,比如传时间:this.$scopedSlots.footer({ time: new Date() })
。
父组件:传一个函数作为子元素,函数的参数就是子组件传过来的数据,比如{({ time }) => <div>时间:{time}</div>}
。
举个例子(子组件给父组件传当前时间):
-
子组件
TimeSlot.jsx
:export default { render(h) { return ( <div class="time-card"> {this.$scopedSlots.footer({ time: new Date() })} </div> ); } };
-
父组件调用:
<TimeSlot> {({ time }) => ( <div class="footer"> 最新更新时间:{time.toLocaleTimeString()} </div> )} </TimeSlot>
渲染后,父组件能拿到子组件的时间数据,动态显示:
<div class="time-card"> <div class="footer">最新更新时间:15:23:45</div> </div>
哪些场景下JSX写slot更顺手?
JSX的“编程式”特性,让它在这些场景下比template更灵活高效:
需要复杂逻辑判断
如果父组件传插槽时,要根据权限、状态等写一堆if/else
、循环,JSX的JavaScript语法比template的v-if/v-for
更直观。
根据用户角色显示不同操作栏”:
<ActionSlot> {() => { if (user.role === 'admin') { return ( <div> <button>删除</button> <button>编辑</button> </div> ); } else { return <button>仅查看</button>; } }} </ActionSlot>
子组件ActionSlot.jsx
只需渲染this.$scopedSlots.default()
,逻辑全在父组件JSX里,清晰又好维护。
封装高度可定制的通用组件
比如写表格组件<MyTable>
,列的渲染、操作按钮全靠插槽,用JSX可以动态生成列的插槽内容(比如循环数组渲染多列),比template写一堆<template #column>
更高效。
伪代码示例:
<MyTable :data="tableData"> {columns.map((col, idx) => ( <template slot={col.key}> {/* 这里用JS逻辑定制每列的渲染 */} {({ row }) => <span>{row[col.key]}</span>} </template> ))} </MyTable>
函数式组件配合JSX
Vue2的函数式组件(functional: true
)本身靠render
函数运行,用JSX写插槽天然契合——不需要this
上下文,传参更直接。
函数式组件示例:
const FunctionalCard = { functional: true, render(h, context) { return ( <div class="func-card"> {context.slots().header} {/* 取具名插槽header */} {context.scopedSlots.default({ msg: 'func msg' })} {/* 传作用域数据 */} </div> ); } }; // 父组件调用 <FunctionalCard> <div slot="header">函数式组件的头部</div> {({ msg }) => <div>{msg}</div>} </FunctionalCard>
JSX写slot最容易踩的3个坑,避坑指南来了!
作用域插槽“拿不到数据”?检查参数和传值
坑点:父组件函数的参数名和子组件传的不一致,或者子组件没调用this.$scopedSlots
。
比如子组件传{ list: data }
,父组件写成({ data }) => ...
,参数名对不上就拿空值。
解决:子组件和父组件约定好传参的key
,比如子组件固定传{ item: ... }
,父组件用({ item }) => ...
接收。
具名插槽“不显示”?检查插槽名和渲染逻辑
坑点:父组件用了v-slot:header
(template写法)但JSX里得用slot="header"
;或者子组件渲染this.$slots.header
,父组件传slot="head"
(名字对不上)。
解决:统一插槽名!子组件this.$slots['xxx']
和父组件slot="xxx"
的xxx
必须完全一致。
“不响应式”?注意数据绑定
坑点:JSX里写插槽时,用了非响应式数据(比如普通变量、闭包变量),数据更新后插槽不刷新。
比如错误写法:
export default { render(h) { let count = 0; setInterval(() => count++, 1000); // count不是响应式的 return ( <MyComponent> {() => <div>{count}</div>} </MyComponent> ); } };
解决:把数据放到data
、计算属性或ref
里(确保响应式),比如改成:
export default { data() { return { count: 0 } }, mounted() { setInterval(() => this.count++, 1000); }, render(h) { return ( <MyComponent> {() => <div>{this.count}</div>} </MyComponent> ); } };
JSX写slot的核心逻辑
记住这张“流程表”:
- 子组件要渲染插槽 → 用
this.$slots
(匿名/具名)或this.$scopedSlots
(作用域)主动抓取; - 父组件传内容 → 匿名插槽直接写子元素,具名插槽加
slot
属性,作用域插槽传函数并接收参数; - 避坑关键 → 统一插槽名、确保数据响应式、约定作用域传参的
key
。
其实JSX里的slot没那么难,核心是理解“编程式控制插槽”和“template声明式”的区别,再结合场景练几个例子,自然就通了~下次写Vue2 JSX组件,插槽再也不慌啦~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。