深入浅析Vue中的slots/scoped slots

一直对Vue中的slot插槽比较感兴趣,下面是自己的一些简单理解,希望可以帮助大家更好的理解slot插槽

下面结合一个例子,简单说明slots的工作原理

dx-li子组件的template如下:

<li class="dx-li">

<slot>

你好!

</slot>

</li>

dx-ul父组件的template如下:

<ul>

<dx-li>

hello juejin!

</dx-li>

</ul>

结合上述例子以及vue中相关源码进行分析

dx-ul父组件中template编译后,生成的组件render函数:

module.exports={

render:function (){

var _vm=this;

var _h=_vm.$createElement;

var _c=_vm._self._c||_h;

// 其中_vm.v为createTextVNode创建文本VNode的函数

return _c('ul',

[_c('dx-li', [_vm._v("hello juejin!")])],

1)

},

staticRenderFns: []

}

传递的插槽内容'hello juejin!'会被编译成dx-li子组件VNode节点的子节点。

渲染dx-li子组件,其中子组件的render函数:

module.exports={

render:function (){

var _vm=this;

var _h=_vm.$createElement;

var _c=_vm._self._c||_h;

// 其中_vm._v 函数为renderSlot函数

return _c('li',

{staticClass: "dx-li" },

[_vm._t("default", [_vm._v("你好 掘金!")])],

2

)

},

staticRenderFns: []

}

初始化dx-li子组件vue实例过程中,会调用initRender函数:

function initRender (vm) {

...

// 其中_renderChildren数组,存储为 'hello juejin!'的VNode节点;renderContext一般为父组件Vue实例

这里为dx-ul组件实例

vm.$slots = resolveSlots(options._renderChildren, renderContext);

...

}

其中resolveSlots函数为:

/**

* 主要作用是将children VNodes转化成一个slots对象.

*/

export function resolveSlots (

children: ?Array<VNode>,

context: ?Component

): { [key: string]: Array<VNode> } {

const slots = {}

// 判断是否有children,即是否有插槽VNode

if (!children) {

return slots

}

// 遍历父组件节点的孩子节点

for (let i = 0, l = children.length; i < l; i++) {

const child = children[i]

// data为VNodeData,保存父组件传递到子组件的props以及attrs等

const data = child.data

/* 移除slot属性

* <span slot="abc"></span>

* 编译成span的VNode节点data = {attrs:{slot: "abc"}, slot: "abc"},所以这里删除该节点attrs的slot

*/

if (data && data.attrs && data.attrs.slot) {

delete data.attrs.slot

}

/* 判断是否为具名插槽,如果为具名插槽,还需要子组件/函数子组件渲染上下文一致。主要作用:

*当需要向子组件的子组件传递具名插槽时,不会保持插槽的名字。

* 举个栗子:

* child组件template:

* <div>

* <div class="default"><slot></slot></div>

* <div class="named"><slot name="foo"></slot></div>

* </div>

* parent组件template:

* <child><slot name="foo"></slot></child>

* main组件template:

* <parent><span slot="foo">foo</span></parent>

* 此时main渲染的结果:

* <div>

* <div class="default"><span slot="foo">foo</span></div>

<div class="named"></div>

* </div>

*/

if ((child.context === context || child.fnContext === context) &&

data && data.slot != null

) {

const name = data.slot

const slot = (slots[name] || (slots[name] = []))

// 这里处理父组件采用template形式的插槽

if (child.tag === 'template') {

slot.push.apply(slot, child.children || [])

} else {

slot.push(child)

}

} else {

// 返回匿名default插槽VNode数组

(slots.default || (slots.default = [])).push(child)

}

}

// 忽略仅仅包含whitespace的插槽

for (const name in slots) {

if (slots[name].every(isWhitespace)) {

delete slots[name]

}

}

return slots

}

然后挂载dx-li组件时,会调用dx-li组件render函数,在此过程中会调用renderSlot函数:

export function renderSlot (

name: string, // 子组件中slot的name,匿名default

fallback: ?Array<VNode>, // 子组件插槽中默认内容VNode数组,如果没有插槽内容,则显示该内容

props: ?Object, // 子组件传递到插槽的props

bindObject: ?Object // 针对<slot v-bind="obj"></slot> obj必须是一个对象

): ?Array<VNode> {

// 判断父组件是否传递作用域插槽

const scopedSlotFn = this.$scopedSlots[name]

let nodes

if (scopedSlotFn) { // scoped slot

props = props || {}

if (bindObject) {

if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {

warn(

'slot v-bind without argument expects an Object',

this

)

}

props = extend(extend({}, bindObject), props)

}

// 传入props生成相应的VNode

nodes = scopedSlotFn(props) || fallback

} else {

// 如果父组件没有传递作用域插槽

const slotNodes = this.$slots[name]

// warn duplicate slot usage

if (slotNodes) {

if (process.env.NODE_ENV !== 'production' && slotNodes._rendered) {

warn(

`Duplicate presence of slot "${name}" found in the same render tree ` +

`- this will likely cause render errors.`,

this

)

}

// 设置父组件传递插槽的VNode._rendered,用于后面判断是否有重名slot

slotNodes._rendered = true

}

// 如果没有传入插槽,则为默认插槽内容VNode

nodes = slotNodes || fallback

}

// 如果还需要向子组件的子组件传递slot

/*举个栗子:

* Bar组件: <div class="bar"><slot name="foo"/></div>

* Foo组件:<div class="foo"><bar><slot slot="foo"/></bar></div>

* main组件:<div><foo>hello</foo></div>

* 最终渲染:<div class="foo"><div class="bar">hello</div></div>

*/

const target = props && props.slot

if (target) {

return this.$createElement('template', { slot: target }, nodes)

} else {

return nodes

}

}

scoped slots理解

dx-li子组件的template如下:

<li class="dx-li">

<slot str="你好 掘金!">

hello juejin!

</slot>

</li>

dx-ul父组件的template如下:

<ul>

<dx-li>

<span slot-scope="scope">

{{scope.str}}

</span>

</dx-li>

</ul>

结合例子和Vue源码简单作用域插槽

dx-ul父组件中template编译后,产生组件render函数:

module.exports={

render:function (){

var _vm=this;

var _h=_vm.$createElement;

var _c=_vm._self._c||_h;

return _c('ul', [_c('dx-li', {

// 可以编译生成一个对象数组

scopedSlots: _vm._u([{

key: "default",

fn: function(scope) {

return _c('span',

{},

[_vm._v(_vm._s(scope.str))]

)

}

}])

})], 1)

},

staticRenderFns: []

}

其中 _vm._u函数:

function resolveScopedSlots (

fns, // 为一个对象数组,见上文scopedSlots

res

) {

res = res || {};

for (var i = 0; i < fns.length; i++) {

if (Array.isArray(fns[i])) {

// 递归调用

resolveScopedSlots(fns[i], res);

} else {

res[fns[i].key] = fns[i].fn;

}

}

return res

}

子组件的后续渲染过程与slots类似。scoped slots原理与slots基本是一致,不同的是编译父组件模板时,会生成一个返回结果为VNode的函数。当子组件匹配到父组件传递作用域插槽函数时,调用该函数生成对应VNode。

总结

其实slots/scoped slots 原理是非常简单的,我们只需明白一点vue在渲染组件时,是根据VNode渲染实际DOM元素的。

slots是将父组件编译生成的插槽VNode,在渲染子组件时,放置到对应子组件渲染VNode树中。

scoped slots是将父组件中插槽内容编译成一个函数,在渲染子组件时,传入子组件props,生成对应的VNode。最后子组件,根据组件render函数返回VNode节点树,update渲染真实DOM元素。同时,可以看出跨组件传递插槽也是可以的,但是必须注意具名插槽传递。

以上是 深入浅析Vue中的slots/scoped slots 的全部内容, 来源链接: utcz.com/z/359507.html

回到顶部