Vue插槽原理
插槽作为vue重要内容分发手段,很多同学对它的原理比较感兴趣,下面我们来探究一下。
测试代码
<!DOCTYPE html>
<html>
<head>
<title>Vue事件处理</title>
<script src="../../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>插槽处理机制</h1>
<comp1>
<span>abc</span>
</comp1>
</div>
<script>
// 声明自定义组件
Vue.component('comp1', { template: '<div><slot></slot></div>' })
// 创建实例
const app = new Vue({
el: '#demo'
});
console.log(app.$options.render);
</script>
</body>
</html>
输出结果如下:可见只是作为span的children出现,并没有什么特别
(function anonymous() {
with(this){return _c('div',{attrs:{"id":"demo"}},[
_c('h1',[_v("插槽处理机制")]),_v(" "),
_c('comp1',[_c('span',[_v("abc")])]),
})
插槽处理流程分析
没有使用v-slot指令,此时组件包含内容作为父组件的children出现,内部有没有slot对编译结果没有影响。下一步就是comp1组件实例化时会怎么做,看一下相关代码:
// core/instance/render.js
vm.$slots = resolveSlots(options._renderChildren, renderContext)
resolveSlots()函数从父组件中获取渲染结果VNode,它们被存入default,这就是默认插槽的内容。这里的renderContext就是父组件实例,显然如果有动态内容要从它里面获取。
这告诉我们为什么我们能在render函数中访问
this.$slots.default获取默认插槽内容
那么谁在使用$slots中的内容,显然是comp1组件的渲染函数,输出看一下:
(function anonymous(
) {
with(this){return _c('div',[_t("default"),_v(" "),
_t("foo",null,{"abc":"abc from comp"})],2)}
})
这里的_t就是renderSlot()的别名,它会用到$slots或$scopedSlots的内容
//src/core/instance/render-helpers/render-slot.js
const scopedSlotFn = this.$scopedSlots[name]
let nodes
if (scopedSlotFn) {
// ...
}
else {
nodes = this.$slots[name] || fallback
}
存在v-slot指令时
如果使用v-slot指令时,
<comp>
<template v-slot:default>abc</template>
<template v-slot:foo="ctx">{{ctx.abc}}</template>
</comp>
编译结果将成为作用域插槽形式:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[
_c('h1',[_v("attr update")]),
_c('comp',{scopedSlots:_u([
{key:"default",fn:function(){return [_v("abc")]},proxy:true},
{key:"foo",fn:function(ctx){return [_v(_s(ctx.abc))]}}])})],1)}
})
上面的_u是resolveScopedSlots()的别名,v-slot的参数会作为key,值会作为fn函数的参数,比如上面的ctx。根组件首次渲染时会调用该函数,返回作用域插槽描述对象$scopedSlots,结构如下:
我们知道ctx来自于子组件,它是怎么传进来的呢?先看一下comp1的渲染函数:
//...
_t("foo",null,{"abc":"abc from comp"})
comp1组件的渲染函数调用_t即renderSlot()时会将属性对象作为参数3传递,它们都来自comp1中名称为foo的具名插槽。所以最后返回的结果是renderSlot()的返回值,也就是前面fn的执行结果:
//src/core/instance/render-helpers/render-slot.js
const scopedSlotFn = this.$scopedSlots[name]
nodes = scopedSlotFn(props)
这就解答了为什么作用域插槽能够使用子组件中的数据,因为vue把作用域插槽转换为函数形式在子组件中调用了。
总结
大家平时使用插槽时经常会记不住不同插槽的用法,经过原理分析后我们能够更清楚的了解到,其实不管匿名插槽、具名插槽还是作用域插槽,最终的编译结果是一致的。既然如此完全可以写成相同样子:
<comp1>
<template v-slot:default>abc</template>
<template v-slot:foo>foo</template>
</comp1>
然后只需记住如果我要的数据是父组件的还是子组件的,如果是后者就给v-slot设置一个属性对象值:
<comp1>
<!--foo1来自承载comp1的父组件-->
<template v-slot:foo>{{foo1}}</template>
<!--bar1来自comp1-->
<template v-slot:bar="{bar1}">{{bar1}}</template>
</comp1>
还有哪些疑问没有解答,欢迎大家留言。