博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
插槽是什么?我来告诉你!
阅读量:3959 次
发布时间:2019-05-24

本文共 12970 字,大约阅读时间需要 43 分钟。

插槽的编译

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

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

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

// 子组件Vue.component('child-component', {
template: `
`,})// 父组件new Vue({
el: '#app', template: `
`})
父组件的插槽编译

当编译第一个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

代码分析:

  1. 首先调用getAndRemoveAttrByRegex方法并给第二个参数传入slotRE正则表达式,用来获取并移除当前ast对象上的v-slot属性。
// beforeconst ast = {
attrsList: [ {
name: 'v-slot:header', value: '' } ]}// afterconst 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对象:

// headerconst ast = {
tag: 'template', slotTarget: '"header"', slotScope: '_empty_' }// defaultconst ast = {
tag: 'template', slotTarget: '"default"', slotScope: '_empty_' }// footerconst 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 = `  

`

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

// final children cleanup// filter out scoped slotselement.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 slots if (el.slotTarget && !el.slotScope) {
data += `slot:${
el.slotTarget},` } // scoped slots if (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.$slots const 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 camelized name: camelize(attr.name), value: attr.value, dynamic: attr.dynamic }))) : null const 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/vnode  res?: Object,  // the following are added in 2.6  hasDynamicKeys?: 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.$slots if (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
, props: ?Object, bindObject: ?Object): ?Array
{
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) } nodes = scopedSlotFn(props) || fallback } else {
nodes = this.$slots[name] || fallback } const target = props && props.slot if (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 = this const {
render, _parentVnode } = vm.$options if (_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: `
`,})new Vue({
el: '#app', data () {
return {
msg: '', isShow: true } }, template: `
`})

作用域插槽和普通插槽最本质的区别是:作用域插槽能拿到子组件的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标签上传递的值:

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

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

总结

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

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

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

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

转载地址:http://liozi.baihongyu.com/

你可能感兴趣的文章