「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
本篇小作文的主题是讨论 parseHTML 方法执行过程中解析到开始标签后调用 parseHTML 方法接收到的参数options.start 回调方法处理开始标签,其主要工作如下:
- 创建
AST节点element - 调用
options.modules中的preTransformNode方法处理element,options.modules来之createCompiler时传入的baseOptions; - 处理
v-pre指令以及在pre标签内的情景,接着处理v-for、v-for、v-if、v-once; - 维护
root节点,root只在第一次处理时会被赋值,后面处理的所有节点都是root的子节点; - 维护
currrentParent变量,这个意义在于:调用parseHTML的时候是以一种一维的方式解析树形的模板字符串,但是建立AST时却需要还原模板描述的节点间的父子关系,也就是说AST是有深度的。 - 非自闭和元素时维护
element入栈stack,当解析到闭合标签时出栈,如果是自闭合标签执行closeElement自动闭合当前元素,因为它没有闭合标签了,闭合标签的逻辑就要在这儿调用
前面一篇说了很多 options.start 方法,但是也只是梗概,从本篇小作文开始将致力于 options.start 方法中的各个能力实现的细节方法,本篇的重点在于 createASTElement、preTransforms
二、createASTElement
方法位置:src/compiler/parser/index.js -> function createASTElement
方法参数:
tag:标签名attrs:标签上的行内属性数组,是经过handleStartTag方法处理过的attrs形如:[{name: attrName, value: attrValue, start, end }]parent:当前元素的parent,用于组织新建AST元素对象之间的关系
方法作用:
创建 type 为 1 的 AST 对象,包含 attrsList,attrsMap,children 属性;这里要说的是 attrList 和 attrMap 的区别,attrList 就是 attrs 参数,上面的参数中有示例,而 attrsMap 是以 { attrKeyName: attrVlaue } 的形式存储;
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1, // ast 节点类型
tag, // 标签名
attrsList: attrs, // 当前标签上的属性数组形如:[{name: attrName, value: attrValue, start, end }]
attrsMap: makeAttrsMap(attrs), // 标签的属性对象 { attrName: attrVal, .... }
rawAttrsMap: {}, // 原始属性对象
parent, // 父节点
children: [] // 以后该 ast 节点的孩子节点都要保存在这个数组中
}
}
三、preTransforms
preTransform 不是个方法,它是个数组,是在 parse 方法中通过 pluckModuleFunction(options.modules, 'preTransformNode') 从 options.modules 上摘取出来的方法数组;
export function parse (template, options) {
// 从 options 中摘取方法
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
parseHTML(template, {
start () {
// for 循环调用 preTranforms 中的方法处理 element 这个新创建的 AST 节点
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
}
})
}
3.1 options.modules
pluckModuleFunciton 接收的 options 参数是 parse 方法接收到的参数,而 parse 方法也是从 createCompiler 接收到 baseOptions:
const { compile, compileToFunctions } = createCompiler(baseOptions)
src/platform/web/compiler/options.js导出的baseOptions如下:
import modules from './modules/index' // modules
export const baseOptions: CompilerOptions = {
expectHTML: true,
modules, // 处理 class、style、v-module
directives, // 处理指令
isPreTag, // 是否是 pre 标签
isUnaryTag, // 是否自闭和标签
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
./modules/index导出的module如下:
import klass from './class'
import style from './style'
import model from './model'
export default [
klass,
style,
model
]
klass、style、model三个模块导出情况如下,只有model导出了preTransformNode方法: src/platforms/web/compiler/modules/style.js
export default {
staticKeys: ['staticStyle'],
transformNode,
genData
}
src/platforms/web/compiler/modules/class.js
export default {
staticKeys: ['staticClass'],
transformNode,
genData
}
src/platforms/web/compiler/modules/model.js
export default {
preTransformNode // 这个就是我们要说的 preTransformNode 方法
}
3.2 preTransformNode
前面我们分析 options.modules,发现只有 model.js 导出了一个 preTransformNode 方法;
方法位置:src/platforms/web/compiler/modules/model.js
方法参数:
AST:AST节点options:compilerOptions就是baseOptions
方法作用:
以这个模板为例:
<input :type="inputType"
v-model="someInputValueInType"
v-if="someIfCondition === 10"
/>
处理存在 v-model 的 input 标签,这个过程不处理 v-model 的双向数据绑定,而是处理 input 的 type 为 checkbox、radio和其他情况的。具体步骤如下:
- 判断
el.tag为input才处理,同时el.attrsMap如果没有v-model,就直接退出; - 获取动态绑定的
type对应的绑定变量,赋值给变量typeBinding,比如上面模板上的inputType; - 如果动态绑定
type不为空,则进一步处理input上的v-if、v-else-if、v-else指令动态绑定的表达式; - 将
v-if表达式变为解析所得的ifConditon,变为ifExtraCondition即&& ifCondition,这个有啥用呢?后面将type为checkbox或radio的v-if表达式变为type === 'checkbox' + ifExtraCondition即type === 'checkbox' && ifCondition - 获取
el(el是ast对象)上的v-else-if绑定的表达式,赋值到elseIfCondition,解析el上有是否有v-else赋值到到hasElse变量 - 克隆
el得到branch0,处理克隆出来的ast的信息并为branch0添加v-if对应的条件,接着克隆el得到branch1,branch1的if条件为type === checkbox && branch0.if,同理克隆branch2,为branch2增加if条件为type === radio && branch.if;最后处理type不为checkbox或radio的其他情况作为branch3,branch3的if就是branch0.if - 返回克隆的
branch0,而非preTransfromNode接收到的el代表的原始ast;
function preTransformNode (el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
const map = el.attrsMap
// 不存在 v-model 属性则直接 return
if (!map['v-model']) {
return
}
// 获取 :type 的值
let typeBinding
if (map[':type'] || map['v-bind:type']) {
typeBinding = getBindingAttr(el, 'type')
}
if (!map.type && !typeBinding && map['v-bind']) {
typeBinding = `(${map['v-bind']}).type`
}
// 如果存在动态绑定的 type 属性,如上面的 :type="inputType" ,typeBingding 就是 inputType
// inputType 是一个变量,代指一个具体的 input 的 type 值,比如 checkbox、radio、color、text
if (typeBinding) {
// 获取 v-if 的值,比如 <input :type="inputType" v-model="someInputValueInType" v-if="someIfCondition === 10" />
// ifCondition 为 someIfCondition === 10
const ifCondition = getAndRemoveAttr(el, 'v-if', true)
// && someIfCondition === 10
const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``
// 是否存在 v-else 属性,<input v-else />
const hasElse = getAndRemoveAttr(el, 'v-else', true) != null
// 获取 v-else-if 属性的值 <input v-else-if="inputElseIfValue" />
const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true)
// 克隆一个新的 el 对象,分别处理 input type 为 checkbox、radio 情形,剩下都是其他类型了
// 具体是哪种情况,通过 el.ifConditions 条件来判断
// 1. checkbox
const branch0 = cloneASTElement(el)
// 处理 input 上带 v-for 的情况 <input v-for="item in arr" :key="index" />
// 处理 v-for 表达式,得到 branch0.for = 被迭代对象如上面的 arr,
// 得到 branch0.alias=迭代条目的名字,如上的 item
processFor(branch0)
// 在 branch0.attrsMap 和 attrList 对象中添加属性 type,值为 checkbox
addRawAttr(branch0, 'type', 'checkbox')
// 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其他指令和一些原生属性
processElement(branch0, options)
// 标记当前对象已经被处理过了,防止被重复处理
branch0.processed = true
// branch0 这个克隆出来的 ast 的 v-if 条件表达式变为:
// type 绑定变量 === 'checkbox' && el 的 v-if表达式
// inputType === 'checkbox' && someIfCondition === 10
branch0.if = `(${typeBinding})==='checkbox'` + ifConditionExtra
// 在 branch0.ifConditions 中放入 { exp, block } 对象
addIfCondition(branch0, {
exp: branch0.if,
block: branch0 // 这个 block 就是将来 exp 代表的条件成立时渲染出来的元素
})
// 再克隆一个新的 ast 对象
// 2. 给新克隆所得的 branch1 新的 ast 增加 type 为 radio 的 else-if 条件
const branch1 = cloneASTElement(el)
// 获取新克隆的 branch1 上的 v-for 指令对应的值
getAndRemoveAttr(branch1, 'v-for', true)
// 在 branch1.attrsMap 和 branch1.attrList 对象中添加 type 属性,值为 radio
addRawAttr(branch1, 'type', 'radio')
// 分别处理 key、ref、插槽、自闭合 slot 标签、动态组件、class、style、v-bind、v-on、其他指令、原生属性
processElement(branch1, options)
// 在 branch1.ifConditions 中放入 { exp, block } 对象
// 你会发现 addIfCondition 是个谁添加?是 branch0,而 branch0 就是克隆的 el
addIfCondition(branch0, {
exp: `(${typeBinding})==='radio'` + ifConditionExtra, // 判断是否为 radio
block: branch1 // 当满足 exp 所代表的条件成立时渲染 block 对应的 branch1 这个 ast
})
// 3. other,input type 为除 checkbox或radio 外的其他值,如 text
const branch2 = cloneASTElement(el)
getAndRemoveAttr(branch2, 'v-for', true)
addRawAttr(branch2, ':type', typeBinding)
processElement(branch2, options)
// 这里同样是给 branch0 进行 addIfCondition 操作
addIfCondition(branch0, {
exp: ifCondition,
block: branch2
})
// 弄了半天,branch1/2 有个啥用???
// 其目的在于给 branch0 通过 addIfConditon 设置条件,使满足不同条件时渲染对应 type 的 input 标签
if (hasElse) {
branch0.else = true
} else if (elseIfCondition) {
branch0.elseif = elseIfCondition
}
// 最后返回的不是el,而是克隆出来的 branch0
return branch0
}
}
}
3.3 preTransfromsNode 为了啥?
起初我也并没看明白,直到第二次看的时候我才完全看懂。它这么做是为了解决一个问题,就是 input 动态绑定 type 时的渲染问题。
例如咱们的例子 <input :type="inputType" v-if="someCondition" /> ,此时尚在编译,并不能准确获知 inputType 所表示的真实类型,inputType 有可能是 checkbox/radio/button/color/calendar... 中的任一个,为了解决这个问题,Vue 就把这一个模板变成下面的一系列模板:
<input v-if="someCondition" />
<input v-else-if="inputType === 'checkbox' && someCondition" />
<input v-else-if="inputType === 'radio' && someCondition" />
<input v-else />
这样无论你的 type 绑定的是个什么值,我相当于预判了你的所有预判,简直了。。。
四、总结
本文详细讨论了创建 AST 的方法 createASTElement 方法,以及来自 options.modules 的 preTransforms 变量所代表的 preTransfromNode 方法;
createASTElement方法创建type为1,即元素的AST节点对象,包含parent、children等用于组织节点间关系的属性;preTransfromsNode方法就是预处理带有v-model且动态绑定了type属性的input标签,目的是解决无论type绑定何种值,最后都能渲染除一个符合预期的input元素。