插槽的编译

对于插槽的编译,我们只需要记住一句话:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

注意:由于在Vue2.6+版本中,对于插槽相关的内容有所改动:它废弃了旧的用法,新增了v-slot指令。虽然依旧会在Vue2.0版本进行兼容,但在Vue3.0版本会将其进行移除,因此我们在分析插槽实现原理这一章节会以最新的v-slot新语法进行分析。

我们使用如下案例来分析插槽的编译原理:

// 子组件
Vue.component('child-component', {template: `<div><slot name="header" /><slot /><slot name="footer" /></div>`,
})// 父组件
new Vue({el: '#app',template: `<child-component><template v-slot:header>插槽头部内容</template><template v-slot>插槽内容</template><template v-slot:footer>插槽底部内容</template></child-component>`
})
父组件的插槽编译

当编译第一个template标签调用processElement方法的时候,会在这个方法里面调用processSlotContent来处理与插槽相关的内容:

export function processElement (element: ASTElement,options: CompilerOptions
) {// ...省略代码processSlotOutlet(element)// ...省略代码return element
}

就我们的例子而言,在processSlotContent方法中,其相关代码如下:

const slotRE = /^v-slot(:|$)|^#/
export const emptySlotScopeToken = `_empty_`
function processSlotContent (el) {let slotScope// ...省略代码if (el.tag === 'template') {// v-slot on <template>const slotBinding = getAndRemoveAttrByRegex(el, slotRE)if (slotBinding) {// ..异常处理const { name, dynamic } = getSlotName(slotBinding)el.slotTarget = nameel.slotTargetDynamic = dynamicel.slotScope = slotBinding.value || emptySlotScopeToken}}// ...省略代码
}

代码分析:

  1. 首先调用getAndRemoveAttrByRegex方法并给第二个参数传入slotRE正则表达式,用来获取并移除当前ast对象上的v-slot属性。
// before
const ast = {attrsList: [{ name: 'v-slot:header', value: '' }]
}
// after
const ast = {attrsList: []
}
  1. 随后通过调用getSlotName方法来获取插槽的名字以及获取是否为动态插槽名。
const { name, dynamic } = getSlotName(slotBinding)
console.log(name)     // "header"
console.log(dynamic)  // falsefunction getSlotName (binding) {let name = binding.name.replace(slotRE, '')if (!name) {if (binding.name[0] !== '#') {name = 'default'} else if (process.env.NODE_ENV !== 'production') {warn(`v-slot shorthand syntax requires a slot name.`,binding)}}return dynamicArgRE.test(name)// dynamic [name]? { name: name.slice(1, -1), dynamic: true }// static name: { name: `"${name}"`, dynamic: false }
}
  1. 最后如果正则解析到有作用域插槽,则赋值给slotScope属性,如果没有则取一个默认的值_empty_

对于第二个、第三个template标签而言,它们的编译过程是一样的,当这三个标签全部编译完毕后,我们可以得到如下三个ast对象:

// header
const ast = { tag: 'template', slotTarget: '"header"', slotScope: '_empty_' }
// default
const ast = { tag: 'template', slotTarget: '"default"', slotScope: '_empty_' }
// footer
const ast = { tag: 'template', slotTarget: '"footer"', slotScope: '_empty_' }

随后,我们在closeElement方法中可以看到如下代码:

if (element.slotScope) {// scoped slot// keep it in the children list so that v-else(-if) conditions can// find it as the prev node.const name = element.slotTarget || '"default"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
}
currentParent.children.push(element)
element.parent = currentParent

首先,我们关注if分支里面的逻辑,element可以理解为以上任意一个template标签的ast对象。当ast对象存在slotScope属性的时候,Vue把当前ast节点挂到父级的scopedSlots属性上面:

// 举例使用,实际为AST对象
const parentAST = {tag: 'child-component',scopedSlots: {'header': 'headerAST','default': 'defaultAST','footer': 'footerAST'}
}

if分支外面,它又维护了父、子AST对象的树形结构,如下:

// 举例使用,实际为AST对象
const parentAST = {tag: 'child-component',children: [{ tag: 'template', slotTarget: '"header"', slotScope: '_empty_', parent: 'parentAST' },{ tag: 'template', slotTarget: '"default"', slotScope: '_empty_', parent: 'parentAST' },{ tag: 'template', slotTarget: '"footer"', slotScope: '_empty_', parent: 'parentAST' }],scopedSlots: {'header': 'headerAST','default': 'defaultAST','footer': 'footerAST'}
}

看到这里,你可能会非常疑惑:插槽的内容应该分发到子组件,为什么要把插槽AST对象添加到父级的Children数组中呢?

如果你注意观察上面代码注释的话,你就能明白为什么样这样做,这样做的目的是:正确维护v-else或者v-else-if标签关系。

const template = `<div><p v-if="showSlot"></p><template v-else-if="showDefaultSlot" v-slot:default>插槽内容</template><p v-else></p></div>
`

tree层级关系确定后,再从children数组中过滤掉插槽AST元素:

// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !(c: any).slotScope)

当父组件编译完毕后,我们可以得到如下ast对象:

const ast = {tag: 'child-component',children: [],scopedSlots: {'header': { tag: 'template', slotTarget: '"header"', slotScope: '_empty_' },'default': { tag: 'template', slotTarget: '"default"', slotScope: '_empty_' },'footer': { tag: 'template', slotTarget: '"footer"', slotScope: '_empty_' }}
}

既然parse解析过程已经结束了,那么我们来看codegen阶段。在genData方法中,与插槽相关的处理逻辑如下:

export function genData (el: ASTElement, state: CodegenState): string {// ...省略代码// slot target// only for non-scoped slotsif (el.slotTarget && !el.slotScope) {data += `slot:${el.slotTarget},`}// scoped slotsif (el.scopedSlots) {data += `${genScopedSlots(el, el.scopedSlots, state)},`}// ...省略代码
}

对于父组件而言,因为它有scopedSlots属性,所以会调用genScopedSlots方法来处理,我们来看一下这个方法的代码:

function genScopedSlots (el: ASTElement,slots: { [key: string]: ASTElement },state: CodegenState
): string {// ...省略代码const generatedSlots = Object.keys(slots).map(key => genScopedSlot(slots[key], state)).join(',')return `scopedSlots:_u([${generatedSlots}]${needsForceUpdate ? `,null,true` : ``}${!needsForceUpdate && needsKey ? `,null,false,${hash(generatedSlots)}` : ``})`
}function genScopedSlot (el: ASTElement,state: CodegenState
): string {const isLegacySyntax = el.attrsMap['slot-scope']// ...省略代码const slotScope = el.slotScope === emptySlotScopeToken? ``: String(el.slotScope)const fn = `function(${slotScope}){` +`return ${el.tag === 'template'? el.if && isLegacySyntax? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`: genChildren(el, state) || 'undefined': genElement(el, state)}}`// reverse proxy v-slot without scope on this.$slotsconst reverseProxy = slotScope ? `` : `,proxy:true`return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
}

如果我们仔细观察genScopedSlotsgenScopedSlot的代码,就能发现核心代码是在genScopedSlot方法对于fn变量的赋值这一块。我们现在不用把所有判断全部搞清楚,只需要按照我们的例子进行分解即可:

const fn = `function(${slotScope}){return ${genChildren(el, state) || 'undefined'}
`

因为template里面只是一个简单的文本内容,所以当调用genChildren方法完毕后,genScopedSlot返回值如下:

let headerResult = '{key:"header",fn:function(){return [_v("插槽头部内容")]},proxy:true}'
let defaultResult = '{key:"header",fn:function(){return [_v("插槽内容")]},proxy:true}'
let footerResult = '{key:"header",fn:function(){return [_v("插槽底部内容")]},proxy:true}'

最后,回到genScopedSlots方法中,把结果串联起来:

const result = `{scopedSlots:_u([{ key:"header",fn:function(){return [_v("插槽头部内容")]},proxy:true },{ key:"default",fn:function(){return [_v("插槽内容")]},proxy:true },{ key:"footer",fn:function(){return [_v("插槽底部内容")]},proxy:true}])}
`
子组件的插槽编译

子组件的插槽的parse解析过程与普通标签没有太大的区别,我们直接看parse阶段完毕后的ast:

const ast = {tag: 'div',children: [{ tag: 'slot', slotName: '"header"' },{ tag: 'slot', slotName: '"default"' },{ tag: 'slot', slotName: '"footer"' }]
}

codegen代码生成阶段,当调用genElement方法时,会命中如下分支:

else if (el.tag === 'slot') {return genSlot(el, state)
}

命中else if分支后,会调用genSlot方法,其代码如下:

function genSlot (el: ASTElement, state: CodegenState): string {const slotName = el.slotName || '"default"'const children = genChildren(el, state)let res = `_t(${slotName}${children ? `,${children}` : ''}`const attrs = el.attrs || el.dynamicAttrs? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({// slot props are camelizedname: camelize(attr.name),value: attr.value,dynamic: attr.dynamic}))): nullconst bind = el.attrsMap['v-bind']if ((attrs || bind) && !children) {res += `,null`}if (attrs) {res += `,${attrs}`}if (bind) {res += `${attrs ? '' : ',null'},${bind}`}return res + ')'
}

genSlot方法不是很复杂,也很好理解,所以我们直接看最后生成的render函数:

const render = `with(this){return _c('div',[_t("header"),_t("default"),_t("footer")],2)
}`

插槽的patch

当处于patch阶段的时候,它会调用render函数生成vnode。在上一节中,我们得到了父、子组件两个render函数:

// 父组件render函数
const parentRender = `with(this){return _c('child-component', {scopedSlots:_u([{ key:"header",fn:function(){return [_v("插槽头部内容")]},proxy:true },{ key:"default",fn:function(){return [_v("插槽内容")]},proxy:true },{ key:"footer",fn:function(){return [_v("插槽底部内容")]},proxy:true}])})
}`// 子组件render函数
const childRender = `with(this){return _c('div',[_t("header"),_t("default"),_t("footer")],2)
}`

当执行render函数的时候,会调用_c、_u、_v以及_t这些函数,在这几个函数中我们重点关注_u和_t这两个函数。

_u函数的代码如下,它定义在src/core/instance/render-helpers/resolve-scoped-slots.js文件中:

// _u函数
export function resolveScopedSlots (fns: ScopedSlotsData, // see flow/vnoderes?: Object,// the following are added in 2.6hasDynamicKeys?: boolean,contentHashKey?: number
): { [key: string]: Function, $stable: boolean } {res = res || { $stable: !hasDynamicKeys }for (let i = 0; i < fns.length; i++) {const slot = fns[i]if (Array.isArray(slot)) {resolveScopedSlots(slot, res, hasDynamicKeys)} else if (slot) {// marker for reverse proxying v-slot without scope on this.$slotsif (slot.proxy) {slot.fn.proxy = true}res[slot.key] = slot.fn}}if (contentHashKey) {(res: any).$key = contentHashKey}return res
}

代码分析:当resolveScopedSlots函数调用的时候,我们传递了一个fns数组,在这个方法中首先会遍历fns,然后把当前遍历的对象赋值到res对象中,其中slot.key当做键,slot.fn当做值。当resolveScopedSlots方法调用完毕后,我们能得到如下res对象:

const res = {header: function () {return [_v("插槽头部内容")]},default: function () {return [_v("插槽内容")]},footer: function () {return [_v("插槽底部内容")]}
}

_t函数的代码如下,它定义在src/core/instance/render-helpers/render-slot.js文件中:

// _t函数
export function renderSlot (name: string,fallback: ?Array<VNode>,props: ?Object,bindObject: ?Object
): ?Array<VNode> {const scopedSlotFn = this.$scopedSlots[name]let nodesif (scopedSlotFn) { // scoped slotprops = 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)}nodes = scopedSlotFn(props) || fallback} else {nodes = this.$slots[name] || fallback}const target = props && props.slotif (target) {return this.$createElement('template', { slot: target }, nodes)} else {return nodes}
}

我们在分析renderSlot方法之前,先来看this.$scopedSlots这个属性。当调用renderSlot方法的时候,这里的this代表子组件实例,其中$scopedSlots方法是在子组件的_render方法被调用的时候赋值的。

Vue.prototype._render = function () {const vm: Component = thisconst { render, _parentVnode } = vm.$optionsif (_parentVnode) {vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots,vm.$slots,vm.$scopedSlots)}// ...省略代码
}

我们可以看到,它调用了normalizeScopedSlots方法,并且第一个参数传递的是父组件的scopedSlots属性,这里的scopedSlots属性就是_u方法返回的res对象:

const res = {header: function () {return [_v("插槽头部内容")]},default: function () {return [_v("插槽内容")]},footer: function () {return [_v("插槽底部内容")]}
}

到这里,我们就把_u_t这两个方法串联起来了。接下来再看renderSlot方法就容易很多。renderSlot方法的主要作用就是把res.headerres.default以及res.footer方法依次调用一遍并且返回生成的vnode。

renderSlot方法调用完毕后,可以得到子组件如下vnode对象:

const childVNode = {tag: 'div',children: [{ text: '插槽头部内容' },{ text: '插槽内容' },{ text: '插槽底部内容' }]
}

作用域插槽

在分析插槽的parse、插槽的patch过程中我们提供的插槽都是普通插槽,还有一种插槽使用方式,我们叫做作用域插槽,如下:

Vue.component('child-component', {data () {return {msg1: 'header',msg2: 'default',msg3: 'footer'}},template: `<div><slot name="header" :msg="msg1" /><slot :msg="msg2" /><slot name="footer" :msg="msg3" /></div>`,
})
new Vue({el: '#app',data () {return {msg: '',isShow: true}},template: `<child-component><template v-slot:header="props">{{props.msg}}</template><template v-slot="props">{{props.msg}}</template><template v-slot:footer="props">{{props.msg}}</template></child-component>`
})

作用域插槽和普通插槽最本质的区别是:作用域插槽能拿到子组件的props。对于这一点区别,它体现在生成fn函数的参数上:

const render = `with(this){return _c('child-component',{scopedSlots:_u([{ key:"header",fn:function(props){return [_v(_s(props.msg))]} },{ key:"default",fn:function(props){return [_v(_s(props.msg))]} },{ key:"footer",fn:function(props){return [_v(_s(props.msg))]} }])})
}`

这里的props就是我们在子组件slot标签上传递的值:

<slot name="header" :msg="msg1" />
<slot :msg="msg2" />
<slot name="footer" :msg="msg3" />

所以,对于我们的例子而言,最后生成的子组件vnode对象如下:

const childVNode = {tag: 'div',children: [{ text: 'header' },{ text: 'default' },{ text: 'footer' }]
}

总结

在这一小节,我们首先回顾了插槽的parse编译过程以及插槽的patch过程。

随后,我们对比了普通插槽和作用域插槽的区别,它们本质上的区别在于数据的作用域,普通插槽在生成vnode时无法访问子组件的props数据,但作用域插槽可以。

最后,我们知道了当插槽template使用了来自父组件的响应式变量或者与v-ifv-for以及动态插槽名一起使用时,当响应式变量更新后,会强制通知子组件重新进行渲染。

觉得写得不错的话,请用你们发财的小手点个赞叭!

插槽是什么?我来告诉你!相关推荐

  1. vue渲染大量数据如何优化_Vue3 Compiler 优化细节,如何手写高性能渲染函数

    送5本<你不知道的 JavaScript 上卷>点我抽奖,祝大家好运 Vue3 的 Compiler 与 runtime 紧密合作,充分利用编译时信息,使得性能得到了极大的提升.本文的目的 ...

  2. linux 查看 pci 设备驱动,如何写linux pci设备驱动程序

    PCI总线应用领域及其广泛并且令人惊奇.不同的pci设备有不同的需求以及不同的问题.因此,在linux内核中pci层支持就非常重要啦.本文档就是想为驱动程序设计开发人员解决pci处理中的各种问题. 0 ...

  3. 计算机内存4g如何,电脑内存4G升到8G,只需三步,让你轻松搞定笔记本内存升级...

    今日看点:电脑内存4G升到8G,只需三步,让你轻松搞定笔记本内存升级 大家好,这期小编给大家讲讲怎么给笔记本升级电脑内存,小编现在用的是朋友的笔记本,小编发现朋友的笔记本的内存只有4GB.小编每次用p ...

  4. Vue 中的作用域插槽

    作用域插槽 <div id="root"><child></child> </div> Vue.component('child', ...

  5. 骨骼动画实现秘密!闲鱼 Flutter 互动引擎告诉你

    简介: 代表骨骼动画是一种通过控制骨骼参数来实现多帧动画的方式,区别于 GIF 的不连贯和序列帧的体积大,骨骼动画有较好的灵活性和流畅性.目前骨骼动画已经被大规模地在游戏和动画中所使用,大有一种取代帧 ...

  6. 四、Vue组件化开发学习笔记——父子组件通信,父级向子级传值(props),子级向父级传值(自定义事件),slot插槽

    一.父子组件的通信 在上一篇博文中,我们提到了子组件是不能引用父组件或者Vue实例的数据的. 但是,在开发中,往往一些数据确实需要从上层传递到下层: 比如在一个页面中,我们从服务器请求到了很多的数据. ...

  7. 三分钟带你弄懂slot插槽——vue进阶

    文章目录 三分钟带你弄懂slot插槽--vue进阶 一.概述 程序员之死 什么是 slot插槽? 2.6.0 版本中的 slot 二.具名插槽 例子 效果图 代码 三.小惊喜 三分钟带你弄懂slot插 ...

  8. vue-slot插槽

    1.什么是插槽? 默认情况下使用子组件时在子组件中编写的元素是不会被渲染的 如果子组件中有部分内容是使用时才确定的, 那么我们就可以使用插槽 插槽就是在子组件中放一个"坑", 以后 ...

  9. raid卡缓存对硬盘性能_告诉你NAS究竟用不用RAID?万兆网络下NAS读取写入实测分...

    老司机通常会告诉你,民用raid都不靠谱,最靠谱的还是单盘独立使用,手动备份重要数据,高级一点会教你使用nas自带的定期备份,定期自动备份重要文件夹.大体上来说,在当下主流还是千兆内网的情况下,这样是 ...

最新文章

  1. 2022-2028年中国椎间孔镜行业市场研究及前瞻分析报告
  2. Java Day02-2(字符串)
  3. 儿童编程软件python-Python编程工具pycharm的使用
  4. c语言平年表示方法,C语言平年闰年问题
  5. python从sqlserver提取数据_通过Python读取sqlserver数据写成json文件的总结
  6. easyui 如何为标签动态追加属性实现渲染效果
  7. 用 JavaScript 验证只能输入数字,并做数字加总
  8. 使用.NET Core优雅获取并展示最新疫情数据
  9. MATLAB 添加文件路径
  10. win7的centos虚拟机上搭建mysql5.6服务
  11. 震惊,线程共享变量使用不当引发血案
  12. Objective-C浅拷贝和深拷贝
  13. 计算机如何恢复记事本,如何使用电脑桌面便签恢复误删除了的记事本数据内容?...
  14. 国家统计局 2019年省市区数据(自取)
  15. 怎么使用小爱同学音响_小爱同学语音唤醒功能怎么设置,小爱同学音箱的优缺点是什么...
  16. python读取图片信息_笔记整理4——python实现提取图片exif信息
  17. python科学计算三维可视化学习笔记(0)
  18. 为什么英语能够成为全球通用语言,汉语却不行?
  19. 博弈论分析题_博弈论复习题及答案分析
  20. golang string(fid)与strconv.Itoa(fid)

热门文章

  1. Linux - 第8节 - 进程信号
  2. ios如何 自定义键盘
  3. wordpress比较好用的模板是哪个,或者是说多人使用的。
  4. 带你认识微商城和小程序商城以及APP商城电商解决方案的区别
  5. 2021三大运营商资费上涨,哪种流量卡可以花最少的钱用最多的流量
  6. 苏州大学计算机组成原理,苏州大学计算机组成原理习题
  7. 信号与系统-1-δ函数尺度运算的证明
  8. 美国医疗领域IT的9个发展趋势(2005年)
  9. 记忆训练一书的思维导图
  10. zczxsssssssssssssss