Vue parse之 从template到astElement 源码详解
栏目: JavaScript · 发布时间: 5年前
内容简介:在紧张的一个星期的整理,笔者的前端小组每个人都整理了一篇文章,笔者整理了本文介绍的是Vue编译中的parse部分的源码分析,也就是从template 到 astElemnt的解析到程。从笔者的Vue编译思想详解一文中,我们已经知道编译个四个流程分别为parse、optimize、code generate、render。具体细节这里不做赘述,附上之前的一张图。
在紧张的一个星期的整理,笔者的前端小组每个人都整理了一篇文章,笔者整理了 Vue编译模版到虚拟树
的思想这一篇幅。建议读者看到这篇之前,先点击这里预习一下整个流程的思想和思路。
本文介绍的是Vue编译中的parse部分的源码分析,也就是从template 到 astElemnt的解析到程。
正文
从笔者的Vue编译思想详解一文中,我们已经知道编译个四个流程分别为parse、optimize、code generate、render。具体细节这里不做赘述,附上之前的一张图。
本文则旨在从思想落实到源代码分析,当然只是针对 parse
这一部分的。
一、 源码结构。
笔者先列出我们在看源码之前,需要先预习的一些概念和准备。
准备
1.正则
parse的最终目标是生成具有众多舒心的astElement,而这些属性有很多则摘自标签的一些属性。 如 div上的v-for、v-if、v-bind等等,最终都会变成astElement的节点属性。 这里先给个例子:
<div v-for="(item,index) in options" :key="item.id"></div>
到
{ alias: "item" attrsList: [], attrsMap: {"v-for": "(item,index) in options", :key: "item.id"}, children: (2) [{…}, {…}], end: 139, for: "options", iterator1: "index", key: "item.id", parent: {type: 1, tag: "div", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …}, plain: false, rawAttrsMap: {v-for: {…}, :key: {…}}, start: 15, tag: "div", type: 1, } 复制代码
可以看到v-for的属性已经被解析和从摘除出来,存在于astElement的多个属性上面了。而 摘除
的这个功能就是出自于正则强大的力量。下面先列出一些重要的正则预热。
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 重要1 const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 重要二 const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*` const qnameCapture = `((?:${ncname}\\:)?${ncname})` const startTagOpen = new RegExp(`^<${qnameCapture}`) const startTagClose = /^\s*(\/?)>/ const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) const doctype = /^<!DOCTYPE [^>]+>/i // #7298: escape - to avoid being pased as HTML comment when inlined in page const comment = /^<!\--/ const conditionalComment = /^<!\[/ export const onRE = /^@|^v-on:/ export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\./ : /^v-|^@|^:/ export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const stripParensRE = /^\(|\)$/g // 在v-for中去除 括号用的。 const dynamicArgRE = /^\[.*\]$/ // 判断是否为动态属性 const argRE = /:(.*)$/ // 配置 :xxx export const bindRE = /^:|^\.|^v-bind:/ // 匹配bind的数据,如果在组件上会放入prop里面 否则放在attr里面。 const propBindRE = /^\./ const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g const slotRE = /^v-slot(:|$)|^#/ const lineBreakRE = /[\r\n]/ const whitespaceRE = /\s+/g const invalidAttributeRE = /[\s"'<>\/=]/ 复制代码
正则基础不太好的同学可以先学两篇正则基础文章,特别详细:
并且附带上两个网站,供大家学习正则。
一次性看到这么多正则是不是有点头晕目眩。不要慌,这里给大家详细讲解下比较复杂多正则。
1)获取属性多正则
attribute 和 dynamicArgAttribute 分别获取普通属性和动态属性的正则表达式。 普通属性大家一定十分属性了,这里对动态属性做下解释。
动态属性,就是key值可能会发生变动对属性,vue对写法如 v-bind:[attrName]="attrVal"
相当于控制你想要传递对参数。
我们先对 attribute
做一个详细对讲解:
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"] )"+|'([^'] )'+|([^\s"'=<>`]+)))?/ 一共分为五个分组:
- 1.([^\s"'<>/=]+) 不匹配 空格、<、>、/、= 等符号。 因为我们配置对是属性。
- 2.\s*(=)\s* 这个是 匹配 = 号,当然了空格页一并匹配了。
- 3."([^"] )" 、'([^'] )' 、([^\s"'=<>`]+) . 这三个则分别匹配三种情况 "val" 、'val' 、val。
这样的话应该比较清晰了,我们来概括下:
attribute匹配的一共是三种情况, name="xxx" name='xxx' name=xxx。
能够保证属性的所有情况都能包含进来。 需要注意的是正则处理后的数组的格式是:
['name','=','val','',''] 或者 ['name','=','','val',''] 或者 ['name','=','','','val'] 复制代码
正则的图:
而关于dynamicArgAttribute, 则是大同小异:
主要是多了 \[[^=]+\][^\s"'<>\/=]*
也就是 [name] 或者 [name]key 这类情况,附上正则详解图:
2)标签处理正则
标签主要包含开始标签 (如 <div>
)和结束标签(如 </div>
),正则分别为以下两个:
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*` const qnameCapture = `((?:${ncname}\\:)?${ncname})` const startTagOpen = new RegExp(`^<${qnameCapture}`) const startTagClose = /^\s*(\/?)>/ const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) 复制代码
能够看到标签的匹配是以qnameCapture为基础的,那么这玩意又是啥呢? 其实qname就是类似于 xml:xxx 的这类带冒号的标签,所以startTagOpen是匹配 <div
或 <xml:xxx
的标签。 endTag匹配的是如 </div>或</xml:xxx>
的标签
3)处理vue的标签
export const onRE = /^@|^v-on:/ 处理绑定事件的正则 export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\./ // v- | @click | :name | .stop 指令匹配 : /^v-|^@|^:/ 复制代码
for 标签比较重要,匹配也稍微复杂点,这里做个详解:
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ 复制代码
首先申明这里的正则是依赖于attribute正则的,我们会拿到v-for里面的内容,举个例子 v-for="item in options"
,我们最终会处理成一个map的形式,大致如下:
const element = { attrMap: { 'v-for':'item in options', ... } } 复制代码
先看 forAliasRE
的分组,一共两个分组分别([\s\S] ?)和([\s\S] ) 会分别匹配 item 和 options。 但是 in或of之前内容可能是比较复杂的,如(value,key) 或者(item,index)等,这个时候就是forIteratorRE开始起作用了。 它一共两个分组都是([^,}]]*),其实就是拿到alias的最后两个参数,大家都知道对于Object我们是可以这么做的:
<div v-for="(value,key,index)"> 复制代码
而数组则是为了获取key 和index的。最终会放在astElement的iterator1 和 iterator2。
好了关于正则就说这么多了,具体的情况还是得自己去看看源码的。
2.源码结构
依然是在开始讲源码前,先大致介绍下源码的结构。先贴个代码出来
function parse() { 模块一:初始化需要的方法 模块二: 初始化所有标记 模块三: 开始识别并创建 astElement 树。 } 复制代码
模块一大致是:
platformIsPreTag = options.isPreTag || no //判断是否为 pre 标签 platformMustUseProp = options.mustUseProp || no // 判断某个属性是否是某个标签的必要属性,如selected 对于option platformGetTagNamespace = options.getTagNamespace || no // 判断是否为 svg or math标签 对函数 const isReservedTag = options.isReservedTag || no // 判断是否为该平台对标签,目前vue源码只有 web 和weex两个平台。 maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag) //是否可能为组件 transforms = pluckModuleFunction(options.modules, 'transformNode') // 数组,成员是方法, 用途是摘取 staticStyle styleBinding staticClass classBinding preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') // ?? postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') // ?? delimiters = options.delimiters // express标志 function closeElement() {...} // 处理astElement对结尾函数 function trimEndingWhitespace() {...} // 处理尾部空格 function checkRootConstraints() {...} // 检查root标签对合格性 复制代码
模块二大致为:
const stack = [] // 配合使用的栈 主要目的是为了完成树状结构。 let root // 根节点记录,树顶 let currentParent // 当前父节点 let inVPre = false // 标记是否在v-pre节点 当中 let inPre = false // 是否在pre标签当中 let warned = false 复制代码
模块三大致为:
parseHTML(template,options) 复制代码
options 是关键,包括很多平台配置和 传入的四个处理方法。大致如下:
options = { warn, expectHTML: options.expectHTML, // 是否期望和浏览器器保证一致。 isUnaryTag: options.isUnaryTag, // 是否为一元标签的判断函数 canBeLeftOpenTag: options.canBeLeftOpenTag, // 可以直接进行闭合的标签 shouldDecodeNewlines: options.shouldDecodeNewlines, shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, shouldKeepComment: options.comments, outputSourceRange: options.outputSourceRange, // 这里分开,上面是平台配置、下面是处理函数。 start, (1) end, (2) chars, (3) commend (4) } 复制代码
笔者之前的parse思想,已经介绍过两个处理函数start和end了,一个是创建astElement另一个是建立父子关系,其中细节会在下文中,详细介绍,这也是本文的重点。切记这四个函数至关重要,下面会用代号讲解。
二、各模块重点功能。
Vue的html解析并非一步到位,先来介绍一些重点的函数功能
1.parseHTML函数功能。
(1)解析开始标签和 处理属性,生成初始化match。代码如下:
/** * 创建match数据结构 * 初始化的状态 * 只有 * tagName * attrs * attrs自己是个数组 也就是 正则达到的效果。。 * start * end */ function parseStartTag () { const start = html.match(startTagOpen) if (start) { const match = { // 匹配startTag的数据结构 tagName: start[1], attrs: [], start: index } advance(start[0].length) let end, attr // 取属性值 while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { attr.start = index advance(attr[0].length) attr.end = index match.attrs.push(attr) } if (end) { match.unarySlash = end[1] // 是否为 一元标记 直接闭合 advance(end[0].length) match.end = index return match } } } 复制代码
parseStartTag的目标是比较原始的,获得类似于
const match = { // 匹配startTag的数据结构 tagName: 'div', attrs: [ { 'id="xxx"','id','=','xxx' }, ... ], start: index, end: xxx } 复制代码
match大致可以概括为获取标签、属性和位置信息。并将此传递给下个函数。
(2)handleStartTag处理parseStartTag传递过来的match。
// parseStartTag 拿到的是 match function handleStartTag (match) { const tagName = match.tagName const unarySlash = match.unarySlash if (expectHTML) { // 是否期望和浏览器的解析保持一致。 if (lastTag === 'p' && isNonPhrasingTag(tagName)) { parseEndTag(lastTag) } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { parseEndTag(tagName) } } const unary = isUnaryTag(tagName) || !!unarySlash // 一元判断 const l = match.attrs.length const attrs = new Array(l) for (let i = 0; i < l; i++) { // 将attrs的 数组模式变成 { name:'xx',value:'xxx' } 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) } if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { attrs[i].start = args.start + args[0].match(/^\s*/).length attrs[i].end = args.end } } if (!unary) { // 非一元标签处理方式 stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end }) lastTag = tagName } if (options.start) { options.start(tagName, attrs, unary, match.start, match.end) } } 复制代码
handleStartTag的本身效果其实非常简单直接,就是吧match的attrs重新处理,因为之前是数组结构,在这里他们将所有的数组式attr变成一个对象,流程大致如下:
从这样:
attrs: [ { 'id="xxx"','id','=','xxx' }, ... ], 复制代码
变成这样:
attrs: [ {name='id',value='xxx' }, ... ], 复制代码
那么其实还有些特殊处理 expectHTML
和 一元标签
。
expectHTML
是为了处理一些异常情况。如 p标签的内部出现div等等、浏览器会特殊处理的情况,而Vue会尽量和浏览器保持一致。具体参考p标签标准。
最后handleStartTag会调用 从parse传递的start(1)函数来做处理,start函数会在下文中有详细的讲解。
(3) parseEndTag
parseEndTag本身的功能特别简单就是直接调用options传递进来的end函数,但是我们观看源码的时候会发现源码还蛮长的。
function parseEndTag (tagName, start, end) { let pos, lowerCasedTagName if (start == null) start = index if (end == null) end = index // Find the closest opened tag of the same type if (tagName) { lowerCasedTagName = tagName.toLowerCase() for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { // If no tag name is provided, clean shop pos = 0 } if (pos >= 0) { // Close all the open elements, up the stack for (let i = stack.length - 1; i >= pos; i--) { if (process.env.NODE_ENV !== 'production' && (i > pos || !tagName) && options.warn ) { options.warn( `tag <${stack[i].tag}> has no matching end tag.`, { start: stack[i].start, end: stack[i].end } ) } if (options.end) { options.end(stack[i].tag, start, end) } } // Remove the open elements from the stack 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) } } } } 复制代码
看起来还蛮长的,其实主要都是去执行options.end, Vue的源码有很多的代码量都是在处理特殊情况,所以看起来很臃肿。这个函数的特殊情况主要有两种:
- 1.编写者失误,有标签没有闭合。会直接一次性和检测的闭合标签一起进入options.end。 如:
<div> <span> <p> </div> 复制代码
在处理div的标签时,根据pos的位置,将pos之前的所有标签和匹配到的标签都会一起遍历的去执行end函数。
-
- p标签和br标签
可能会遇到 </p>
和 </br>
标签 这个时候 p标签会走跟浏览器自动补全效果,先start再end。 而br则是一元标签,直接进入end效果。
2.start、end、comment、chars四大函数。
1)start函数
start函数非常长。这里截取重点部分
start() { ... let element: ASTElement = createASTElement(tag, attrs, currentParent) ... if (!inVPre) { processPre(element) if (element.pre) { inVPre = true } } if (platformIsPreTag(element.tag)) { inPre = true } if (inVPre) { processRawAttrs(element) } else if (!element.processed) { // structural directives processFor(element) processIf(element) processOnce(element) } if (!root) { root = element if (process.env.NODE_ENV !== 'production') { checkRootConstraints(root) } } if (!unary) { currentParent = element stack.push(element) } else { closeElement(element) } } 复制代码
- 1).创建astElement节点。
结构如下:
{ type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent, children: [] } 复制代码
-
2)处理属性 当然在这里只是处理部分属性,且分为两种情况:
(1)pre模式 直接摘取所有属性
(2)普通模式 分别处理processFor(element) 、processIf(element) 、 processOnce(element)。
2)end函数
end函数非常短
end (tag, start, end) { const element = stack[stack.length - 1] // pop stack stack.length -= 1 currentParent = stack[stack.length - 1] if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { element.end = end } closeElement(element) }, 复制代码
end函数第一件事就是取出当前栈的父元素赋值给currentParent,然后执行closeElement,为的就是能够创建完整的树节点关系。 所以closeElement才是end函数的重点。
下面详细解释下closeElement
function closeElement (element) { trimEndingWhitespace(element) // 去除 未部对空格元素 if (!inVPre && !element.processed) { element = processElement(element, options) // 处理Vue相关对一些属性关系 } // tree management if (!stack.length && element !== root) { // allow root elements with v-if, v-else-if and v-else if (root.if && (element.elseif || element.else)) { if (process.env.NODE_ENV !== 'production') { checkRootConstraints(element) } addIfCondition(root, { // 处理root到 条件展示 exp: element.elseif, block: element }) } else if (process.env.NODE_ENV !== 'production') { warnOnce( `Component template should contain exactly one root element. ` + `If you are using v-if on multiple elements, ` + `use v-else-if to chain them instead.`, { start: element.start } ) } } if (currentParent && !element.forbidden) { if (element.elseif || element.else) { // 处理 elseif else 块级 processIfConditions(element, currentParent) } else { if (element.slotScope) { // 处理slot, 将生成的各个slot的astElement 用对象展示出来。 // 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 } } // final children cleanup // filter out scoped slots element.children = element.children.filter(c => !(c: any).slotScope) // remove trailing whitespace node again trimEndingWhitespace(element) // check pre state if (element.pre) { inVPre = false } if (platformIsPreTag(element.tag)) { inPre = false } // apply post-transforms for (let i = 0; i < postTransforms.length; i++) { postTransforms[i](element, options) } } 复制代码
主要是做了五个操作:
- 1.processElement。
processElement是closeElement非常重要的一个处理函数。先把代码贴出来。
export function processElement ( element: ASTElement, options: CompilerOptions ) { processKey(element) // determine whether this is a plain element after // removing structural attributes element.plain = ( !element.key && !element.scopedSlots && !element.attrsList.length ) processRef(element) processSlotContent(element) processSlotOutlet(element) processComponent(element) for (let i = 0; i < transforms.length; i++) { element = transforms[i](element, options) || element } processAttrs(element) return element } 复制代码
可以看到主要是processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs和最后一个遍历的执行的transforms。
processSlotContent是处理展示在组件内部的slot,但是在这个地方只是简单的将给el添加两个属性作用域插槽的slotScope和 slotTarget,也就是目标slot。 processSlotOutlet,则是简单的摘取 slot元素上面的name,并赋值给slotName。 processComponent 并不是处理component,而是摘取动态组件的is属性。 processAttrs是获取所有的属性和动态属性。
transforms是处理class和style的函数数组。这里不做赘述了。
- 2.添加elseif 或else的block。
最终生成的的ifConditions块级的格式大致为:
[ { exp:'showToast', block: castElement1 }, { exp:'showOther', block: castElement2 }, { exp: undefined, block: castElement3 } ] 复制代码
这里会将条件展示处理成一个数组,exp存放所有的展示条件,如果是else 则为undefined。
- 3.处理slot,将各个slot对号入座到一个对象scopedSlots。
processElement完成的slotTarget的赋值,这里则是将所有的slot创建的astElement以对象的形式赋值给currentParent的scopedSlots。以便后期组件内部实例话的时候可以方便去使用vm. slot的初始化。
- 4.处理树到父子关系,element.parent = currentParent。
- 5.postTransforms
不做具体介绍了,感兴趣的同学自己去研究下吧。
3)chars函数
chars(){ ... const children = currentParent.children ... if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { child = { type: 2, expression: res.expression, tokens: res.tokens, text } } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { child = { type: 3, text } } } 复制代码
chars主要处理两中文本情况,静态文本和表达式,举个例子:
<div>name</div> 复制代码
name就是静态文本,创建的type为3.
<div>{{name}}</div> 复制代码
而在这个里面name则是表达式,创建的节点type为2。
做个总结就是:普通tag的type为1,纯文本type为2,表达式type为3。
4)comment函数比较简单
comment (text: string, start, end) { // adding anyting as a sibling to the root node is forbidden // comments should still be allowed, but ignored if (currentParent) { const child: ASTText = { type: 3, text, isComment: true } if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { child.start = start child.end = end } currentParent.children.push(child) } } 复制代码
也是纯文本,只是节点加上了一个isComment:true的标志。
三、整体流程总结。
普通标签处理流程描述
- 1.识别开始标签,生成匹配结构match。
const match = { // 匹配startTag的数据结构 tagName: 'div', attrs: [ { 'id="xxx"','id','=','xxx' }, ... ], start: index, end: xxx } 复制代码
- 2.处理attrs,将数组处理成 {name:'xxx',value:'xxx'}
- 3.生成astElement,处理for,if和once的标签。
- 4.识别结束标签,将没有闭合标签的元素一起处理。
- 5.建立父子关系,最后再对astElement做所有跟Vue 属性相关对处理。slot、component等等。
文本或表达式的处理流程描述。
- 1、截取符号<之前的字符串,这里一定是所有的匹配规则都没有匹配上,只可能是文本了。
- 2、使用chars函数处理该字符串。
- 3、判断字符串是否含有delimiters,默认也就是${},有的话创建type为2的节点,否则type为3.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- JDK SPI源码详解
- 【zookeeper源码】启动流程详解
- 详解RunLoop之源码分析
- 详解CopyOnWrite容器及其源码
- React Scheduler 源码详解(1)
- React Scheduler 源码详解(2)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员修炼之道(影印版)
Andrew Hunt、David Thomas / 中国电力出版社 / 2003-8-1 / 39.00
本书直击编程陈地,穿过了软件开发中日益增长的规范和技术藩篱,对核心过程进行了审视——即根据需求,创建用户乐于接受的、可工作和易维护的代码。本书包含的内容从个人责任到职业发展,直至保持代码灵活和易于改编重用的架构技术。从本书中将学到防止软件变质、消除复制知识的陷阱、编写灵活、动态和易适应的代码、避免出现相同的设计、用契约、断言和异常对代码进行防护等内容。一起来看看 《程序员修炼之道(影印版)》 这本书的介绍吧!
XML 在线格式化
在线 XML 格式化压缩工具
HSV CMYK 转换工具
HSV CMYK互换工具