一直对 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 元素. 同时, 可以看出跨组件传递插槽也是可以的, 但是必须注意具名插槽传递.
来源: http://www.jb51.net/article/137669.htm