vue html end tags,Vue parse之 从template到astElement 源码详解

d28f3cac9bd474dc3556c51f74c44967.png

前奏

在紧张的一个星期的整理,笔者的前端小组每个人都整理了一篇文章,笔者整理了Vue编译模版到虚拟树的思想这一篇幅。建议读者看到这篇之前,先点击这里预习一下整个流程的思想和思路。

本文介绍的是Vue编译中的parse部分的源码分析,也就是从template 到 astElemnt的解析到程。

正文

从笔者的 Vue编译思想详解一文中,我们已经知道编译个四个流程分别为parse、optimize、code generate、render。具体细节这里不做赘述,附上之前的一张图。

6d3c61da2d87c355c1ff0ac76ac48eef.png

本文则旨在从思想落实到源代码分析,当然只是针对parse这一部分的。

一、 源码结构。

笔者先列出我们在看源码之前,需要先预习的一些概念和准备。

准备

1.正则

parse的最终目标是生成具有众多属性的astElement树,而这些属性有很多则摘自标签的一些属性。

如 div上的v-for、v-if、v-bind等等,最终都会变成astElement的节点属性。

这里先给个例子:

{

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(`^

const startTagClose = /^\s*(\/?)>/

const endTag = new RegExp(`^]*>`)

const 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",通过改变attrName来改变传递的属性的key值。(非动态属性只能修改val值)。

我们先对attribute这个通用正则做一个详细的讲解:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

很长对不对??

但是细细的拆分的化,一共五个分组。

1.([^\s"'<>/=]+)

这个分组是匹配 非空格、"、'、、/、= 等符号的字符串。 主要会匹配到属性的key值部分。如下面的属性:

id="container"

复制代码

([^\s"'<>/=]+)会匹配到id。

2.\s*(=)\s* 这个是 匹配 = 号,当然了空格页一并匹配了。比如下面的属性:

id="container"

id = "container"

复制代码

都会匹配到 = 号,第二个会把空格一起匹配了。

3."([^"])"正则一 、'([^'])'正则二 、([^\s"'=<>`]+)正则三 . 这三个正则分别匹配三种情况 "val" 、'val' 、val。还是继续拿例子来讲。

id="container" // exp1

id='container' // exp2

id=container // exp3

复制代码

对于exp1,正则一会匹配到"container", exp2 ,正则2匹配到'container',exp3的话正则三会匹配到container。

Vue源码的正则基本将大多数情况都考虑在内了。

这样的话应该比较清晰了,我们来概括下:

attribute匹配的一共是三种情况, name="xxx" name='xxx' name=xxx。能够保证属性的所有情况都能包含进来。

需要注意的是正则处理后的数组的格式是:

['name','=','val','','']

或者

['name','=','','val','']

或者

['name','=','','','val']

复制代码

下面讲源码的时候,会知道这种数组格式是attr属性的原始状态,parse后期会将这种属性处理成attrMap的形式,大致如下:

{

name:'xxx',

id:'container'

}

复制代码

关于这个正则,我们附上一个讲解图:

38237c666de586565731b1211ca819d8.png

而关于dynamicArgAttribute, 则是大同小异:

主要是多了\[[^=]+\][^\s"'<>\/=]* 也就是 [name] 或者 [name]key 这类情况,附上正则详解图:

c39dd8ba9d0db93f1787f00290c408c7.png

2)标签处理正则

标签主要包含开始标签 (如

)和结束标签(如
),正则分别为以下两个:

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`

const qnameCapture = `((?:${ncname}\\:)?${ncname})`

const startTagOpen = new RegExp(`^

const startTagClose = /^\s*(\/?)>/

const endTag = new RegExp(`^]*>`)

复制代码

能够看到标签的匹配是以qnameCapture为基础的,那么这玩意又是啥呢?

其实qname就是类似于xml:xxx的这类带冒号的标签,所以startTagOpen是匹配

endTag匹配的是如

或的标签

3)处理vue的标签

export const onRE = /^@|^v-on:/ 处理绑定事件的正则

export const dirRE = process.env.VBIND_PROP_SHORTHAND

? /^v-|^@|^:|^\./ // v- | @click | :name | .stop 指令匹配

: /^v-|^@|^:/

复制代码

一眼就能看出来,对不对?直接进入复杂的for标签。

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',

...

}

}

复制代码

也就是说我们会在item in options的基础上进行正则匹配。

先看forAliasRE的分组,一共两个分组分别是([\s\S]*?)和([\s\S]*) 会分别匹配 item和 options。这里举的例子比较简单。

实际上 in或of之前的内容可能会比较复杂的,如(value,key) 或者(item,index)等,甚至可能(value,key,index),这个时候就是forIteratorRE开始起作用了。

它一共两个分组都是([^,\}\]]*),其实就是拿到alias的最后两个参数,大家都知道Vue对于Object的循环,是可以这么做的,例子如下:

复制代码

而forIteratorRE则是为了获取key 和index的。最终会放在astElement的iterator1 和 iterator2。

{

iterator1:',key',

iterator2:',index'

}

复制代码

好了关于正则就说这么多了,具体的情况还是得自己去看看源码的。

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标签对合格性

复制代码

模块二则是一些parse函数作用域内的全局标志和存储容器,代码如下:

const stack = [] // 配合使用的栈 主要目的是为了完成树状结构。

let root // 根节点记录,树顶

let currentParent // 当前父节点

let inVPre = false // 标记是否在v-pre节点 当中

let inPre = false // 是否在pre标签当中

let warned = false

复制代码

模块三是核心部分,也就是解析template的部分,这个函数一旦执行完, 模块2的root会变成一颗以astElement为节点的dom树。

,其代码大致为:

parseHTML(template,options)

复制代码

parseHTML函数和

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另一个是建立父子关系,其中细节会在下文中,详细介绍,这也是本文的重点。

chars函数处理的是文本节点,commend处理的则是注释节点。

切记这四个函数至关重要,下面会用代号讲解。

二、各模块重点功能。

Vue的html解析并非一步到位,先来介绍一些重点的函数功能

1.parseHTML函数内部功能函数详细讲解。

(1)解析开始标签和处理属性,生成初始化match。

前面我们说到了startTagOpen是用来匹配开始标签的。而parseHTML里面的parseStartTag函数则是利用该正则,匹配开始标签,创立一种初始的数据结构match,保存相应的属性,对于开始标签里的所有属性,如id、class、v-bind,都会保存到match.attr中。

代码如下:

/**

* 创建match数据结构

* 初始化的状态

* 只有

* tagName

* attrs

* attrs自己是个数组 也就是 正则达到的效果。。

* start

* end

*/

function parseStartTag () {

const start = html.match(startTagOpen) // 匹配开始标签。c

if (start) {

const match = { // 创建相应的数据结构

tagName: start[1],

attrs: [],

start: index

}

advance(start[0].length)

let end, attr

//遍历的摘取取属性值,并保存到attrs

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

}

}

}

复制代码

上面的while中,我们是用开始标签的结束符作为结束条件的。

startTagClose的正则是

const startTagClose = /^\s*(\/?)>/

复制代码

它本身除了判断是否已经结束,还有一个\/?是用来判断是否为一元标签的。

一元标签就是如可以只写一个标签的元素。这个标记后面会用到。

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 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的标签时,根据pos的位置,将pos之前的所有标签和匹配到的标签都会一起遍历的去执行end函数。

p标签和br标签

可能会遇到

和 标签 这个时候 p标签会走跟浏览器自动补全效果,先start再end。

而br则是一元标签,直接进入end效果。

2.start、end、comment、chars四大函数。

1)start函数

start函数非常长。这里截取重点部分

start() {

...

let element: ASTElement = createASTElement(tag, attrs, currentParent) // 1.创建astElement

...

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。

我们一个个来探讨一下,给大家留个印象,实际上,后面会有案例详细讲解函数们的作用。

1.首先最为简单的是processKey和processRef,在这两个函数处理之前,我们的key属性和ref属性都是保存在astElement上面的attrs和attrsMap,经过这两个函数之后,attrs里面的key和ref会被干掉,变成astElement的直属属性。

2.探讨一下slot的处理方式,我们知道的是,slot的具体位置是在组件中定义的,而需要替换的内容又是组件外面嵌套的代码,Vue对这两块的处理是分开的。

先说组件内的属性摘取,主要是slot标签的name属性,这是processSlotOutLet完成的。

// handle outlets

function processSlotOutlet (el) {

if (el.tag === 'slot') {

el.slotName = getBindingAttr(el, 'name') // 就是这一句了。

if (process.env.NODE_ENV !== 'production' && el.key) {

warn(

`\`key\` does not work on because slots are abstract outlets ` +

`and can possibly expand into multiple elements. ` +

`Use the key on a wrapping element instead.`,

getRawBindingAttr(el, 'key')

)

}

}

}

复制代码其次是摘取需要替换的内容,也就是

processSlotContent,这是是处理展示在组件内部的slot,但是在这个地方只是简单的将给el添加两个属性作用域插槽的slotScope和 slotTarget,也就是目标slot。

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。有兴趣的童鞋可以去看看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主要处理两中文本情况,静态文本和表达式,举个例子:

name

复制代码

name就是静态文本,创建的type为3.

{{name}}

复制代码

而在这个里面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的标志。

3.核心代码parseHTML内部探索

上面完成了一些重要函数的讲解,下面开始识别器的探索。

我们的主要目的是了解parse的主要目的和过程。不会在一些细枝末节作太多赘述。

1)概览

parseHTML函数的结构如下:

function parseHTML (html, options) {

const stack = []

const expectHTML = options.expectHTML

const isUnaryTag = options.isUnaryTag || no

const canBeLeftOpenTag = options.canBeLeftOpenTag || no

let index = 0 // 坐标标志

let last, lastTag // 上一个标签

while(html) {

last = html;

...

}

function advance (n) {

index += n

html = html.substring(n)

}

}

复制代码

parseHTML原理是用各个正则,不断的识别并前进的的过程。举个列子:

text

复制代码

startTagOpen会先匹配到

。这样就结束了。

当然了,匹配到到结果都是通过各个功能函数去处理。

2)标记

先介绍下各个参数的作用,在详细了解while里面的逻辑。

这里的核心参数一共有stack、index、last、lastTag。

他们贯穿了整个匹配线路,index相信大家已经明白是起什么作用的了。我们这里分析下其他属性的作用域。

1)现看下stack的功能吧:

先看一个示例

复制代码

这种误写的情况,如果按顺序识别的话,那么span标签永远不会得到end函数的处理,因为没有识别到闭合标签。所以stack有着检查错误的功能。

stack的处理方式是,识别到开始标签就会推入stack。识别到闭合标签就会把对应的闭合标签推出来。

像上面那种情况,当识别到到时候,我们会发现,stack里面上面到span,下面才是div,我们会把这两个一起处理掉。这样能保证生成的astElement树的结构包括span。

2)last的作用

请大家思考一个问题,什么时候我们才会结束?

其实就是parseHTML函数不起作用了,换句话说就是while绕了一圈发现,index没有变,html也没有变。

剩下的部分,我们会当作文本处理掉。

而这块的逻辑就是:

while(html){

last = html;

....

....

if(last===html){

optios.chars(html);

}

}

复制代码

有没有恍然大悟的感觉?

原来最后一步都是判断中间的处理部分有没有动html。last就是记录处理前的样式,然后在后面对比。没有变动了就只剩下文本了。我们直接当文本处理了。

lastTag。

这个标记使用的地方特别多,记录的是上个标签。因为有些特殊的情况,需要判断上个标签。

如p标签,记录了上个标签是lastTag,如果里面出现了div等标签,我们会从:

复制代码

变成:

复制代码

原因请参考这里。

3)while循环解析器之剖析

while的轮廓:

while(html) {

last = html;

if (!lastTag || !isPlainTextElement(lastTag)) {

let textEnd = html.indexOf('

if (textEnd === 0) {

... 模块一

}

let text, rest, next

if (textEnd >= 0) {

... 模块二

}

if (textEnd < 0) {

text = html

}

if (text) {

advance(text.length)

}

if (options.chars && text) { // 空格等 经过这个函数处理为文本节点

options.chars(text, index - text.length, index) // 模块三

}

} else {

// 模块四

}

}

复制代码

笔者将上面的代码大致分为四个模块,我们逐一来分析讲解。

模块一的代码:

if (comment.test(html)) {

const commentEnd = html.indexOf('-->')

if (commentEnd >= 0) {

if (options.shouldKeepComment) {

options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)

}

advance(commentEnd + 3)

continue

}

}

// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment

if (conditionalComment.test(html)) {

const conditionalEnd = html.indexOf(']>')

if (conditionalEnd >= 0) {

advance(conditionalEnd + 2)

continue

}

}

// Doctype:

const doctypeMatch = html.match(doctype)

if (doctypeMatch) {

advance(doctypeMatch[0].length)

continue

}

// End tag:

const endTagMatch = html.match(endTag)

if (endTagMatch) {

const curIndex = index

advance(endTagMatch[0].length)

parseEndTag(endTagMatch[1], curIndex, index)

continue

}

// Start tag:

const startTagMatch = parseStartTag()

if (startTagMatch) {

handleStartTag(startTagMatch)

if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {

advance(1)

}

continue

}

复制代码

模块一是在let textEnd = html.indexOf('

模块一的主要功能是匹配comment、conditionalComment、doctypeMatch、endTagMatch、startTagMatch五种情况。他们的共同特性是匹配并且处理完后,会调用advance函数进行前进。

不同的是comment、endTagMatch、startTagMatch会分别进入options.comment、options.end和options.start函数。

comment函数比较简单,这里不做赘述来,让我们具体看endTagMatch和startTagMatch。

1) 先看startTag:

const startTagMatch = parseStartTag()

if (startTagMatch) {

handleStartTag(startTagMatch)

if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {

advance(1)

}

continue

}

复制代码

parseStartTag函数之前我们有说过,除了匹配还会通过attribute正则摘取所有的属性,并生成一个match对象。

格式如下:

match = {

tagName:'xxx',

attrs:[

['id','=','container','',''],

['v-if','=','show','','']

],

start:xx,

end: xx

}

复制代码

然后把结果交给handleStartTag进行处理。

handleStartTag的功能前面也有说明,主要是将原始的正则匹配到到内容,格式一下:

attrs:[

['id','=','container','',''],

['v-if','=','show','','']

],

复制代码

会变成:

attrs:[

{name:'id',value:'container'},

{name:'v-if',value:'show'}

]

复制代码

并把类match结构推入到stack当中,最后执行了options.start函数。

2)再看endTag

const endTagMatch = html.match(endTag)

if (endTagMatch) {

const curIndex = index

advance(endTagMatch[0].length) // 前进

parseEndTag(endTagMatch[1], curIndex, index) // 进入

continue

}

复制代码

可以看到匹配到endTag,主要是进入了parseEndTag函数。

前面已经说过,parseEndTag函数主要是判断结束标签,再stack到位置,并把stack尾部到这个位置之间到所有到标签都通过options.end函数处理掉。options.end则使用closeElement去处理各个astElement到父子关系。

模块二

let text, rest, next

if (textEnd >= 0) { // 有0的情况,是因为模块一都没有匹配上。

rest = html.slice(textEnd)

while (

!endTag.test(rest) &&

!startTagOpen.test(rest) &&

!comment.test(rest) &&

!conditionalComment.test(rest)

) {

// < in plain text, be forgiving and treat it as text

next = rest.indexOf('

if (next < 0) break // 没有就不玩了

textEnd += next

rest = html.slice(textEnd)

}

text = html.substring(0, textEnd)

}

复制代码

模块二主要是检查下

模块三

if (options.chars && text) { // 空格等 经过这个函数处理为文本节点

options.chars(text, index - text.length, index) // 模块三

}

复制代码

模块三为类文本信息,我们会通过options.chars函数去处理,这个函数则会进一步,判断是否存在表达式文本,就是我们经常绑定到值如:

{{name}}

复制代码

模块四

这个模块处理到是script或style标签,这里暂且不做赘述了,请大家自行去研究。

三、具体示例探索。

说了太多概念,不免会有些抽象,那么直接给出一个具体的示例吧。

show attr bind

{{item.id}}

{{item.text}}

复制代码

刚进来到达while流程的是html就是完整的代码:

html = "

show attr bind

{{item.id}}

{{item.text}}
"

复制代码

先通过parseStartTag解析

,得到的结果为:

match = {

attrs:[

{

0:class="container",

1: "class",

2: "=",

3: "container",

4: undefined,

5: undefined,

end: 22,

groups: undefined,

index: 0,

input: " class="container" id="root">↵↵

↵ show attr bind↵
↵↵
↵ {{item.id}} ↵
{{item.text}}
↵ ↵
",

start: 4

},

{

0: " id="root"",

1: "id"

2: "="

3: "root"

4: undefined

5: undefined

end: 32

groups: undefined

index: 0

input: " id="root">↵↵

↵ show attr bind↵
↵↵
↵ {{item.id}} ↵
{{item.text}}
↵ ↵
"

start: 22

},

],

end: 33

start: 0

tagName: "div"

unarySlash: ""

}

复制代码

我们能看到解析到每个属性,也就是attrs的对象的时候,都会用input去记录还剩下的html。

然后将这个结果交给handleStartTag,去处理。

handleStartTag会将上面的attrs重新加工下,从数组变成:

[

{ //之前是数组的形式

"name":"class",

"value":"container",

"start":5,

"end":22

},

{"name":"id","value":"root","start":23,"end":32}

]

复制代码

将相应的参数传递给options.start去处理。这个函数的入参大致如下:

options.start(

tagName, // div

attrs, // 上面处理过的attrs

unary, // 一元标签

match.start, // 开始

match.end // 结束

)

复制代码

那么start函数本身呢,就去创建astElement,并处理掉v-for、v-if、v-once几种标签,这几种标签的处理方式,大致相同,从attrs去掉对应的属性,然后直接给astElement本身创建新的属性,下面给出处理后的格式如下:

1.v-if

{

if:'show',

ifConditions:[

{

exp:'show',

block: astElement

},

{

exp: 'show2',

block: astElement

},

{

exp: undefined,

block: astElement

}

],

}

复制代码

猜猜上述对ifConditions的第三个exp的undefined会是什么情况?

其实就是v-else的处理方式。

神秘面纱可以揭开了,关于v-if 、v-else-if 不会同时作为父节点的chidren而存在,而是只有一个children,那就是v-if,然后其他的会存放在ifConditions里面。

那么它们在源码的具体流程是怎么样子的?

// 1.遇到v-if节点,则在start函数中,使用processIf函数,添加ifConditions.

function processIf (el) {

const exp = getAndRemoveAttr(el, 'v-if') // 添加v-if属性

if (exp) { // 是if ,

el.if = exp

addIfCondition(el, { // 让我们直接为astElement添加一个ifConditions属性

exp: exp,

block: el

})

} else { // 不是v-if 只是 给节点加上 el.else 或 el.elseif

if (getAndRemoveAttr(el, 'v-else') != null) {

el.else = true

}

const elseif = getAndRemoveAttr(el, 'v-else-if')

if (elseif) {

el.elseif = elseif

}

}

}

// 2. 那么什么时候加上其他条件的节点,别急别急,还记得前面的流程吗,end函数里面我们会执行closeElement。

// 而这个函数有一个processIfConditions,如果不记得了,请翻上去看一看。

function processIfConditions (el, parent) {

const prev = findPrevElement(parent.children) // 找到上一个节点,其实就是 倒数最后一个

if (prev && prev.if) { // 如果上一个节点是if 那么ok,我们就是要把当前节点推到这个节点里面。

addIfCondition(prev, {

exp: el.elseif,

block: el

})

} else if (process.env.NODE_ENV !== 'production') { // 天呐,你写错了,v-else或v-else-if之前没有v-if,直接给错误。

warn(

`v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +

`used on element without corresponding v-if.`,

el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']

)

}

}

复制代码2.v-for

那么v-for我们最终会处理成什么样子呢?以及又是这么处理成这种样子的。

如果我们的案例是这样的:

v-for="(item,index) in list"

复制代码

我们得到的结果会是:

{

for:'list',

alias:'item',

iterator1:'index'

}

复制代码

这里没有牵扯到closeElement了,直接在processFor一步到味,我们详细的看看吧。

// 1.processFor函数,主要是通过parseFor摘取属性,然后通过extend拷贝给el。所以重点还是parseFor函数。

export function processFor (el: ASTElement) {

let exp

if ((exp = getAndRemoveAttr(el, 'v-for'))) { // exp摘取的是v-for里面的内容,这里是(item,index) in list

const res = parseFor(exp) // 摘取属性

if (res) {

extend(el, res) // 拷贝给el

} else if (process.env.NODE_ENV !== 'production') {

warn(

`Invalid v-for expression: ${exp}`,

el.rawAttrsMap['v-for']

)

}

}

}

2.详细结果在注释里面了。

export function parseFor (exp: string): ?ForParseResult {

// 传入了 exp = (item,index) in list

const inMatch = exp.match(forAliasRE) // 获取了一个数组,这个正则我们前面说了,这里是

// ['(item,index) in list','(item,index)','list']

if (!inMatch) return

const res = {}

res.for = inMatch[2].trim() // list 不是吗?

const alias = inMatch[1].trim().replace(stripParensRE, '') // item,index 对吗

const iteratorMatch = alias.match(forIteratorRE) // 这个正则我们也说过了,也是数组

// [',index','index']

if (iteratorMatch) {

res.alias = alias.replace(forIteratorRE, '').trim()

res.iterator1 = iteratorMatch[1].trim() // index对吗

if (iteratorMatch[2]) {

res.iterator2 = iteratorMatch[2].trim()

}

} else {

res.alias = alias

}

return res

}

复制代码

好的,结果出来了。

接着我们对解析案例,我们已经处理了开始标签

,那么剩下对还有

show attr bind

{{item.id}}

{{item.text}}

复制代码

那么接下来呢? parseStartTag会匹配到什么呢?

吗?

不好意思,并不是。现实的template各个标签之间都有空格,所以在while循环中,对于

如果是,那么从位置0 到 下一个

很显然,从位置0到,下一个开始标签

之间是有很多空格的,我们会生成一个文本空节点。

然后中间的过程我们省略的说吧。

处理

处理文本节点show attr bind

处理结束标签

好了,这是我们处理的第一个结束标签

,我们详细的看看吧。

// 我们知道对于结束标签我们匹配到后,是直接交给parseEndTag函数处理的。这个函数容错能力我们不说了,前面已经

// 有了详细的讲解,我们需要明白它会调用options.end函数。end会交给closeElement。

// closeElement会建立父子关系并处理好多好多属性

1.processKey

2.processRef

3.processSlotContent

4.processSlotContent

5.processComponent

6.processIfConditions

....

复制代码

到了这里我们还剩下:

{{item.id}}

{{item.text}}

复制代码

然后继续省略的讲解:

处理

,可以参照上面笔者描述的v-for处理方式看。

处理空节点

处理开始标签

处理文本标签{{item.id}},需要注意的是,expression建立的astElement的type为2。

处理结束标签

处理空节点

处理

开始标签

处理文本标签{{item.text}},type也是2

处理

结束标签,结束处理方式相同。

处理空节点

处理

结束标签,结束处理方式相同。

处理空节点

处理

结束标签,结束处理方式相同。

处理空节点

四、整体流程总结。

普通标签处理流程描述

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.

注释流程描述

1、匹配注释符号。

2、 使用comment函数处理。

3、直接创建type为3的节点。

完结感言

时间仓促,希望多多支持。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值