只爱二锅头 發表於 2025-3-5 09:35:00

花3分钟来了解一下Vue3中的插槽到底是什么玩意

<h1 id="前言">前言</h1>
<p><code>插槽</code>看着是一个比较神秘的东西,特别是<code>作用域插槽</code>还能让我们在父组件里面直接访问子组件里面的数据,这让插槽变得更加神秘了。<code>其实Vue3的插槽远比你想象的简单</code>,这篇文章我们来揭开插槽的神秘面纱。</p>
<p><strong>欧阳也在找工作,坐标成都求内推!</strong></p>
<h1 id="看个demo">看个demo</h1>
<p>我们先来看个常见的插槽demo,其中子组件代码如下:</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;slot&gt;&lt;/slot&gt;
&lt;slot name="header"&gt;&lt;/slot&gt;
&lt;slot name="footer" :desc="desc"&gt;&lt;/slot&gt;
&lt;/template&gt;

&lt;script setup&gt;
import { ref } from "vue";
const desc = ref("footer desc");
&lt;/script&gt;
</code></pre>
<p>在子组件中我们定义了三个插槽,第一个是默认插槽,第二个是name为<code>header</code>的插槽,第三个是name为<code>footer</code>的插槽,并且将<code>desc</code>变量传递给了父组件。</p>
<p>我们再来看看父组件代码如下:</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;ChildDemo&gt;
    &lt;p&gt;default slot&lt;/p&gt;
    &lt;template v-slot:header&gt;
      &lt;p&gt;header slot&lt;/p&gt;
    &lt;/template&gt;
    &lt;template v-slot:footer="{ desc }"&gt;
      &lt;p&gt;footer slot: {{ desc }}&lt;/p&gt;
    &lt;/template&gt;
&lt;/ChildDemo&gt;
&lt;/template&gt;

&lt;script setup lang="ts"&gt;
import ChildDemo from "./child.vue";
&lt;/script&gt;
</code></pre>
<p>在父组件中的代码很常规,分别使用<code>v-slot</code>指令给<code>header</code>和<code>footer</code>插槽传递内容。</p>
<h1 id="来看看编译后的父组件">来看看编译后的父组件</h1>
<p>我们在浏览器中来看看编译后的父组件代码,简化后如下:</p>
<pre><code class="language-javascript">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(
      () =&gt;
          _cache ||
          (_cache = [
            _createElementVNode(
            "p",
            null,
            "header slot",
            -1
            /* HOISTED */
            ),
          ])
      ),
      footer: _withCtx(({ desc }) =&gt; [
      _createElementVNode(
          "p",
          null,
          "footer slot: " + _toDisplayString(desc),
          1
          /* TEXT */
      ),
      ]),
      default: _withCtx(() =&gt; [
      _cache ||
          (_cache = _createElementVNode(
            "p",
            null,
            "default slot",
            -1
            /* HOISTED */
          )),
      ]),
      _: 1,
      /* STABLE */
    })
);
}
export default /* @__PURE__ */ _export_sfc(_sfc_main, [
["render", _sfc_render],
]);
</code></pre>
<p>从上面的代码可以看到template中的代码编译后变成了render函数。</p>
<p>在render函数中<code>_createBlock($setup["ChildDemo"]</code>表示在渲染子组件<code>ChildDemo</code>,并且在执行<code>createBlock</code>函数时传入了第三个参数是一个对象。对象中包含<code>header</code>、<code>footer</code>、<code>default</code>三个方法,<code>这三个方法对应的是子组件</code>ChildDemo`中的三个插槽。执行这三个方法就会生成这三个插槽对应的虚拟DOM。</p>
<p>并且我们观察到插槽<code>footer</code>处的方法还接收一个对象作为参数,并且对象中还有一个<code>desc</code>字段,这个字段就是子组件传递给父组件的变量。</p>
<p>方法最外层的<code>withCtx</code>方法是为了给插槽的方法注入当前组件实例的上下文。</p>
<p>通过上面的分析我们可以得出一个结论:<code>在父组件中插槽经过编译后会变成一堆由插槽name组成的方法,执行这些方法就会生成插槽对应的虚拟DOM。默认插槽就是default方法,方法接收的参数就是子组件中插槽给父组件传递的变量</code>。但是有一点要注意,在父组件中我们只是定义了这三个方法,执行这三个方法的地方却不是在父组件,而是在子组件。</p>
<h1 id="编译后的子组件">编译后的子组件</h1>
<p>我们来看看编译后的子组件,简化后代码如下:</p>
<pre><code class="language-javascript">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]]);
</code></pre>
<p>同样的我们观察里面的render函数,里面的这个:</p>
<pre><code class="language-javascript">[
_renderSlot(_ctx.$slots, "default"),
_renderSlot(_ctx.$slots, "header"),
_renderSlot(_ctx.$slots, "footer", { desc: $setup.desc }),
]
</code></pre>
<p>对应的就是源代码里面的这个:</p>
<pre><code class="language-javascript">&lt;slot&gt;&lt;/slot&gt;
&lt;slot name="header"&gt;&lt;/slot&gt;
&lt;slot name="footer" :desc="desc"&gt;&lt;/slot&gt;
</code></pre>
<p>在上面我们看见一个<code>$slots</code>对象,这个是什么东西呢?</p>
<p>其实<code>useSlots</code>就是返回的<code>$slots</code>对象,我们直接在控制台中使用<code>useSlots</code>打印出<code>$slots</code>对象看看,代码如下:</p>
<pre><code class="language-javascript">&lt;script setup&gt;
import { ref, useSlots } from "vue";

const slots = useSlots();
console.log(slots);
const desc = ref("footer desc");
&lt;/script&gt;
</code></pre>
<p>我们来浏览器中看看此时打印的<code>slots</code>对象是什么样的,如下图:<br>
<img src="https://img2024.cnblogs.com/blog/1217259/202503/1217259-20250304171840460-433966667.png"></p>
<p>从上图中可以看到slots对象好像有点熟悉,这个对象中包含<code>default</code>、<code>footer</code>、<code>header</code>这三个方法,其实这个slots对象就是前面我们讲的父组件中定义的那个对象,执行对象的<code>default</code>、<code>footer</code>、<code>header</code>方法就会生成对应插槽的虚拟DOM。</p>
<p>前面我们讲了在父组件中会定义<code>default</code>、<code>footer</code>、<code>header</code>这三个方法,那这三个方法又是在哪里执行的呢?</p>
<p>答案是:在子组件里面执行的。</p>
<p>在执行<code>_renderSlot(_ctx.$slots, "default")</code>方法时就会去执行<code>slots</code>对象里面的<code>default</code>方法,这个是<code>renderSlot</code>函数的代码截图:<br>
<img src="https://img2024.cnblogs.com/blog/1217259/202503/1217259-20250304171856012-1550859464.png"></p>
<p>从上图中可以看到在<code>renderSlot</code>函数中首先会使用<code>slots</code>拿到对应的插槽方法,如果执行的是<code>_renderSlot(_ctx.$slots, "footer", { desc: $setup.desc })</code>,这里拿到的就是<code>footer</code>方法。</p>
<p>然后就是执行<code>footer</code>方法,前面我们讲过了这里的<code>footer</code>方法需要接收参数,并且从参数中结构出<code>desc</code>属性。刚好我们执行<code>renderSlot</code>方法时就给他传了一个对象,对象中就有一个<code>desc</code>属性,这不就对上了吗!</p>
<p>并且由于执行<code>footer</code>方法会生成虚拟DOM,所以footer生成的虚拟DOM是属于子组件里面的,同理footer对应的真实DOM也是属于在子组件的DOM树里面。</p>
<p>通过上面的分析我们可以得出一个结论就是:<code>子组件中的插槽实际就是在执行父组件插槽对应的方法,在执行方法时可以将子组件的变量传递给父组件,这就是作用域插槽的原理。</code></p>
<h1 id="总结">总结</h1>
<p>这篇文章我们讲了经过编译后父组件的插槽会被编译成一堆方法,这些方法组成的对象就是<code>$slots</code>对象。在子组件中会去执行这些方法,并且可以将子组件的变量传给父组件,由父组件去接收参数,这就是<code>作用域插槽</code>的原理。了解了这个后当我们在<code>useSlots</code>、<code>jsx</code>、<code>tsx</code>中定义和使用插槽就不会那么迷茫了。</p>
<p>关注公众号:【前端欧阳】,给自己一个进阶vue的机会</p>
<p><img src="https://img2024.cnblogs.com/blog/1217259/202406/1217259-20240606112202286-1547217900.jpg"></p><br><br>
来源:https://www.cnblogs.com/heavenYJJ/p/18750915
頁: [1]
查看完整版本: 花3分钟来了解一下Vue3中的插槽到底是什么玩意