[vue源码笔记08]vue2.x的模板编译之构建ast

术语和流程

模板:template

抽象语法树:astElement

定向转化后的语法树:transformed-ast

代码字符串:code

渲染函数:render

转化流程:template –> astElement –> transformed-ast –> code –> render

所有源代码默认尽可能剔除开发环境的校验代码,便于关注主干逻辑

源代码

转化入口$mount

以下源代码位于src/platforms/web/entry-runtime-with-compiler.js

// 主要是标准化template模板
// 传入template可以是元素节点,也可以是节点选择器,也可以是模板字符串
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
// 其实是可以传入render函数的,这里就不考虑该分支逻辑了
if (!options.render) {
// 获取最终template模板
let template = options.template
if (template) {
if (typeof template === 'string') {
// 如果获取到options中的template为以'#'开头的字符串,则以此为id查找dom对象
// 并获取其innerHTML作为template
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) { // 如果template为dom对象,则取其innerHTML为template
template = template.innerHTML
} else {
return this
}
} else if (el) { // 如果options中没有template,则获取el对象的outerHTML为template
template = getOuterHTML(el)
}
// 通过template生成render方法
if (template) {
// 将template模板字符串转为render函数
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines, // false
shouldDecodeNewlinesForHref, // false
delimiters: options.delimiters, // 自定义插入变量分隔符,默认为"{{"和"}}"
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 此处为后续调用render进行渲染、patch以及diff等
return mount.call(this, el, hydrating)
}

function compileToFunctions

compileToFunctionscreateCompileToFunctionFn生成,传入compile函数作为编译函数,闭包变量cache用于缓存编译结果,compileToFunctions并不涉及编译的具体逻辑,只是对options做了一些处理

export function createCompileToFunctionFn (compile: Function): Function {
// 闭包变量,用于缓存编译结果
const cache: {
[key: string]: CompiledFunctionResult;
} = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
// 检查是否命中缓存
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// 调用compile进行编译
const compiled = compile(template, options)
// 将编译得到的code转化为render-function
const res = {}
const fnGenErrors = []
// 将render字符串生成render函数
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
return (cache[key] = res)
}
}

function compile

该函数位于src/compiler/create-compiler.js,作用是合并传入options和baseOptions生成最终的options:finalOptions作为参数传给baseCompile

// 传入baseCompile,为编译函数
function createCompilerCreator (baseCompile: Function): Function {
// baseOptions为基础option和传入option做一次合并生成最终options
return function createCompiler (baseOptions: CompilerOptions) {
return function compile (
template: string,
options?: CompilerOptions // 传入options
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
finalOptions.warn = (msg, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
// 合并options.modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// 合并options.directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives),
options.directives
)
}
// 拷贝其他options项目
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
const compiled = baseCompile(template, finalOptions)
compiled.errors = errors
compiled.tips = tips
return compiled
}
}
}

baseOptions

位于src/platforms/web/compiler/options.js

const modules = [
klass,
style,
model
]
const directives = {
model,
text,
html
}
const baseOptions: CompilerOptions = {
expectHTML: true,
modules, // 处理class类、style、v-model
directives, // 处理指令
isPreTag, // (tag: ?string): boolean => tag === 'pre'
isUnaryTag, // 和上一个属性类似,返回一些一元tag的函数
mustUseProp, // 一些需要绑定属性的标签,如<input checked /> 中的checked属性,是和数据绑定相关的
canBeLeftOpenTag, // 和上一个属性类似,返回一些自闭合的tag
isReservedTag, // 保留的html标签
getTagNamespace, // 针对svg、math标签
staticKeys: genStaticKeys(modules)
}

function baseCompile

位于src/compiler/index.js

function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// template --> ast
const ast = parse(template.trim(), options)
// transform ast
optimize(ast, options)
// ast --> code
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}

function parse

位于src/compiler/parser/index.js,作用是对options做进一步解析,为编译做最后的准备,以及为parseHTML提供上下文环境

function parse(template: string, options: CompilerOptions): ASTElement | void {
warn = options.warn || baseWarn
platformIsPreTag = options.isPreTag || no // no: (a, b, c) => false
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
// transforms、preTransforms、postTransforms在对属性的转化中将被调用
// 从options.modules列表中提取出键名为'transformNode'的值,组成新的列表
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
// 插值分隔符
delimiters = options.delimiters
// 编译栈 等编译上下文需要的变量
const stack = []
const preserveWhitespace = options.preserveWhitespace !== false
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false
function warnOnce(msg) {
if (!warned) {
warned = true
warn(msg)
}
}
// 处理开始标签
function start(tag, attrs, unary) {
// code
}
function end() {
// code
}
function chars() {
// code
}
function comment(text: string) {
currentParent.children.push({
type: 3,
text,
isComment: true
})
}
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
start,
end,
chars,
comment
})
return root
}

function start

// 对handleStartTag处理过的节点再做处理,基于匹配结果对象创建astElement
// 对开始节点的属性进行处理
function start(tag, attrs, unary) { // unary表示是否为一元自闭合标签
// 获取父节点的命名空间,或者如果tag === 'svg' || 'math'返回对应命名空间
const ns =
(currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// 创建astElement
/*
{
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent,
children: []
}
*/
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
// <style> 和 <script>标签
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
}
// 执行 pre-transforms,来自传入合并的baseOptions,处理input标签
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
processPre(element) // 判断是否包含'v-pre'属性,如果包含则将astElement.pre标记为true
if (element.pre) {
inVPre = true // 当前处于pre的编译环境
}
}
// (tag) => tag === 'pre'
if (platformIsPreTag(element.tag)) {
inPre = true
}
// 如果v-pre,则进行对应编译
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) { // element.processed标记当前astElement有没有经过处理
processFor(element) // 处理v-for
processIf(element) // 处理v-if、v-else 等
processOnce(element) // 处理v-once
processElement(element, options) // 处理其他属性
}
// 如果根节点还没有出现,则当前节点置为根节点
if (!root) {
root = element
} else if (!stack.length) {
// 允许根节点出现v-if v-else 等情况
if (root.if && (element.elseif || element.else)) {
// 添加if表达式
addIfCondition(root, {
exp: element.elseif,
block: element
})
}
}
if (currentParent && !element.forbidden) {
// 处理v-else
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) {
currentParent.plain = false
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[
name
] = element
} else {
currentParent.children.push(element)
element.parent = currentParent
}
}
// 非一元自闭合标签
if (!unary) {
currentParent = element // 标记之后的标签都是当前标签的子节点
stack.push(element) // 将当前标签入栈
} else {
endPre(element)
}
}

functions preTransforms

// 处理<input :type="type" v-model="txt" /> 这样的情况
// 对于input标签动态设置'type'属性同时包含'v-model'的情况
// 由ifCondition来最终决定type='checkbox' || 'radio' || 'other'
function preTransformNode (el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
const map = el.attrsMap
if (map['v-model'] && (map['v-bind:type'] || map[':type'])) {
const typeBinding: any = getBindingAttr(el, 'type')
const ifCondition = getAndRemoveAttr(el, 'v-if', true)
const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``
const hasElse = getAndRemoveAttr(el, 'v-else', true) != null // 是否存在'v-else'属性
const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true)
// 1. checkbox
const branch0 = cloneASTElement(el) // 克隆节点
// process for on the main node
processFor(branch0)
addRawAttr(branch0, 'type', 'checkbox') // 添加type=checkbox属性
processElement(branch0, options)
branch0.processed = true // prevent it from double-processed
branch0.if = `(${typeBinding})==='checkbox'` + ifConditionExtra
addIfCondition(branch0, {
exp: branch0.if,
block: branch0
})
// 2. 添加 radio else-if 语句
const branch1 = cloneASTElement(el)
getAndRemoveAttr(branch1, 'v-for', true)
addRawAttr(branch1, 'type', 'radio')
processElement(branch1, options)
addIfCondition(branch0, {
exp: `(${typeBinding})==='radio'` + ifConditionExtra,
block: branch1
})
// 3. other
const branch2 = cloneASTElement(el)
getAndRemoveAttr(branch2, 'v-for', true)
addRawAttr(branch2, ':type', typeBinding)
processElement(branch2, options)
addIfCondition(branch0, {
exp: ifCondition,
block: branch2
})
if (hasElse) {
branch0.else = true
} else if (elseIfCondition) {
branch0.elseif = elseIfCondition
}
return branch0
}
}
}

function processPre

// 判断当前astElement的attrsMap中是否包含'v-pre'属性
function processPre (el) {
if (getAndRemoveAttr(el, 'v-pre') != null) {
el.pre = true
}
}

processRawAttrs

// 直接将所有属性当作静态属性做处理
function processRawAttrs (el) {
const l = el.attrsList.length
if (l) {
const attrs = el.attrs = new Array(l)
for (let i = 0; i < l; i++) {
attrs[i] = {
name: el.attrsList[i].name,
value: JSON.stringify(el.attrsList[i].value)
}
}
} else if (!el.pre) {
el.plain = true
}
}

processFor

// v-for表达式: 'item in list' --> el.for = 'list', el.alias = 'item', el.iterator1 = undefined
// v-for表达式: '(item, index) in list' --> el.for = 'list', el.alias = 'item', el.iterator1 = 'index'
// v-for表达式: '(val, key, index) of obj' --> el.for = 'obj', el.alias = 'val', el.iterator1 = 'key', el.iterator2 = 'index'
function processFor (el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) { // 获取v-for属性
// /(.*?)\s+(?:in|of)\s+(.*)/
// 匹配'item in list' 或者'item of list'表达式
const inMatch = exp.match(forAliasRE)
el.for = inMatch[2].trim() // 表达式的'list'
// '(item, index)' --> 'item, index'
const alias = inMatch[1].trim().replace(stripParensRE, '')
// 匹配出遍历器'item, index' --> 'index'
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
// 从'item, index'中提取出'item'
el.alias = alias.replace(forIteratorRE, '')
// 从'item, index'中提取出'index'
el.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
el.iterator2 = iteratorMatch[2].trim()
}
} else {
el.alias = alias
}
}
}

processIf

// 处理v-if、v-else-if、v-else指令
// v-if:el.ifConditions = [] 添加if表达式
// v-else-if: el.elseif = elseif
// v-else: el.else = true
function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}

processOnce

function processOnce (el) {
const once = getAndRemoveAttr(el, 'v-once')
if (once != null) {
el.once = true
}
}

function processElement

function processElement (element: ASTElement, options: CompilerOptions) {
processKey(element) // 处理:key 属性
// 如果astElement不包含:key和其它属性,则标记element.plain = true
element.plain = !element.key && !element.attrsList.length
processRef(element) // 处理ref、v-bind:ref属性
processSlot(element) // 处理slot相关属性
processComponent(element) // 处理is、:is、inline-template这样的属性
// 执行transforms函数,来自于parse函数的baseOptions,
// 这两个函数分别用来处理class、:class和style、:style属性
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
}
processAttrs(element) // 处理属性列表
}
processKey
function processKey (el) {
const exp = getBindingAttr(el, 'key') // 获取属性v-bind:key或:key
if (exp) {
el.key = exp
}
}
processRef
function processRef (el) {
const ref = getBindingAttr(el, 'ref')
if (ref) {
el.ref = ref
el.refInFor = checkInFor(el) // 检查是否ref属性同时出现在v-for的节点,这种情况需要特殊处理
}
}
function checkInFor (el: ASTElement): boolean {
let parent = el
while (parent) {
if (parent.for !== undefined) {
return true
}
parent = parent.parent
}
return false
}
processSlot
function processSlot (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name')
} else {
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
el.slotScope = slotScope
}
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget)
}
}
}
}
processComponent
// 处理<template :is="com">的情况
function processComponent (el) {
let binding
if ((binding = getBindingAttr(el, 'is'))) {
el.component = binding
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true
}
}
processAttrs
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) { // 匹配以“v-、@、:”开头的属性名,即动态属性
// 如果匹配到上述,则标记当前astElement为动态
el.hasBindings = true
// 修饰符:'@click.a.b' --> {a: true, b: true}
// v-a:b.c.d --> {c: true, d: true} v-a为指令,b为参数,c、d为修饰符
modifiers = parseModifiers(name) // 处理类似 @click.capture\@click.once之类的修饰符capture、once之类的
if (modifiers) {
name = name.replace(modifierRE, '') // 将属性名去除修饰符
}
if (bindRE.test(name)) { // 匹配以“:、v-bind:”开头的属性名
name = name.replace(bindRE, '') // 属性名去除':'、'v-bind'
value = value
isProp = false
if (modifiers) {
if (modifiers.prop) {
isProp = true
name = camelize(name) // 'a-b' --> 'aB'
if (name === 'innerHtml') name = 'innerHTML'
}
if (modifiers.camel) {
name = camelize(name)
}
// 处理sync修饰符
if (modifiers.sync) {
addHandler(
el,
`update:${camelize(name)}`,
genAssignmentCode(value, `$event`)
)
}
}
if (isProp || (
!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
)) {
addProp(el, name, value)
} else {
addAttr(el, name, value)
}
} else if (onRE.test(name)) { // 匹配以“@、v-on”开头的属性名,处理事件绑定
name = name.replace(onRE, '')
addHandler(el, name, value, modifiers, false, warn) // 绑定事件
} else { // 处理普通指令、自定义指令
// 去除'v-': 'v-a:b' ---> 'a:b'
name = name.replace(dirRE, '')
// 匹配指令参数:'a:b' ---> [":b", "b", index: 1, input: "a:b", groups: undefined]
const argMatch = name.match(argRE)
// 分离出指令参数
const arg = argMatch && argMatch[1]
if (arg) {
name = name.slice(0, -(arg.length + 1)) // 截取指令name
}
addDirective(el, name, rawName, value, arg, modifiers)
}
} else {
addAttr(el, name, JSON.stringify(value)) // 将{name: 'id', value: 'demo'} push加入attrs属性数组
// #6887 firefox doesn't update muted state if set via attribute
// even immediately after element creation
if (!el.component &&
name === 'muted' &&
platformMustUseProp(el.tag, el.attrsMap.type, name)) {
addProp(el, name, 'true')
}
}
}
}
addAttr
function addAttr (el: ASTElement, name: string, value: string) {
(el.attrs || (el.attrs = [])).push({ name, value })
}
addProp
function addProp (el: ASTElement, name: string, value: string) {
(el.props || (el.props = [])).push({ name, value })
}
addHandler
// 处理事件,将事件保存到el.events = {click: {value: 'handleClick'}} handleClick为事件回调
// 带native修饰符:el.nativeEvents = {click: {value: 'handleClick'}}
function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: Function
) {
modifiers = modifiers || emptyObject
// capture修饰符最终转化为'!name'
if (modifiers.capture) {
delete modifiers.capture
name = '!' + name
}
// once修饰符最终转化为'~name'
if (modifiers.once) {
delete modifiers.once
name = '~' + name
}
// passive修饰符最终转化为'&name'
if (modifiers.passive) {
delete modifiers.passive
name = '&' + name
}
if (name === 'click') {
if (modifiers.right) { // 右键事件
name = 'contextmenu'
delete modifiers.right
} else if (modifiers.middle) { // 中键事件
name = 'mouseup'
}
}
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = { value }
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
const handlers = events[name]
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
}
addDirective
function addDirective (
el: ASTElement,
name: string,
rawName: string,
value: string,
arg: ?string,
modifiers: ?ASTModifiers
) {
(el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers })
}
functions transforms
function transformNode (el: ASTElement, options: CompilerOptions) {
const warn = options.warn || baseWarn
const staticClass = getAndRemoveAttr(el, 'class') // 获取astElement的静态class属性
if (staticClass) {
el.staticClass = JSON.stringify(staticClass)
}
const classBinding = getBindingAttr(el, 'class', false)// 获取:class属性
if (classBinding) {
el.classBinding = classBinding
}
}
function transformNode (el: ASTElement, options: CompilerOptions) {
const warn = options.warn || baseWarn
const staticStyle = getAndRemoveAttr(el, 'style') // 获取astElement的静态style属性
const styleBinding = getBindingAttr(el, 'style', false /* getStatic */) // 获取:style属性
if (styleBinding) {
el.styleBinding = styleBinding
}
}

function chars

// 处理文本节点或者插值变量
function chars(text: string) {
const children = currentParent.children // 当前父节点的children
text =
inPre || text.trim()
? isTextTag(currentParent)
? text
: decodeHTMLCached(text) // 转移html字符串,如'文本&lt' --> '文本<'
: // only preserve whitespace if its not right after a starting tag
preserveWhitespace && children.length
? ' '
: ''
if (text) {
let expression
if (
// 非pre模式
!inVPre &&
text !== ' ' &&
// 以分隔符delimiters(默认'{{}}'匹配是否包含插值变量)
// 将插值字符转化为插值表达式,'{{count}}' --> '_s(count)'
// '_s'为Vue中预定义的转化插值变量的方法,后面再介绍
(expression = parseText(text, delimiters))
) {
// 包含插值表达式的文本节点
children.push({
type: 2,
expression,
text
})
} else if (
text !== ' ' ||
!children.length ||
children[children.length - 1].text !== ' '
) {
// 静态文本节点
children.push({
type: 3,
text
})
}
}
}

function parseText

// 处理插值变量
function parseText (
text: string,
delimiters?: [string, string]
): string | void {
// 根据传入分隔符创建正则,或者使用默认正则
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
const tokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index
// 使用分隔符正则匹配文本字符串,从中匹配出插值变量并转化为插值表达式字符串
while ((match = tagRE.exec(text))) {
index = match.index
// 静态文本节点
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
// 变量表达式
const exp = match[1].trim()
tokens.push(`_s(${exp})`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return tokens.join('+')
}

function end

function end() {
// 取出栈顶节点
const element = stack[stack.length - 1]
// 获取栈顶节点最后一个子节点
const lastNode = element.children[element.children.length - 1]
// 如果最后一个子节点是一个文本节点,且其值为' ',同时不是处于pre环境则丢弃它
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop()
}
// 弹出栈顶元素,表示该元素编译已完成
// 将currentParent置为下一个元素,表示接下来编译是在其内部
stack.length -= 1
currentParent = stack[stack.length - 1]
endPre(element) // 结束pre状态
}
function endPre(element) {
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
}

工具方法

pluckModuleFunction

// modules中的每一项提取出键值为key的属性,同时该项非空
// 例如:[{a: 'a', b: 'b'}, {a: 'aa', b: 'bb'}] --> ['a', 'aa']
function pluckModuleFunction<F: Function> (
modules: ?Array<Object>,
key: string
): Array<F> {
return modules
? modules.map(m => m[key]).filter(_ => _)
: []
}

getAndRemoveAttr

// 获取astElement 的attrsMap中的某个属性
// 同时通过传入removeFromMap参数标记是否移除它
function getAndRemoveAttr (
el: ASTElement,
name: string,
removeFromMap?: boolean // 是否移除当前属性
): ?string {
let val
if ((val = el.attrsMap[name]) != null) {
const list = el.attrsList
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list.splice(i, 1)
break
}
}
}
if (removeFromMap) {
delete el.attrsMap[name]
}
return val
}

getBindingAttr

// 获取动态绑定的属性
// 如果v-bind:key、:key
// 也可以通过参数getStatic参数标记是否降级获取静态属性
function getBindingAttr (
el: ASTElement,
name: string,
getStatic?: boolean
): ?string {
const dynamicValue =
getAndRemoveAttr(el, ':' + name) ||
getAndRemoveAttr(el, 'v-bind:' + name)
if (dynamicValue != null) {
return dynamicValue
} else if (getStatic !== false) {
const staticValue = getAndRemoveAttr(el, name)
if (staticValue != null) {
return JSON.stringify(staticValue)
}
}
}

function parseHTML

作用是将template转化为astElement

function parseHTML(html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no // const no = (a?: any, b?: any, c?: any) => false
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0
let last, lastTag
// 遍历匹配传入模板字符串html
while (html) {
last = html
// plaintextElement: script、style、textarea
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
// 如果'<'在字符串第一个字符,表明为标签起始位置或者结束位置
if (textEnd === 0) {
// /^<!--/匹配注释节点,进行处理
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 如果编译结果需要保留注释节点,则调用在parse声明的comment方法进行转化
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3) // 截去处理过的字符
continue
}
}
// /^<!\[/ 匹配处理conditionalComment
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// /^<!DOCTYPE [^>]+>/i 匹配处理doctype
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// /^<\/((?:[a-zA-Z_][\w\-\.]*\:)?[a-zA-Z_][\w\-\.]*)[^>]*>/ 匹配处理结束标签
// 如:</p>
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
// 对于匹配的结束标签调用本方法内声明的parseEndTag进行处理
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 匹配开始标签,调用本方法中声明的handleStartTag方法进行处理
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
continue
}
}
// 如果以上正则匹配都未成功,且经过前面的匹配处理'<'字符的位置不在剩余模板字符串的首位
// 则可能是这种情况:“文本节点</p>”或者“{{count}}</p>”
// 接下来处理文本节点或者插值变量
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd) // 截去'<'前面的内容
while (
!endTag.test(rest) && // 不匹配结束标签
!startTagOpen.test(rest) && // 不匹配开始标签
!comment.test(rest) && // 不匹配注释节点
!conditionalComment.test(rest) // 不匹配conditionalComment
) {
// 如果剩余部分除了首字符'<'仍存在,则将前面的'<'也当作文本处理
// 具体实例类似剩余html为'a<b+c</p>',这里indexOf('<') === 1但是前面的匹配又不会通过
// 到这里实际上'a<b+c'应该是<p>标签内的文本
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd) // 文本节点为截取'<'前面的内容
advance(textEnd)
}
// 如果剩余html不包含'<'则认为剩余的html为纯文本
if (textEnd < 0) {
text = html
html = ''
}
// 处理文本节点
if (options.chars && text) {
options.chars(text) // 调用parse中声明的参数函数chars
}
}
// 一次遍历开始,但是剩余的html中'<'字符位置不在首位的情况
else {
let endTagLength = 0
const stackedTag = lastTag.toLowerCase()
const reStackedTag =
reCache[stackedTag] ||
(reCache[stackedTag] = new RegExp(
'([\\s\\S]*?)(</' + stackedTag + '[^>]*>)',
'i'
))
const rest = html.replace(reStackedTag, function(all, text, endTag) {
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!--([\s\S]*?)-->/g, '$1')
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
if (options.chars) {
options.chars(text)
}
return ''
})
index += html.length - rest.length
html = rest
parseEndTag(stackedTag, index - endTagLength, index)
}
if (html === last) {
options.chars && options.chars(html)
break
}
}
// 结束while循环进行清理
parseEndTag()
}

工具方法advance

// 向后移动n个位置,并将html的前面n个字符截去
function advance(n) {
index += n
html = html.substring(n)
}

function parseStartTag

// 匹配开始标签,并截去匹配过的字符,返回匹配结果传递给handleStartTag进行进一步处理
function parseStartTag() {
// /^<((?:[a-zA-Z_][\w\-\.]*\:)?[a-zA-Z_][\w\-\.]*)/
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1], // 标签名
attrs: [], // 属性列表
start: index // 保存的是当前匹配节点在template字符串中的开始位置
}
advance(start[0].length)
let end, attr
while (
// /^\s*(\/?)>/ 匹配标签结束标志如:'p>'、'/>'
!(end = html.match(startTagClose)) &&
// /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 匹配标签属性如:type="text"
(attr = html.match(attribute))
) {
// 将匹配的属性推入attrs列表
advance(attr[0].length)
match.attrs.push(attr)
}
if (end) { // end表示匹配的结束标志
match.unarySlash = end[1] // 表示自闭合标签<img />
advance(end[0].length)
match.end = index // 保存的是当前匹配节点在template字符串中的开始位置
return match
}
}
}

function handleStartTag

// 进一步处理parseStartTag方法的匹配结果
function handleStartTag(match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) { // expectHTML === true
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
// "colgroup、dd、dt、li、options、p、td、tfoot、th、thead、tr、source"
// 上述标签
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
// "area、base、br、col、embed、frame、hr、img、input、isindex、keygen、link、meta、param、source、track、wbr"
// 上述标签默认是一元自闭合标签
const unary = isUnaryTag(tagName) || !!unarySlash
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines =
tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
}
// 非一元自闭合标签将被推入stack栈,lastTag变量将被赋值为当前标签名
// 这样做的目的主要是表示后续匹配的标签节点以及文本节点都作为当前节点的子节点
// 直到匹配到当前节点的结束标志将当前标签出栈结束本节点的匹配转化
if (!unary) {
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs
})
lastTag = tagName
}
// 调用parse中声明的start方法
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}

function parseEndTag

function parseEndTag(tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
}
// 从上向下遍历栈stack,找出和当前结束标签tagName相同的开始标签的位置pos
if (tagName) {
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
pos = 0
}
if (pos >= 0) {
// 将栈stack中pos上面的开始标签全部end
for (let i = stack.length - 1; i >= pos; i--) {
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// 移除pos位置上方的所有开始标签
// 将lastTag置为pos - 1位置的开始标签
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}

流程回顾

上面对template–> ast过程中设计的方法都粗浅得进行了注释,但是缺乏整体性,下面尝试从整体流程的角度进行一次分析

流程图:

parseHTML流程.jpg

编译环境

模板编译过程中有两个编译执行环境:parseparseHTML

parse提供了startendchars方法分别用来处理开始标签、结束标签和文本节点,同时其有内部变量stack(后面称parse-stack)、currentParentinPre分别是节点astElement栈、当前节点的父节点、pre环境。

parseHTML提供了handleStartTaghandleEndTag方法分别用来匹配开始标签和结束标签,同时其内部变量stack(后面成parseHTML-stack)、indexlastTag分别是开始节点栈、当前匹配结束点、上一个匹配的标签。

编译流程

匹配到开始标签

依次经过handleStartTagstart进行处理

  1. parseHTML环境下handleStartTag进行处理
    1. 匹配出tag
    2. 匹配出属性attrs
    3. 标签描述对象推入parseHTML-stacklastTag置为当前匹配的tagName
  2. parse环境下start进行处理
    1. 基于tagattrscurrentParent创建astElement
    2. 如果标签包含’v-pre’属性或者为pre标签则将inPre置为true
    3. 调用属性处理方法处理属性
    4. 将当前astElement推入currentParent.children同时astElement.parent = currentParent
    5. currentParent置为当前astElement并且将当前astElement推入parse-stack

匹配到文本节点

经过chars进行处理,parse环境下

  1. 将插值变量转化为插值表达式推入currentParent.children
  2. 将普通文本节点转化为描述对象推入currentParent.children

匹配到结束标签

依次经过handleEndTagend进行处理:

  1. parseHTML环境下handleEndTag
    1. 从上向下遍历parseHTML-stack找出第一个和当前结束标签具有相同标签名的开始标签的位置pos
    2. 如果pos的位置不是parseHTML-stack的顶部位置,则表示在pos上方的开始标签都缺失结束标签,则给予warn,同时依次调用end结束未闭合的标签
    3. 如果pos位置等于parseHTML-stack.length - 1,则表示开始标签刚好匹配结束标签,调用end结束
    4. 如果未找到对应的开始标签,即pos === -1,如果tag为br或者p则当作开始标签处理
    5. parseHTML-stack中移除已经匹配到结束标签的开始标签:parseHTML-stack.length = poslastTag赋值为最上面的开始标签
  2. parse环境下调用end
    1. 找到parse-stack栈顶astElement:element
    2. 找到element的最后一个child:lastNode
    3. 如果lastNode.text === ' '且非pre且为文本节点,则将其移除
    4. 弹出parse-stack栈顶元素,currentParent置为新的栈顶元素

原创文章,作者:我心飞翔,如若转载,请注明出处:https://www.pipipi.net/14729.html

发表评论

登录后才能评论