一、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前端网发表,如需转载,请注明页面地址。
code前端网



