前言
插槽
看着是一个比较神秘的东西,特别是作用域插槽
还能让我们在父组件里面直接访问子组件里面的数据,这让插槽变得更加神秘了。其实Vue3的插槽远比你想象的简单
,这篇文章我们来揭开插槽的神秘面纱。
欧阳也在找工作,坐标成都求内推!
看个demo
我们先来看个常见的插槽demo,其中子组件代码如下:
<template><slot></slot><slot name="header"></slot><slot name="footer" :desc="desc"></slot>
</template><script setup>
import { ref } from "vue";
const desc = ref("footer desc");
</script>
在子组件中我们定义了三个插槽,第一个是默认插槽,第二个是name为header
的插槽,第三个是name为footer
的插槽,并且将desc
变量传递给了父组件。
我们再来看看父组件代码如下:
<template><ChildDemo><p>default slot</p><template v-slot:header><p>header slot</p></template><template v-slot:footer="{ desc }"><p>footer slot: {{ desc }}</p></template></ChildDemo>
</template><script setup lang="ts">
import ChildDemo from "./child.vue";
</script>
在父组件中的代码很常规,分别使用v-slot
指令给header
和footer
插槽传递内容。
来看看编译后的父组件
我们在浏览器中来看看编译后的父组件代码,简化后如下:
import {createBlock as _createBlock,createElementVNode as _createElementVNode,openBlock as _openBlock,toDisplayString as _toDisplayString,withCtx as _withCtx,
} from "/node_modules/.vite/deps/vue.js?v=64ab5d5e";const _sfc_main = /* @__PURE__ */ _defineComponent({// ...省略
});function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(),_createBlock($setup["ChildDemo"], null, {header: _withCtx(() =>_cache[0] ||(_cache[0] = [_createElementVNode("p",null,"header slot",-1/* HOISTED */),])),footer: _withCtx(({ desc }) => [_createElementVNode("p",null,"footer slot: " + _toDisplayString(desc),1/* TEXT */),]),default: _withCtx(() => [_cache[1] ||(_cache[1] = _createElementVNode("p",null,"default slot",-1/* HOISTED */)),]),_: 1,/* STABLE */}));
}
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render],
]);
从上面的代码可以看到template中的代码编译后变成了render函数。
在render函数中_createBlock($setup["ChildDemo"]
表示在渲染子组件ChildDemo
,并且在执行createBlock
函数时传入了第三个参数是一个对象。对象中包含header
、footer
、default
三个方法,这三个方法对应的是子组件
ChildDemo`中的三个插槽。执行这三个方法就会生成这三个插槽对应的虚拟DOM。
并且我们观察到插槽footer
处的方法还接收一个对象作为参数,并且对象中还有一个desc
字段,这个字段就是子组件传递给父组件的变量。
方法最外层的withCtx
方法是为了给插槽的方法注入当前组件实例的上下文。
通过上面的分析我们可以得出一个结论:在父组件中插槽经过编译后会变成一堆由插槽name组成的方法,执行这些方法就会生成插槽对应的虚拟DOM。默认插槽就是default方法,方法接收的参数就是子组件中插槽给父组件传递的变量
。但是有一点要注意,在父组件中我们只是定义了这三个方法,执行这三个方法的地方却不是在父组件,而是在子组件。
编译后的子组件
我们来看看编译后的子组件,简化后代码如下:
import {createElementBlock as _createElementBlock,Fragment as _Fragment,openBlock as _openBlock,renderSlot as _renderSlot,
} from "/node_modules/.vite/deps/vue.js?v=64ab5d5e";const _sfc_main = {// ...省略
};function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(),_createElementBlock(_Fragment,null,[_renderSlot(_ctx.$slots, "default"),_renderSlot(_ctx.$slots, "header"),_renderSlot(_ctx.$slots, "footer", { desc: $setup.desc }),],64 /* STABLE_FRAGMENT */));
}export default /*#__PURE__*/ _export_sfc(_sfc_main, [["render", _sfc_render]]);
同样的我们观察里面的render函数,里面的这个:
[_renderSlot(_ctx.$slots, "default"),_renderSlot(_ctx.$slots, "header"),_renderSlot(_ctx.$slots, "footer", { desc: $setup.desc }),
]
对应的就是源代码里面的这个:
<slot></slot>
<slot name="header"></slot>
<slot name="footer" :desc="desc"></slot>
在上面我们看见一个$slots
对象,这个是什么东西呢?
其实useSlots
就是返回的$slots
对象,我们直接在控制台中使用useSlots
打印出$slots
对象看看,代码如下:
<script setup>
import { ref, useSlots } from "vue";const slots = useSlots();
console.log(slots);
const desc = ref("footer desc");
</script>
我们来浏览器中看看此时打印的slots
对象是什么样的,如下图:
从上图中可以看到slots对象好像有点熟悉,这个对象中包含default
、footer
、header
这三个方法,其实这个slots对象就是前面我们讲的父组件中定义的那个对象,执行对象的default
、footer
、header
方法就会生成对应插槽的虚拟DOM。
前面我们讲了在父组件中会定义default
、footer
、header
这三个方法,那这三个方法又是在哪里执行的呢?
答案是:在子组件里面执行的。
在执行_renderSlot(_ctx.$slots, "default")
方法时就会去执行slots
对象里面的default
方法,这个是renderSlot
函数的代码截图:
从上图中可以看到在renderSlot
函数中首先会使用slots[name]
拿到对应的插槽方法,如果执行的是_renderSlot(_ctx.$slots, "footer", { desc: $setup.desc })
,这里拿到的就是footer
方法。
然后就是执行footer
方法,前面我们讲过了这里的footer
方法需要接收参数,并且从参数中结构出desc
属性。刚好我们执行renderSlot
方法时就给他传了一个对象,对象中就有一个desc
属性,这不就对上了吗!
并且由于执行footer
方法会生成虚拟DOM,所以footer生成的虚拟DOM是属于子组件里面的,同理footer对应的真实DOM也是属于在子组件的DOM树里面。
通过上面的分析我们可以得出一个结论就是:子组件中的插槽实际就是在执行父组件插槽对应的方法,在执行方法时可以将子组件的变量传递给父组件,这就是作用域插槽的原理。
总结
这篇文章我们讲了经过编译后父组件的插槽会被编译成一堆方法,这些方法组成的对象就是$slots
对象。在子组件中会去执行这些方法,并且可以将子组件的变量传给父组件,由父组件去接收参数,这就是作用域插槽
的原理。了解了这个后当我们在useSlots
、jsx
、tsx
中定义和使用插槽就不会那么迷茫了。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会