「这是我参与2022首次更文挑战的第22天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
本篇小作文继续讨论了 options.start 方法的内部方法的作用:
-
processIf:处理ast节点对象上的v-if/v-else-if/v-else,将这些条件渲染信息添加到el.if/el.elseif/el.else; -
processOnce:处理v-once,得到el.once = true; -
checkRootContraints:检查根元素的限制,例如不能用slot和template作为根; -
以及
options.start接收到自闭和标签时执行的closeElement及其工具方法,这里并没有说完closeElement方法,后面两篇将会这个讲述这个过程
二、 procesKey
方法位置:src/compiler/parser/index.js -> processKey
方法参数:el,ast 节点对象
方法作用:解析 el 上的 key 属性值,这个 key 就是我们使用 v-for 设置的 :key="xxx";在这个过程中会检查 template 标签上不能使用 key,transition-group 子元素不能使用 v-for 的 index 作为 key;
function processKey (el) {
// 拿到 key 的属性值
const exp = getBindingAttr(el, 'key')
if (exp) {
// 关于 key 使用上的异常处理
if (process.env.NODE_ENV !== 'production') {
// template 标签不允许使用 key
if (el.tag === 'template') {
}
// 不要在 <transition-group> 子元素上
// 使用 v-for 的 index 作为 key,这和没有 key 没区别
if (el.for) {
}
}
// 设置 el.key = exp
el.key = exp
}
}
三、processRef
方法位置:src/compiler/parser/index.js -> functions processRef
方法参数:el,ast 节点对象
方法作用:处理 el 上的 ref 属性,el.ref = ref,随后 checkInFor(el) 即检查当前节点是否处于有 v-for 的元素包裹中(子代或者其他后代),如果被有 v-for 的元素包围了,ref 就是这些 DOM 组成的数组;
function processRef (el) {
const ref = getBindingAttr(el, 'ref')
if (ref) {
el.ref = ref
// 判断包含 ref 属性的元素是否包含在具有 v-for 指令的元素内或后代元素中
// 如果是,则 ref 将会是有这个 ref 的元素组成的的数组
el.refInFor = checkInFor(el)
}
}
四、 checkInFor
方法位置:src/compiler/parser/index.js -> function checkInFor
方法参数:el,ast 节点对象
方法作用:查看当前元素的长辈(父辈及更高)是否有 v-for 指令。原理就是 while(parent) 循环,检查 parent.for 是否有值,parent.for 是处理 v-for 的时候添加到 el 上的属性。el.for 的值指向可迭代对象,el.alias 指向可迭代对象的条目名称,例如 item;
function checkInFor (el: ASTElement): boolean {
let parent = el
while (parent) {
if (parent.for !== undefined) {
return true
}
parent = parent.parent
}
return false
}
五、processSlotContent
方法位置:src/compiler/parser/index.js -> function processSlotContent
方法参数:el,ast 节点对象
方法作用:处理被作为插槽传递给组件的内容;解析插槽名、插槽是否为动态插槽、作用域插槽的的值,当在组件上直接使用 v-slot 语法时,将前述内容放到 el.scopedSlot 对象上,其他情况下直接放到 el 上;
- 如果
el.tag即当前el标签为template时,从el上获取scope属性值或者slot-scope值; - 如果不是
template标签,获取slot-scope属性值; - 处理完插槽作用域,接着处理
slot插槽的名字,- 3.1 即具名插槽的名字,
slot="xxx"是老旧的具名插槽语法,设置el.slotTarget = 插槽名或者 "default" - 3.2 接着还要判断插槽名字是否使用了动态绑定的语法即
v-bind:slot="xxx"或者:slot="xxx"这种形式的,就是动态绑定了动态插槽语法,如果用了就设置el.slotTargetDynamic属性,命中为true,否则false
- 3.1 即具名插槽的名字,
- 处理
Vue 2.6以后的版本中v-slot指令:- 4.1 如果
v-slot在template标签上,通过slotBinding = getAndReomveAttrByRegex(el, slotReg)获取v-slot绑定的slot信息:name,dynamic,即插槽名和是否动态插槽。期间检测是否存在v-slot和slot-scope混用的情况;将name赋值给el.slotTarget,dynamic赋值给el.slotTargetDynamic,而作用域插槽的值el.slotScope = slotBinding.value - 4.2
else就是直接在组件上使用v-slot指令,要手动创建一个<template></template>ast 节点slotContainer,用于包装这个组件,所以直接在组件上v-slot只不过是一个语法糖;接着进行前面4.1中的赋值el.slotTarget/el.slotTargetDynamic/el.slotScope,然后将这个包装slotContrainer的children中的非作用域插槽的元素的筛选出来,并且将其父元素parent设置为slotContrainer; - 4.3 清空
el.children,因为他的孩子都跑到了el.scopedSlots - 4.4 标记
el.plain为false
- 4.1 如果
function processSlotContent (el) {
let slotScope
if (el.tag === 'template') {
// template 标签上使用 scope 属性的提示
slotScope = getAndRemoveAttr(el, 'scope')
if (process.env.NODE_ENV !== 'production' && slotScope) {
}
// el.slotScope = val
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
// slot-scope 也可以用到非 template 标签上
if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
// 元素不能同时使用 slot-scope 和 v-for 指令,v-for 优先级更高
// 应该用 template 标签作为容器,将 slot-scope 放到 template 标签上,
// 而 v-for 写在外面
}
// 将 slotScope 添加到 el 对象上
el.slotScope = slotScope
}
// 获取 slot 属性值,老的具名插槽的语法
// <div slot="someSlotName" ></div>
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
// el.slotTarget = 插槽名(具名插槽)
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
// 动态插槽名:<div v-bind:slot="someDynamicSlot" 或者 :slot="sds" >
el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
// 保留 slot 作为一个原生属性以兼容原生的 shadow DOM,仅当非作用域插槽
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
}
}
// 2.6 以后的 v-slot 语法
if (process.env.NEW_SLOT_SYNTAX) {
if (el.tag === 'template') {
// v-slot 在 template 标签上,得到 v-slot 的值
// v-slot on <template>
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
// 异常提示
if (process.env.NODE_ENV !== 'production') {
if (el.slotTarget || el.slotScope) {
// 不同插槽语法禁止混用
warn(
`Unexpected mixed usage of different slot syntaxes.`,
el
)
}
if (el.parent && !maybeComponent(el.parent)) {
// <template v-slot> 这能出现在组件内部的直接子级,比如
// <comp>
// <template v-slot>xx</template>
// </comp>
// 而不能是
// <comp>
// <div>
// <template v-slot>xx</template>
// </div>
// </comp>
warn(
`<template v-slot> can only appear at the root level inside ` +
`the receiving component`,
el
)
}
}
// 得到插槽名称
const { name, dynamic } = getSlotName(slotBinding)
// 插槽名
el.slotTarget = name
// 是否为动态插槽
el.slotTargetDynamic = dynamic
// 作用域插槽的值
el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
}
} else {
// 处理组件上的 v-slot,<comp v-slot:header />
// slotBinding = { name: 'v-slot:header', value: '', start, end }
// v-slot on component, denotes default slot
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
if (process.env.NODE_ENV !== 'production') {
// el 不是组件,提示 v-slot 只能出现在组件或者 template 标签上
if (!maybeComponent(el)) {
warn(
`v-slot can only be used on components or <template>.`,
slotBinding
)
}
// 语法混用
if (el.slotScope || el.slotTarget) {
warn(
`Unexpected mixed usage of different slot syntaxes.`,
el
)
}
// 为了避免歧义,当存在其他命名插槽时,默认插槽也该用 <template> 语法
if (el.scopedSlots) {
warn(
`To avoid scope ambiguity, the default slot should also use ` +
`<template> syntax when there are other named slots.`,
slotBinding
)
}
}
// 将组件的孩子添加到它的默认插槽内
// add the component's children to its default slot
const slots = el.scopedSlots || (el.scopedSlots = {})
// 获取插槽名称以及是否为动态插槽
const { name, dynamic } = getSlotName(slotBinding)
// 创建一个 template 标签的 ast 对象,用于容纳插槽内容,父级是 el
const slotContainer = slots[name] = createASTElement('template', [], el)
// 插槽名
slotContainer.slotTarget = name
// 是否为动态插槽
slotContainer.slotTargetDynamic = dynamic
// 所有的孩子,将每一个孩子的 parent 属性都设置为 slotContainer
slotContainer.children = el.children.filter((c: any) => {
if (!c.slotScope) {
// 筛选并给插槽内非插槽元素设置 parent 属性
// 为 slotContainer,也就是这个语法糖的 template 元素
c.parent = slotContainer
return true
}
})
// el 的 插槽都被弄到 el.scopedSlots 属性了
el.children = []
// mark el non-plain so data gets generated
el.plain = false
}
}
}
}
六、总结
本篇小作文在讨论 options.start 中的 closeElement 的内部工具方法:processKey、processRef、checkInFor、processSlotContent:
-
processKey:检查元素上的key属性并检查使用key的限制; -
processRef:处理ref属性,若ref被v-for包裹,则ref指向一个数组; -
checkInFor:检测当前元素的父辈元素中是否有v-for指令; -
processSlotContent:处理插槽内容,获取插槽名、动态插槽以及2.6.x以后的v-slot指令;