写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue 版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面 关注公众号 也可以吧
【Vue 原理】Slot – 源码版之作用域插槽
今天探索 Slot 的另一部分,作用域插槽。
首先,设置一个模板例子
把子组件的 child 传给 插槽
父组件会解析成下面的渲染函数
with(this) {return _c('div', {},
[_c('test', {
scopedSlots: _u([{
key: "default",
fn: function(slotProps) {return [ "我是放在组件的 slot :"+slotProps]
}
}])
})],
1)
}
其中,_u 是 resolveScopedSlots,Vue 会给每个实例都注册一个 _u 方法。
作用主要是把数组变成对象 map 并返回
看下 resolveScopedSlots 源码
给每个实例注册 _u
function resolveScopedSlots(fns, res) {res = res || {};
for (var i = 0; i < fns.length; i++) {res[fns[i].key] = fns[i].fn;
}
return res
}
把传入的数组组装成对象,像是下面这样
[{
key: "default",
fn: function(slotProps) {return ["我是放在组件的 slot :" + slotProps]
}
}]
---------_u 把上面变成下面 ------
{default:function(slotProps) {return ["我是放在组件的 slot :" + slotProps]
}
}
插槽怎么解析
你可以看到了,在父组件的渲染函数中,作用域 Slot 被包装成了一个函数,并且保存在 test 组件的 scopeSlot 中,用于后面解析内部组件时使用
包装成函数,是为了改变插槽内容的变量访问的作用域。
通过函数参数传递的形式,让插槽的变量,在解析时,先访问函数变量。如果没有,再去父组件上获取
那么这个函数的参数是从哪里传进来的呢?让我们一探究竟
额外:组件解析
上面的 test 在父组件中解析成一个节点
{
tag:'test',
children:["我是放在组件的 slot :11"]
}
而解析 test 时,也会被解析成一个节点
{
tag:'main',
children:["我在子组件里面","我是放在组件的 slot :11"]
}
就是 test 会有两个节点,第一个节点我认为是外壳节点,第二个节点是渲染节点,是真正插入页面的。
那么这两个有什么关系呢?外壳节点保存着所有父组件里给这个子组件绑定的数据,比如 props,插槽等。然后提供给 组件解析时使用
按顺序理一下解析流程
1、插槽函数保存到外壳节点
之前的父渲染函数,子组件的插槽解析成一个节点处理函数,如下,然后作为 scopedSlots 保存在 test 组件的外壳节点上
{
tag:'test',
data:{
scopedSlots:{
// 插槽包装成的函数
default:function(slotProps) {return [ "我是放在组件的 slot :"+slotProps]
}
}
},
children:["我是放在组件的 slot :11"]
}
2、插槽函数另存为
然后,test 组件会创建自身实例,并且初始化,在初始化的过程中,会把 外壳节点上的 $scopedSlots 另存为到本实例上,方便后面子组件解析内部模板直接调用
// 这个函数作用是,执行渲染函数,得到组件节点
Vue.prototype._render = function() {
var vm = this;
var ref = vm.$options;
// _parentVnode 就是外壳节点
var _parentVnode = ref._parentVnode;
if (_parentVnode) {vm.$scopedSlots = _parentVnode.data.scopedSlots || {};
}
... 省略 N 多执行渲染函数的代码
vm.$vnode = _parentVnode;
return vnode
};
3、子组件解析内部
看下子组件模板,绑定了 child 在 slot 上,用于传给插槽
执行子组件解析成的渲染函数如下
with(this) {
return _c('main', [
"我在子组件里面",
_t("default", null, {child: child})
], 2)
}
其中,child 会从子组件上获取,所以 child 是 11
渲染函数中,看到子组件中的 slot 的占位标签如下
<slot :child=child ></slot>
被解析成了一个_t 函数(怎么解析的话,又是一篇,太多先不说)
_t('default', null, { child:child})
看下_t,他是 renderSlot,上一篇文章提过。这个方法,会兼容处理作用域 Slot 和普通 Slot,上篇文章省略了处理作用域 Slot 的代码,现在看一下
function renderSlot(name, fallback, props) {
// 看了上面,所以可以从实例上获取 $scopedSlots
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
if (scopedSlotFn) {props = props || {};
// 开始执行插槽函数
nodes = scopedSlotFn(props);
}
return nodes
}
_t 的作用是,执行会直接返回节点,直接替换子组件 slot 占位符,完成插入功能
_t 就是 renderSlot,函数会根据【插槽名字】找到对应的【作用域 Slot 包装成的函数】,然后执行它,把子组件内的数据【{child:child}】子传进去
于是,作用域 Slot 生成的函数,就接收到了子组件传入的数据啦
所以 作用域 Slot 就可以拿传入的参数进行解析了
插槽怎么插入子组件
_t(‘default’,null,{ child:child}) 执行完毕,会返回节点,这个节点就是 slot 解析生成的节点
["我是放在组件的 slot :"+ {child:11} ]
子组件渲染函数执行完毕,生成的 vnode 如下
{
tag:'main',
children:[
"我在子组件里面",
"我是放在组件的 slot : {child:11}"
]
}
作用域插槽,成功地替换了原来的占位符 <slot>,当上了正宫娘娘
最后,来张图看下总的流程