Vue.js 模板解析器原理
本文来自《深入浅出Vue.js》模板编译原理篇的第九章,主要讲述了如何将模板解析成AST,这一章的内容是全书最复杂且烧脑的章节。本文排版较为紧凑和图片是未经加工的原稿,真实纸质书的排版和图片会更加精致。
通过第8章的学习,我们知道解析器在整个模板编译中的位置。我们只有将模板解析成AST后,才能基于AST做优化或者生成代码字符串,那么解析器是如何将模板解析成AST的呢?
本章中,我们将详细介绍解析器内部的运行原理。
9.1 解析器的作用
解析器要实现的功能是将模板解析成AST。
例如:
<div >
<p > {{name}}p >
div >
复制代码
上面的代码是一个比较简单的模板,它转换成AST后的样子如下:
{
tag : "div"
type: 1 ,
staticRoot : false ,
static : false ,
plain : true ,
parent : undefined ,
attrsList : [],
attrsMap : {},
children : [
{
tag : "p"
type: 1 ,
staticRoot : false ,
static : false ,
plain : true ,
parent : {tag : "div" , ...},
attrsList : [],
attrsMap : {},
children : [{
type : 2 ,
text : "{{name}}" ,
static : false ,
expression : "_s(name)"
}]
}
]
}
复制代码
其实AST并不是什么很神奇的东西,不要被它的名字吓倒。它只是用JS中的对象来描述一个节点,一个对象代表一个节点,对象中的属性用来保存节点所需的各种数据。比如,parent
属性保存了父节点的描述对象,children
属性是一个数组,里面保存了一些子节点的描述对象。再比如,type
属性代表一个节点的类型等。当很多个独立的节点通过parent
属性和children
属性连在一起时,就变成了一个树,而这样一个用对象描述的节点树其实就是AST。
9.2 解析器内部运行原理
事实上,解析器内部也分了好几个子解析器,比如HTML解析器、文本解析器以及过滤器解析器,其中最主要的是HTML解析器。顾名思义,HTML解析器的作用是解析HTML,它在解析HTML的过程中会不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。
伪代码如下:
parseHTML(template, {
start (tag, attrs, unary) {
},
end () {
},
chars (text) {
},
comment (text) {
}
})
复制代码
你可能不能很清晰地理解,下面我们举个简单的例子:
<div > <p > 我是Berwinp >div >
复制代码
当上面这个模板被HTML解析器解析时,所触发的钩子函数依次是:start
、start
、chars
、end
、end
。
也就是说,解析器其实是从前向后解析的。解析到时,会触发一个标签开始的钩子函数
start
;然后解析到
时,又触发一次钩子函数
start
;接着解析到
我是Berwin
这行文本,此时触发了文本钩子函数
chars
;然后解析到
,触发了标签结束的钩子函数
end
;接着继续解析到
,此时又触发一次标签结束的钩子函数end
,解析结束。
因此,我们可以在钩子函数中构建AST节点。在start
钩子函数中构建元素类型的节点,在chars
钩子函数中构建文本类型的节点,在comment
钩子函数中构建注释类型的节点。
当HTML解析器不再触发钩子函数时,就代表所有模板都解析完毕,所有类型的节点都在钩子函数中构建完成,即AST构建完成。
我们发现,钩子函数start
有三个参数,分别是tag
、attrs
和unary
,它们分别代表标签名、标签的属性以及是否是自闭合标签。
而文本节点的钩子函数chars
和注释节点的钩子函数comment
都只有一个参数,只有text
。这是因为构建元素节点时需要知道标签名、属性和自闭合标识,而构建注释节点和文本节点时只需要知道文本即可。
什么是自闭合标签?举个简单的例子,input
标签就属于自闭合标签:
,而div
标签就不属于自闭合标签:
。
在start
钩子函数中,我们可以使用这三个参数来构建一个元素类型的AST节点,例如:
function createASTElement (tag, attrs, parent ) {
return {
type : 1 ,
tag,
attrsList : attrs,
parent,
children : []
}
}
parseHTML(template, {
start (tag, attrs, unary) {
let element = createASTElement(tag, attrs, currentParent)
}
})
复制代码
在上面的代码中,我们在钩子函数start
中构建了一个元素类型的AST节点。
如果是触发了文本的钩子函数,就使用参数中的文本构建一个文本类型的AST节点,例如:
parseHTML(template, {
chars (text) {
let element = {type : 3 , text}
}
})
复制代码
如果是注释,就构建一个注释类型的AST节点,例如:
parseHTML(template, {
comment (text) {
let element = {type : 3 , text, isComment : true }
}
})
复制代码
你会发现,9.1节中看到的AST是有层级关系的,一个AST节点具有父节点和子节点,但是9.2节中介绍的创建节点的方式,节点是被拉平的,没有层级关系。因此,我们需要一套逻辑来实现层级关系,让每一个AST节点都能找到它的父级。下面我们介绍一下如何构建AST层级关系。
构建AST层级关系其实非常简单,我们只需要维护一个栈(stack)即可,用栈来记录层级关系,这个层级关系也可以理解为DOM的深度。
HTML解析器在解析HTML时,是从前向后解析。每当遇到开始标签
,就触发钩子函数start
。每当遇到结束标签
,就会触发钩子函数end
。
基于HTML解析器的逻辑,我们可以在每次触发钩子函数start
时,把当前构建的节点推入栈中;每当触发钩子函数end
时,就从栈中弹出一个节点。
这样就可以保证每当触发钩子函数start
时,栈的最后一个节点就是当前正在构建的节点的父节点,如图9-1所示。
图9-1 使用栈记录DOM层级关系(
英文为代码体 )
下面我们用一个具体的例子来描述如何从0到1构建一个带层级关系的AST。
假设有这样一个模板:
<div >
<h1 > 我是Berwinh1 >
<p > 我今年23岁p >
div >
复制代码
上面这个模板被解析成AST的过程如图9-2所示。
图9-2构建AST的过程(下面的(1)~(12)需要改成图中那样黑底白字的 )
图9-2给出了构建AST的过程,图中的黑底白数字代表解析的步骤,具体如下。
(1) 模板的开始位置是div
的开始标签,于是会触发钩子函数start
。start
触发后,会先构建一个div
节点。此时发现栈是空的,这说明div
节点是根节点,因为它没有父节点。最后,将div
节点推入栈中,并将模板字符串中的div
开始标签从模板中截取掉。
(2) 这时模板的开始位置是一些空格,这些空格会触发文本节点的钩子函数,在钩子函数里会忽略这些空格。同时会在模板中将这些空格截取掉。
(3) 这时模板的开始位置是h1
的开始标签,于是会触发钩子函数start
。与前面流程一样,start
触发后,会先构建一个h1
节点。此时发现栈的最后一个节点是div
节点,这说明h1
节点的父节点是div
,于是将h1
添加到div
的子节点中,并且将h1
节点推入栈中,同时从模板中将h1
的开始标签截取掉。
(4) 这时模板的开始位置是一段文本,于是会触发钩子函数chars
。chars
触发后,会先构建一个文本节点,此时发现栈中的最后一个节点是h1
,这说明文本节点的父节点是h1
,于是将文本节点添加到h1
节点的子节点中。由于文本节点没有子节点,所以文本节点不会被推入栈中。最后,将文本从模板中截取掉。
(5) 这时模板的开始位置是h1
结束标签,于是会触发钩子函数end
。end
触发后,会把栈中最后一个节点弹出来。
(6) 与第(2)步一样,这时模板的开始位置是一些空格,这些空格会触发文本节点的钩子函数,在钩子函数里会忽略这些空格。同时会在模板中将这些空格截取掉。
(7) 这时模板的开始位置是p
开始标签,于是会触发钩子函数start
。start
触发后,会先构建一个p
节点。由于第(5)步已经从栈中弹出了一个节点,所以此时栈中的最后一个节点是div
,这说明p
节点的父节点是div
。于是将p
推入div
的子节点中,最后将p
推入到栈中,并将p
的开始标签从模板中截取掉。
(8) 这时模板的开始位置又是一段文本,于是会触发钩子函数chars
。当chars
触发后,会先构建一个文本节点,此时发现栈中的最后一个节点是p
节点,这说明文本节点的父节点是p
节点。于是将文本节点推入p
节点的子节点中,并将文本从模板中截取掉。
(9) 这时模板的开始位置是p
的结束标签,于是会触发钩子函数end
。当end
触发后,会从栈中弹出一个节点出来,也就是把p
标签从栈中弹出来,并将p
的结束标签从模板中截取掉。
(10) 与第(2)步和第(6)步一样,这时模板的开始位置是一些空格,这些空格会触发文本节点的钩子函数并且在钩子函数里会忽略这些空格。同时会在模板中将这些空格截取掉。
(11) 这时模板的开始位置是div
的结束标签,于是会触发钩子函数end
。其逻辑与之前一样,把栈中的最后一个节点弹出来,也就是把div
弹了出来,并将div
的结束标签从模板中截取掉。
(12)这时模板已经被截取空了,也就代表着HTML解析器已经运行完毕。这时我们会发现栈已经空了,但是我们得到了一个完整的带层级关系的AST语法树。这个AST中清晰写明了每个节点的父节点、子节点及其节点类型。
9.3 HTML解析器
通过前面的介绍,我们发现构建AST非常依赖HTML解析器所执行的钩子函数以及钩子函数中所提供的参数,你一定会非常好奇HTML解析器是如何解析模板的,接下来我们会详细介绍HTML解析器的运行原理。
9.3.1 运行原理
事实上,解析HTML模板的过程就是循环的过程,简单来说就是用HTML模板字符串来循环,每轮循环都从HTML模板中截取一小段字符串,然后重复以上过程,直到HTML模板被截成一个空字符串时结束循环,解析完毕,如图9-2所示。
在截取一小段字符串时,有可能截取到开始标签,也有可能截取到结束标签,又或者是文本或者注释,我们可以根据截取的字符串的类型来触发不同的钩子函数。
循环HTML模板的伪代码如下:
function parseHTML (html, options ) {
while (html) {
}
}
复制代码
为了方便理解,我们手动模拟HTML解析器的解析过程。例如,下面这样一个简单的HTML模板:
<div >
<p > {{name}}p >
div >
复制代码
它在被HTML解析器解析的过程如下。
最初的HTML模板:
``
复制代码
第一轮循环时,截取出一段字符串,并且触发钩子函数
start
,截取后的结果为:
`
{{name}}
`
复制代码
第二轮循环时,截取出一段字符串:
`
`
复制代码
并且触发钩子函数chars
,截取后的结果为:
`{{name}}
`
复制代码
第三轮循环时,截取出一段字符串
,并且触发钩子函数start
,截取后的结果为:
`{{name}}
`
复制代码
第四轮循环时,截取出一段字符串{{name}}
,并且触发钩子函数chars
,截取后的结果为:
`
`
复制代码
第五轮循环时,截取出一段字符串
,并且触发钩子函数
end
,截取后的结果为:
`
`
复制代码
第六轮循环时,截取出一段字符串:
`
`
复制代码
并且触发钩子函数chars
,截取后的结果为:
`
`
复制代码
第七轮循环时,截取出一段字符串
,并且触发钩子函数
end
,截取后的结果为:
``
复制代码
解析完毕。
HTML解析器的全部逻辑都是在循环中执行,循环结束就代表解析结束。接下来,我们要讨论的重点是HTML解析器在循环中都干了些什么事。
你会发现HTML解析器可以很聪明地知道它在每一轮循环中应该截取哪些字符串,那么它是如何做到这一点的呢?
通过前面的例子,我们发现一个很有趣的事,那就是每一轮截取字符串时,都是在整个模板的开始位置截取。我们根据模板开始位置的片段类型,进行不同的截取操作。
例如,上面例子中的第一轮循环:如果是以开始标签开头的模板,就把开始标签截取掉。
再例如,上面例子中的第四轮循环:如果是以文本开始的模板,就把文本截取掉。
这些被截取的片段分很多种类型,示例如下。
开始标签,例如。
结束标签,例如
。
HTML注释,例如
。
DOCTYPE,例如
。
条件注释,例如我是注释
。
文本,例如我是Berwin
。
通常,最常见的是开始标签、结束标签、文本以及注释。
9.3.2 截取开始标签
上一节中我们说过,每一轮循环都是从模板的最前面截取,所以只有模板以开始标签开头,才需要进行开始标签的截取操作。
那么,如何确定模板是不是以开始标签开头?
在HTML解析器中,想分辨出模板是否以开始标签开头并不难,我们需要先判断HTML模板是不是以<
开头。
如果HTML模板的第一个字符不是<
,那么它一定不是以开始标签开头的模板,所以不需要进行开始标签的截取操作。
如果HTML模板以<
开头,那么说明它至少是一个以标签开头的模板,但这个标签到底是什么类型的标签,还需要进一步确认。
如果模板以<
开头,那么它有可能是以开始标签开头的模板,同时它也有可能是以结束标签开头的模板,还有可能是注释等其他标签,因为这些类型的片段都以<
开头。那么,要进一步确定模板是不是以开始标签开头,还需要借助正则表达式来分辨模板的开始位置是否符合开始标签的特征。
那么,如何使用正则表达式来匹配模板以开始标签开头?我们看下面的代码:
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname} \\:)?${ncname} )`
const startTagOpen = new RegExp (`^<${qnameCapture} ` )
'
' .match(startTagOpen)
'
我是Berwin
'.match(startTagOpen)
'我是Berwin' .match(startTagOpen)
复制代码
通过上面的例子可以看到,只有'
'
可以成功匹配,而以
开头的或者以文本开头的模板都无法成功匹配。
在9.2节中,我们介绍了当HTML解析器解析到标签开始时,会触发钩子函数start
,同时会给出三个参数,分别是标签名(tagName
)、属性(attrs
)以及自闭合标识(unary
)。
因此,在分辨出模板以开始标签开始之后,需要将标签名、属性以及自闭合标识解析出来。
在分辨模板是否以开始标签开始时,就可以得到标签名,而属性和自闭合标识则需要进一步解析。
当完成上面的解析后,我们可以得到这样一个数据结构:
const start = '
' .match(startTagOpen)
if (start) {
const match = {
tagName : start[1 ],
attrs : []
}
}
复制代码
这里有一个细节很重要:在前面的例子中,我们匹配到的开始标签并不全。例如:
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname} \\:)?${ncname} )`
const startTagOpen = new RegExp (`^<${qnameCapture} ` )
'
' .match(startTagOpen)
'
' .match(startTagOpen)
'
' .match(startTagOpen)
复制代码
可以看出,上面这个正则表达式虽然可以分辨出模板是否以开始标签开头,但是它的匹配规则并不是匹配整个开始标签,而是开始标签的一小部分。
事实上,开始标签被拆分成三个小部分,分别是标签名、属性和结尾,如图9-3所示。
图9-3 开始标签被拆分成三个小部分(
代码用代码体 )
通过“标签名”这一段字符,就可以分辨出模板是否以开始标签开头,此后要想得到属性和自闭合标识,则需要进一步解析。
1. 解析标签属性
在分辨模板是否以开始标签开头时,会将开始标签中的标签名这一小部分截取掉,因此在解析标签属性时,我们得到的模板是下面伪代码中的样子:
' class="box">div > '
复制代码
通常,标签属性是可选的,一个标签的属性有可能存在,也有可能不存在,所以需要判断标签是否存在属性,如果存在,对它进行截取。
下面的伪代码展示了如何解析开始标签中的属性,但是它只能解析一个属性:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
let html = ' class="box">
'
let attr = html.match(attribute)
html = html.substring(attr[
0 ].length)
console .log(attr)
']
复制代码
如果标签上有很多属性,那么上面的处理方式就不足以支撑解析任务的正常运行。例如下面的代码:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
let html = ' class="box" id="el">'
let attr = html.match(attribute)
html = html.substring(attr[0 ].length)
console .log(attr)
复制代码
可以看到,这里只解析出了class
属性,而id
属性没有解析出来。
此时剩余的HTML模板是这样的:
' id="el">div > '
复制代码
所以属性也可以分成多个小部分,一小部分一小部分去解析与截取。
解决这个问题时,我们只需要每解析一个属性就截取一个属性。如果截取完后,剩下的HTML模板依然符合标签属性的正则表达式,那么说明还有剩余的属性需要处理,此时就重复执行前面的流程,直到剩余的模板不存在属性,也就是剩余的模板不存在符合正则表达式所预设的规则。
例如:
const startTagClose = /^\s*(\/?)>/
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
let html = ' class="box" id="el">'
let end, attr
const match = {tagName : 'div' , attrs : []}
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
html = html.substring(attr[0 ].length)
match.attrs.push(attr)
}
复制代码
上面这段代码的意思是,如果剩余HTML模板不符合开始标签结尾部分的特征,并且符合标签属性的特征,那么进入到循环中进行解析与截取操作。
通过match
方法解析出的结果为:
{
tagName : 'div' ,
attrs : [
[' class="box"' , 'class' , '=' , 'box' , null , null ],
[' id="el"' , 'id' ,'=' , 'el' , null , null ]
]
}
复制代码
可以看到,标签中的两个属性都已经解析好并且保存在了attrs
中。
此时剩余模板是下面的样子:
">"
复制代码
我们将属性解析后的模板与解析之前的模板进行对比:
' class="box" id="el">'
'>'
{
tagName : 'div' ,
attrs : []
}
{
tagName : 'div' ,
attrs : [
[' class="box"' , 'class' , '=' , 'box' , null , null ],
[' id="el"' , 'id' ,'=' , 'el' , null , null ]
]
}
复制代码
可以看到,标签上的所有属性都已经被成功解析出来,并保存在attrs
属性中。
2. 解析自闭合标识
如果我们接着上面的例子继续解析的话,目前剩余的模板是下面这样的:
'>'
复制代码
开始标签中结尾部分解析的主要目的是解析出当前这个标签是否是自闭合标签。
举个例子:
<div > div >
复制代码
这样的div
标签就不是自闭合标签,而下面这样的input
标签就属于自闭合标签:
<input type ="text" />
复制代码
自闭合标签是没有子节点的,所以前文中我们提到构建AST层级时,需要维护一个栈,而一个节点是否需要推入到栈中,可以使用这个自闭合标识来判断。
那么,如何解析开始标签中的结尾部分呢?看下面这段代码:
function parseStartTagEnd (html ) {
const startTagClose = /^\s*(\/?)>/
const end = html.match(startTagClose)
const match = {}
if (end) {
match.unarySlash = end[1 ]
html = html.substring(end[0 ].length)
return match
}
}
console .log(parseStartTagEnd('>' ))
console .log(parseStartTagEnd('/>
' ))
复制代码
这段代码可以正确解析出开始标签是否是自闭合标签。
从代码中打印出来的结果可以看到,自闭合标签解析后的unarySlash
属性为/
,而非自闭合标签为空字符串。
3. 实现源码
前面解析开始标签时,我们将其拆解成了三个部分,分别是标签名、属性和结尾。我相信你已经对开始标签的解析有了一个清晰的认识,接下来看一下Vue.js中真实的代码是什么样的:
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname} \\:)?${ncname} )`
const startTagOpen = new RegExp (`^<${qnameCapture} ` )
const startTagClose = /^\s*(\/?)>/
function advance (n ) {
html = html.substring(n)
}
function parseStartTag ( ) {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName : start[1 ],
attrs : []
}
advance(start[0 ].length)
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0 ].length)
match.attrs.push(attr)
}
if (end) {
match.unarySlash = end[1 ]
advance(end[0 ].length)
return match
}
}
}
复制代码
上面的代码是Vue.js中解析开始标签的源码,这段代码中的html
变量是HTML模板。
调用parseStartTag
就可以将剩余模板开始部分的开始标签解析出来。如果剩余HTML模板的开始部分不符合开始标签的正则表达式规则,那么调用parseStartTag
就会返回undefined
。因此,判断剩余模板是否符合开始标签的规则,只需要调用parseStartTag
即可。如果调用它后得到了解析结果,那么说明剩余模板的开始部分符合开始标签的规则,此时将解析出来的结果取出来并调用钩子函数start
即可:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
continue
}
复制代码
前面我们说过,所有解析操作都运行在循环中,所以continue
的意思是这一轮的解析工作已经完成,可以进行下一轮解析工作。
从代码中可以看出,如果调用parseStartTag
之后有返回值,那么会进行开始标签的处理,其处理逻辑主要在handleStartTag
中。这个函数的主要目的就是将tagName
、attrs
和unary
等数据取出来,然后调用钩子函数将这些数据放到参数中。
9.3.3 截取结束标签
结束标签的截取要比开始标签简单得多,因为它不需要解析什么,只需要分辨出当前是否已经截取到结束标签,如果是,那么触发钩子函数就可以了。
那么,如何分辨模板已经截取到结束标签了呢?其道理其实和开始标签的截取相同。
如果HTML模板的第一个字符不是<
,那么一定不是结束标签。只有HTML模板的第一个字符是<
时,我们才需要进一步确认它到底是不是结束标签。
进一步确认时,我们只需要判断剩余HTML模板的开始位置是否符合正则表达式中定义的规则即可:
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname} \\:)?${ncname} )`
const endTag = new RegExp (`^<\\/${qnameCapture} [^>]*>` )
const endTagMatch = '' .match(endTag)
const endTagMatch2 = ''.match(endTag)
console .log(endTagMatch)
", "div", index: 0, input: ""]
console .log(endTagMatch2)
复制代码
上面代码可以分辨出剩余模板是否是结束标签。当分辨出结束标签后,需要做两件事,一件事是截取模板,另一件事是触发钩子函数。而Vue.js中相关源码被精简后如下:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
html = html.substring(endTagMatch[0 ].length)
options.end(endTagMatch[1 ])
continue
}
复制代码
可以看出,先对模板进行截取,然后触发钩子函数。
9.3.4 截取注释
分辨模板是否已经截取到注释的原理与开始标签和结束标签相同,先判断剩余HTML模板的第一个字符是不是<
,如果是,再用正则表达式来进一步匹配:
const comment = /^' )
if (commentEnd >= 0 ) {
if (options.shouldKeepComment) {
options.comment(html.substring(4 , commentEnd))
}
html = html.substring(commentEnd + 3 )
continue
}
}
复制代码
在上面的代码中,我们使用正则表达式来判断剩余的模板是否符合注释的规则,如果符合,就将这段注释文本截取出来。
这里有一个有意思的地方,那就是注释的钩子函数可以通过选项来配置,只有options.shouldKeepComment
为真时,才会触发钩子函数,否则只截取模板,不触发钩子函数。
9.3.5 截取条件注释
条件注释不需要触发钩子函数,我们只需要把它截取掉就行了。
截取条件注释的原理与截取注释非常相似,如果模板的第一个字符是<
,并且符合我们事先用正则表达式定义好的规则,就说明需要进行条件注释的截取操作。
在下面的代码中,我们通过indexOf
找到条件注释结束位置的下标,然后将结束位置前的字符都截取掉:
const conditionalComment = /^
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>' )
if (conditionalEnd >= 0 ) {
html = html.substring(conditionalEnd + 2 )
continue
}
}
复制代码
我们来举个例子:
const conditionalComment = /^
let html = ' '
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>' )
if (conditionalEnd >= 0 ) {
html = html.substring(conditionalEnd + 2 )
}
}
console .log(html)
复制代码
从打印结果中可以看到,HTML中的条件注释部分截取掉了。
通过这个逻辑可以发现,在Vue.js中条件注释其实没有用,写了也会被截取掉,通俗一点说就是写了也白写。
9.3.6 截取DOCTYPE
DOCTYPE
与条件注释相同,都是不需要触发钩子函数的,只需要将匹配到的这一段字符截取掉即可。下面的代码将DOCTYPE
这段字符匹配出来后,根据它的length
属性来决定要截取多长的字符串:
const doctype = /^]+>/i
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
html = html.substring(doctypeMatch[0 ].length)
continue
}
复制代码
示例如下:
const doctype = /^]+>/i
let html = ''
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
html = html.substring(doctypeMatch[0 ].length)
}
console .log(html)
复制代码
从打印结果可以看到,HTML中的DOCTYPE
被成功截取掉了。
9.3.7 截取文本
若想分辨在本轮循环中HTML模板是否已经截取到文本,其实很简单,我们甚至不需要使用正则表达式。
在前面的其他标签类型中,我们都会判断剩余HTML模板的第一个字符是否是<
,如果是,再进一步确认到底是哪种类型。这是因为以<
开头的标签类型太多了,如开始标签、结束标签和注释等。然而文本只有一种,如果HTML模板的第一个字符不是<
,那么它一定是文本了。
例如:
我是文本div >
复制代码
上面这段HTML模板并不是以<
开头的,所以可以断定它是以文本开头的。
那么,如何从模板中将文本解析出来呢?我们只需要找到下一个<
在什么位置,这之前的所有字符都属于文本,如图9-4所示。
图9-4 尖括号前面的字符都属于文本
在代码中可以这样实现:
while (html) {
let text
let textEnd = html.indexOf('<' )
if (textEnd >= 0 ) {
text = html.substring(0 , textEnd)
html = html.substring(textEnd)
}
if (textEnd < 0 ) {
text = html
html = ''
}
if (options.chars && text) {
options.chars(text)
}
}
复制代码
上面的代码共有三部分逻辑。
第一部分是截取文本,这在前面介绍过了。<
之前的所有字符都是文本,直接使用html.substring
从模板的最开始位置截取到<
之前的位置,就可以将文本截取出来。
第二部分是一个条件:如果在整个模板中都找不到<
,那么说明整个模板全是文本。
第三部分是触发钩子函数并将截取出来的文本放到参数中。
关于文本,还有一个特殊情况需要处理:如果<
是文本的一部分,该如何处理?
举个例子:
1<2
复制代码
在上面这样的模板中,如果只截取第一个<
前面的字符,最后被截取出来的将只有1,而不能把所有文本都截取出来。
那么,该如何解决这个问题呢?
有一个思路是,如果将<
前面的字符截取完之后,剩余的模板不符合任何需要被解析的片段的类型,就说明这个<
是文本的一部分。
什么是需要被解析的片段的类型?在9.3.1节中,我们说过HTML解析器是一段一段截取模板的,而被截取的每一段都符合某种类型,这些类型包括开始标签、结束标签和注释等。
说的再具体一点,那就是上面这段代码中的1被截取完之后,剩余模板是下面的样子:
<2
复制代码
<2
符合开始标签的特征么?不符合。
<2
符合结束标签的特征么?不符合。
<2
符合注释的特征么?不符合。
当剩余的模板什么都不符合时,就说明<
属于文本的一部分。
当判断出<
是属于文本的一部分后,我们需要做的事情是找到下一个<
并将其前面的文本截取出来加到前面截取了一半的文本后面。
这里还用上面的例子,第二个<
之前的字符是<2
,那么把<2
截取出来后,追加到上一次截取出来的1
的后面,此时的结果是:
1<2
复制代码
截取后剩余的模板是:
div >
复制代码
如果剩余的模板依然不符合任何被解析的类型,那么重复此过程。直到所有文本都解析完。
说完了思路,我们看一下具体的实现,伪代码如下:
while (html) {
let text, rest, next
let textEnd = html.indexOf('<' )
if (textEnd >= 0 ) {
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
next = rest.indexOf('<' , 1 )
if (next < 0 ) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0 , textEnd)
html = html.substring(textEnd)
}
if (textEnd < 0 ) {
text = html
html = ''
}
if (options.chars && text) {
options.chars(text)
}
}
复制代码
在代码中,我们通过while
来解决这个问题(注意是里面的while
)。如果剩余的模板不符合任何被解析的类型,那么重复解析文本,直到剩余模板符合被解析的类型为止。
在上面的代码中,endTag
、startTagOpen
、comment
和conditionalComment
都是正则表达式,分别匹配结束标签、开始标签、注释和条件注释。
在Vue.js源码中,截取文本的逻辑和其他的实现思路一致。
9.3.8 纯文本内容元素的处理
什么是纯文本内容元素呢?script
、style
和textarea
这三种元素叫作纯文本内容元素。解析它们的时候,会把这三种标签内包含的所有内容都当作文本处理。那么,具体该如何处理呢?
前面介绍开始标签、结束标签、文本、注释的截取时,其实都是默认当前需要截取的元素的父级元素不是纯文本内容元素 。事实上,如果要截取元素的父级元素是纯文本内容元素的话,处理逻辑将完全不一样。
事实上,在while
循环中,最外层的判断条件就是父级元素是不是纯文本内容元素。例如下面的伪代码:
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
} else {
}
}
复制代码
在上面的代码中,lastTag
代表父元素。可以看到,在while
中,首先进行判断,如果父元素不存在或者不是纯文本内容元素,那么进行正常的处理逻辑,也就是前面介绍的逻辑。
而当父元素是script
这种纯文本内容元素时,会进入到else
这个语句里面。由于纯文本内容元素都被视作文本处理,所以我们的处理逻辑就变得很简单,只需要把这些文本截取出来并触发钩子函数chars
,然后再将结束标签截取出来并触发钩子函数end
。
也就是说,如果父标签是纯文本内容元素,那么本轮循环会一次性将这个父标签给处理完毕。
伪代码如下:
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
} else {
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp ('([\\s\\S]*?)(' + stackedTag + '[^>]*>)' , 'i' ))
const rest = html.replace(reStackedTag, function (all, text ) {
if (options.chars) {
options.chars(text)
}
return ''
})
html = rest
options.end(stackedTag)
}
}
复制代码
上面代码中的正则表达式可以匹配结束标签前包括结束标签自身在内的所有文本。
我们可以给replace
方法的第二个参数传递一个函数。在这个函数中,我们得到了参数text
(代表结束标签前的所有内容),触发了钩子函数chars
并把text
放到钩子函数的参数中传出去。最后,返回了一个空字符串,代表将匹配到的内容都截掉了。注意,这里的截掉会将内容和结束标签一起截取掉。
最后,调用钩子函数end
并将标签名放到参数中传出去,代表本轮循环中的所有逻辑都已处理完毕。
假如我们现在有这样一个模板:
<div id ="el" >
<script > console .log(1 )script >
div >
复制代码
当解析到script
中的内容时,模板是下面的样子:
console.log(1)script >
div >
复制代码
此时父元素为script
,所以会进入到else
中的逻辑进行处理。在其处理过程中,会触发钩子函数chars
和end
。
钩子函数chars
的参数为script
中的所有内容,本例中大概是下面的样子:
chars('console.log(1)' )
复制代码
钩子函数end
的参数为标签名,本例中是script
。
处理后的剩余模板如下:
div >
复制代码
9.3.9 使用栈维护DOM层级
通过前面几节的介绍,特别是9.3.8节中的介绍,你一定会感到很奇怪,如何知道父元素是谁?
在前面几节中,我们并没有介绍HTML解析器内部其实也有一个栈来维护DOM层级关系,其逻辑与9.2.1节相同:就是每解析到开始标签,就向栈中推进去一个;每解析到标签结束,就弹出来一个。因此,想取到父元素并不难,只需要拿到栈中的最后一项即可。
同时,HTML解析器中的栈还有另一个作用,它可以检测出HTML标签是否正确闭合。例如:
<div > <p > div >
复制代码
在上面的代码中,p
标签忘记写结束标签,那么当HTML解析器解析到div
的结束标签时,栈顶的元素却是p
标签。这个时候从栈顶向栈底循环找到div
标签,在找到div
标签之前遇到的所有其他标签都是忘记了闭合的标签,而Vue.js会在非生产环境下在控制台打印警告提示。
关于使用栈来维护DOM层级关系的具体实现思路,9.2.1节已经详细介绍过,这里不再重复介绍。
9.3.10 整体逻辑
前面我们把开始标签、结束标签、注释、文本、纯文本内容元素等的截取方式拆分开,单独进行了详细介绍。本节中,我们就来介绍如何将这些解析方式组装起来完成HTML解析器的功能。
首先,HTML解析器是一个函数。就像9.2节介绍的那样,HTML解析器最终的目的是实现这样的功能:
parseHTML(template, {
start (tag, attrs, unary) {
},
end () {
},
chars (text) {
},
comment (text) {
}
})
复制代码
所以HTML解析器在实现上肯定是一个函数,它有两个参数——模板和选项:
export function parseHTML (html, options ) {
}
复制代码
我们的模板是一小段一小段去截取与解析的,所以需要一个循环来不断截取,直到全部截取完毕:
export function parseHTML (html, options ) {
while (html) {
}
}
复制代码
在循环中,首先要判断父元素是不是纯文本内容元素,因为不同类型父节点的解析方式将完全不同:
export function parseHTML (html, options ) {
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
} else {
}
}
}
复制代码
在上面的代码中,我们发现这里已经把整体逻辑分成了两部分,一部分是父标签是正常标签的逻辑,另一部分是父标签是script
、style
、textarea
这种纯文本内容元素的逻辑。
如果父标签为正常的元素,那么有几种情况需要分别处理,比如需要分辨出当前要解析的一小段模板到底是什么类型。是开始标签?还是结束标签?又或者是文本?
我们把所有需要处理的情况都列出来,有下面几种情况:
文本
注释
条件注释
DOCTYPE
结束标签
开始标签
我们会发现,在这些需要处理的类型中,除了文本之外,其他都是以标签形式存在的,而标签是以<
开头的。
所以逻辑就很清晰了,我们先根据<
来判断需要解析的字符是文本还是其他的:
export function parseHTML (html, options ) {
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<' )
if (textEnd === 0 ) {
}
let text, rest, next
if (textEnd >= 0 ) {
}
if (textEnd < 0 ) {
text = html
html = ''
}
if (options.chars && text) {
options.chars(text)
}
} else {
}
}
}
复制代码
在上面的代码中,我们可以通过<
来分辨是否需要进行文本解析。关于文本解析的内容,详见9.3.7节。
如果通过<
分辨出即将解析的这一小部分字符不是文本而是标签类,那么标签类有那么多类型,我们需要进一步分辨具体是哪种类型:
export function parseHTML (html, options ) {
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<' )
if (textEnd === 0 ) {
if (comment.test(html)) {
continue
}
if (conditionalComment.test(html)) {
continue
}
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
continue
}
const endTagMatch = html.match(endTag)
if (endTagMatch) {
continue
}
const startTagMatch = parseStartTag()
if (startTagMatch) {
continue
}
}
let text, rest, next
if (textEnd >= 0 ) {
}
if (textEnd < 0 ) {
text = html
html = ''
}
if (options.chars && text) {
options.chars(text)
}
} else {
}
}
}
复制代码
关于不同类型的具体处理方式,前面已经详细介绍过,这里不再重复。
9.4 文本解析器
文本解析器的作用是解析文本。你可能会觉得很奇怪,文本不是在HTML解析器中被解析出来了么?准确地说,文本解析器是对HTML解析器解析出来的文本进行二次加工。为什么要进行二次加工?
文本其实分两种类型,一种是纯文本,另一种是带变量的文本。例如下面这样的文本是纯文本:
Hello Berwin
复制代码
而下面这样的是带变量的文本:
Hello {{name}}
复制代码
在Vue.js模板中,我们可以使用变量来填充模板。而HTML解析器在解析文本时,并不会区分文本是否是带变量的文本。如果是纯文本,不需要进行任何处理;但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。
我们在9.2节中介绍过,每当HTML解析器解析到文本时,都会触发chars
函数,并且从参数中得到解析出的文本。在chars
函数中,我们需要构建文本类型的AST,并将它添加到父节点的children
属性中。
而在构建文本类型的AST时,纯文本和带变量的文本是不同的处理方式。如果是带变量的文本,我们需要借助文本解析器对它进行二次加工,其代码如下:
parseHTML(template, {
start (tag, attrs, unary) {
},
end () {
},
chars (text) {
text = text.trim()
if (text) {
const children = currentParent.children
let expression
if (expression = parseText(text)) {
children.push({
type : 2 ,
expression,
text
})
} else {
children.push({
type : 3 ,
text
})
}
}
},
comment (text) {
}
})
复制代码
在chars
函数中,如果执行parseText
后有返回结果,则说明文本是带变量的文本,并且已经通过文本解析器(parseText
)二次加工,此时构建一个带变量的文本类型的AST并将其添加到父节点的children
属性中。否则,就直接构建一个普通的文本节点并将其添加到父节点的children
属性中。而代码中的currentParent
是当前节点的父节点,也就是前面介绍的栈中的最后一个节点。
假设chars
函数被触发后,我们得到的text
是一个带变量的文本:
"Hello {{name}}"
复制代码
这个带变量的文本被文本解析器解析之后,得到的expression
变量是这样的:
"Hello " +_s(name)
复制代码
上面代码中的_s
其实是下面这个toString
函数的别名:
function toString (val ) {
return val == null
? ''
: typeof val === 'object'
? JSON .stringify(val, null , 2 )
: String (val)
}
复制代码
假设当前上下文中有一个变量name
,其值为Berwin
,那么expression
中的内容被执行时,它的内容是不是就是Hello Berwin
了?
我们举个例子:
var obj = {name : 'Berwin' }
with (obj) {
function toString (val ) {
return val == null
? ''
: typeof val === 'object'
? JSON .stringify(val, null , 2 )
: String (val)
}
console .log("Hello " +toString(name))
}
复制代码
在上面的代码中,我们打印出来的结果是"Hello Berwin"
。
事实上,最终AST会转换成代码字符串放在with
中执行,这部分内容会在第11章中详细介绍。
接着,我们详细介绍如何加工文本,也就是文本解析器的内部实现原理。
在文本解析器中,第一步要做的事情就是使用正则表达式来判断文本是否是带变量的文本,也就是检查文本中是否包含{{xxx}}
这样的语法。如果是纯文本,则直接返回undefined
;如果是带变量的文本,再进行二次加工。所以我们的代码是这样的:
function parseText (text ) {
const tagRE = /\{\{((?:.|\n)+?)\}\}/g
if (!tagRE(text)) {
return
}
}
复制代码
在上面的代码中,如果是纯文本,则直接返回。如果是带变量的文本,该如何处理呢?
一个解决思路是使用正则表达式匹配出文本中的变量,先把变量左边的文本添加到数组中,然后把变量改成_s(x)
这样的形式也添加到数组中。如果变量后面还有变量,则重复以上动作,直到所有变量都添加到数组中。如果最后一个变量的后面有文本,就将它添加到数组中。
这时我们其实已经有一个数组,数组元素的顺序和文本的顺序是一致的,此时将这些数组元素用+
连起来变成字符串,就可以得到最终想要的效果,如图9-5所示。
图9-5 文本解析过程
在图9-5中,最上面的字符串代表即将解析的文本,中间两个方块代表数组中的两个元素。最后,使用数组方法join
将这两个元素合并成一个字符串。
具体实现代码如下:
function parseText (text ) {
const tagRE = /\{\{((?:.|\n)+?)\}\}/g
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)))
}
tokens.push(`_s(${match[1 ].trim()} )` )
lastIndex = index + match[0 ].length
}
if (lastIndex < text.length) {
tokens.push(JSON .stringify(text.slice(lastIndex)))
}
return tokens.join('+' )
}
复制代码
这是文本解析器的全部代码,代码并不多,逻辑也不是很复杂。
这段代码有一个很关键的地方在lastIndex
:每处理完一个变量后,会重新设置lastIndex
的位置,这样可以保证如果后面还有其他变量,那么在下一轮循环时可以从lastIndex
的位置开始向后匹配,而lastIndex
之前的文本将不再被匹配。
下面用文本解析器解析不同的文本看看:
parseText('你好{{name}}' )
parseText('你好Berwin' )
parseText('你好{{name}}, 你今年已经{{age}}岁啦' )
复制代码
从上面代码的打印结果可以看到,文本已经被正确解析了。
9.5 总结
解析器的作用是通过模板得到AST(抽象语法树)。
生成AST的过程需要借助HTML解析器,当HTML解析器触发不同的钩子函数时,我们可以构建出不同的节点。
随后,我们可以通过栈来得到当前正在构建的节点的父节点,然后将构建出的节点添加到父节点的下面。
最终,当HTML解析器运行完毕后,我们就可以得到一个完整的带DOM层级关系的AST。
HTML解析器的内部原理是一小段一小段地截取模板字符串,每截取一小段字符串,就会根据截取出来的字符串类型触发不同的钩子函数,直到模板字符串截空停止运行。
文本分两种类型,不带变量的纯文本和带变量的文本,后者需要使用文本解析器进行二次加工。
更多精彩内容可以观看《深入浅出Vue.js》
关于《深入浅出Vue.js》
本书使用最最容易理解的文笔来描述Vue.js的内部原理,对于想学习Vue.js原理的小伙伴是非常值得入手的一本书。
京东:item.jd.com/12573168.ht…
亚马逊:www.amazon.cn/gp/product/…
当当:product.dangdang.com/26922892.ht…
扫码京东购买
转载于:https://juejin.im/post/5ca44160518825440a4b9fab
你可能感兴趣的:(Vue.js 模板解析器原理)
element实现动态路由+面包屑
软件技术NINI
vue案例 vue.js 前端
el-breadcrumb是ElementUI组件库中的一个面包屑导航组件,它用于显示当前页面的路径,帮助用户快速理解和导航到应用的各个部分。在Vue.js项目中,如果你已经安装了ElementUI,就可以很方便地使用el-breadcrumb组件。以下是一个基本的使用示例:安装ElementUI(如果你还没有安装的话):你可以通过npm或yarn来安装ElementUI。bash复制代码npmi
扫地机类清洁产品之直流无刷电机控制
悟空胆好小
清洁服务机器人 单片机 人工智能
扫地机类清洁产品之直流无刷电机控制1.1前言扫地机产品有很多的电机控制,滚刷电机1个,边刷电机1-2个,清水泵电机,风机一个,部分中高端产品支持抹布功能,也就是存在抹布盘电机,还有追觅科沃斯石头等边刷抬升电机,滚刷抬升电机等的,这些电机有直流有刷电机,直接无刷电机,步进电机,电磁阀,挪动泵等不同类型。电机的原理,驱动控制方式也不行。接下来一段时间的几个文章会作个专题分析分享。直流有刷电机会自动持续
基于社交网络算法优化的二维最大熵图像分割
智能算法研学社(Jack旭)
智能优化算法应用 图像分割 算法 php 开发语言
智能优化算法应用:基于社交网络优化的二维最大熵图像阈值分割-附代码文章目录智能优化算法应用:基于社交网络优化的二维最大熵图像阈值分割-附代码1.前言2.二维最大熵阈值分割原理3.基于社交网络优化的多阈值分割4.算法结果:5.参考文献:6.Matlab代码摘要:本文介绍基于最大熵的图像分割,并且应用社交网络算法进行阈值寻优。1.前言阅读此文章前,请阅读《图像分割:直方图区域划分及信息统计介绍》htt
如何部分格式化提示模板:LangChain中的高级技巧
nseejrukjhad
langchain java 服务器 python
标题:如何部分格式化提示模板:LangChain中的高级技巧内容:如何部分格式化提示模板:LangChain中的高级技巧引言在使用大型语言模型(LLM)时,提示工程是一个关键环节。LangChain提供了强大的提示模板功能,让我们能更灵活地构建和管理提示。本文将介绍LangChain中一个高级特性-部分格式化提示模板,这个技巧可以让你的提示管理更加高效和灵活。什么是部分格式化提示模板?部分格式化提
Day1笔记-Python简介&标识符和关键字&输入输出
~在杰难逃~
Python python 开发语言 大数据 数据分析 数据挖掘
大家好,从今天开始呢,杰哥开展一个新的专栏,当然,数据分析部分也会不定时更新的,这个新的专栏主要是讲解一些Python的基础语法和知识,帮助0基础的小伙伴入门和学习Python,感兴趣的小伙伴可以开始认真学习啦!一、Python简介【了解】1.计算机工作原理编程语言就是用来定义计算机程序的形式语言。我们通过编程语言来编写程序代码,再通过语言处理程序执行向计算机发送指令,让计算机完成对应的工作,编程
git - Webhook让部署自动化
大猪大猪
我们现在有一个需求,将项目打包上传到gitlab或者github后,程序能自动部署,不用手动地去服务器中进行项目更新并运行,如何做到?这里我们可以使用gitlab与github的挂钩,挂钩的原理就是,每当我们有请求到gitlab与github服务器时,这时他俩会根据我们配置的挂钩地扯进行访问,webhook挂钩程序会一直监听着某个端口请求,一但收到他们发过来的请求,这时就知道用户有请求提交了,这时
ARM驱动学习之基础小知识
JT灬新一
ARM 嵌入式 arm开发 学习
ARM驱动学习之基础小知识•sch原理图工程师工作内容–方案–元器件选型–采购(能不能买到,价格)–原理图(涉及到稳定性)•layout画板工程师–layout(封装、布局,布线,log)(涉及到稳定性)–焊接的一部分工作(调试阶段板子的焊接)•驱动工程师–驱动,原理图,layout三部分的交集容易发生矛盾•PCB研发流程介绍–方案,原理图(网表)–layout工程师(gerber文件)–PCB板
Faiss:高效相似性搜索与聚类的利器
网络·魚
大数据 faiss
Faiss是一个针对大规模向量集合的相似性搜索库,由FacebookAIResearch开发。它提供了一系列高效的算法和数据结构,用于加速向量之间的相似性搜索,特别是在大规模数据集上。本文将介绍Faiss的原理、核心功能以及如何在实际项目中使用它。Faiss原理:近似最近邻搜索:Faiss的核心功能之一是近似最近邻搜索,它能够高效地在大规模数据集中找到与给定查询向量最相似的向量。这种搜索是近似的,
ES聚合分析原理与代码实例讲解
光剑书架上的书
大厂Offer收割机 面试题 简历 程序员读书 硅基计算 碳基计算 认知计算 生物计算 深度学习 神经网络 大数据 AIGC AGI LLM Java Python 架构设计 Agent 程序员实现财富自由
ES聚合分析原理与代码实例讲解1.背景介绍1.1问题的由来在大规模数据分析场景中,特别是在使用Elasticsearch(ES)进行数据存储和检索时,聚合分析成为了一个至关重要的功能。聚合分析允许用户对数据集进行细分和分组,以便深入探索数据的结构和模式。这在诸如实时监控、日志分析、业务洞察等领域具有广泛的应用。1.2研究现状目前,ES聚合分析已经成为现代大数据平台的核心组件之一。它支持多种类型的聚
2022-08-28
蔚蓝一片晴
初三暑假培训收获点滴从8月25至8月27日三天两晚的培训结束了,回到家中,该静下心来整理一下触动心灵的收获,成为成长的积淀。1.在优秀团队中快速成长与提升,做一名反思成长型教师一名专业型教师的教学指导包括了教学原理知识、案例知识、策略知识。面对教学中的遇到的有趣的情形、问题会去研究其理,寻找更好的教法学法对策。从新手到成熟型教师,再走向专业型教师,需要的是觉醒与反思,多进行案例研究,从案例中观察、
STM32中的计时与延时
lupinjia
STM32 stm32 单片机
前言在裸机开发中,延时作为一种规定循环周期的方式经常被使用,其中尤以HAL库官方提供的HAL_Delay为甚。刚入门的小白可能会觉得既然有官方提供的延时函数,而且精度也还挺好,为什么不用呢?实际上HAL_Delay中有不少坑,而这些也只是HAL库中无数坑的其中一些。想从坑里跳出来还是得加强外设原理的学习和理解,切不可只依赖HAL库。除了延时之外,我们在开发中有时也会想要确定某段程序的耗时,这就需要
第1步win10宿主机与虚拟机通过NAT共享上网互通
学习3人组
大数据 大数据
VM的CentOS采用NAT共用宿主机网卡宿主机器无法连接到虚拟CentOS要实现宿主机与虚拟机通信,原理就是给宿主机的网卡配置一个与虚拟机网关相同网段的IP地址,实现可以互通。1、查看虚拟机的IP地址2、编辑虚拟机的虚拟网络的NAT和DHCP的配置,设置虚拟机的网卡选择NAT共享模式3、宿主机的IP配置,确保vnet8的IPV4属性与虚拟机在同一网段4、ping测试连通性[root@localh
5分钟说透AppStore审核原理,让你拥有上架新思路!
Q仔本人噢
在AppStore上架是越来越难了!相信非常多公司的技术人员都为此困扰,然而外包团队水平又层次不齐,容易遇坑,实在是内忧外患。是什么原因导致审核机制频繁调整?又是什么原因使得审核变得越发严格?那么接下来听小Q分解,马上给各位带来解答!首先看一下近一年的上下架的情况:近一年上架情况近一年下架情况通过数据我们发现越是马甲包产量权重高的分类里被下架的app数量越多,苹果此举可谓是上有政策,下有对策。通过
2019-03-24
李飞720
姓名:李飞企业名称:临沂鑫道食品有限公司组别373期利他1组日精进打卡第338天】【知~学习】1、阿米巴经营一段2、活用人才1段3、活法、一段【行~实践】一、修身:读书、抽烟减量、俯卧撑个跑步3公里二、齐家、劝说老爸与姑姑和好三、建功、业务洽谈【经典名句分享】1、依据原理原则追求事物的本质,以“作为人,何谓正确”进行判断2、经营者必须为员工物质和精神两方面的幸福殚精竭虑,倾尽全力,必须超脱私心,让
计算机木马详细编写思路
小熊同学哦
php 开发语言 木马 木马思路
导语:计算机木马(ComputerTrojan)是一种恶意软件,通过欺骗用户从而获取系统控制权限,给黑客打开系统后门的一种手段。虽然木马的存在给用户和系统带来严重的安全风险,但是了解它的工作原理与编写思路,对于我们提高防范意识、构建更健壮的网络安全体系具有重要意义。本篇博客将深入剖析计算机木马的详细编写思路,以及如何复杂化挑战,以期提高读者对计算机木马的认识和对抗能力。计算机木马的基本原理计算机木
在RabbitMQ中四种常见的消息路由模式
Xwzzz_
rabbitmq 分布式
1.Fanout模式Fanout模式的交换机是扇出交换机(FanoutExchange),它会将消息广播给所有绑定到它的队列,而不考虑消息的内容或路由键。工作原理:生产者发送消息到FanoutExchange。FanoutExchange会将消息广播给所有绑定到它的队列,所有绑定的队列都会收到这条消息。消费者监听绑定的队列,处理收到的消息。特点:没有路由键:消息不需要路由键,所有绑定的队列都会接收
C++八股
Petrichorzncu
八股总结 c++ 开发语言
这里写目录标题C++内存管理C++的构造函数,复制构造函数,和析构函数深复制与浅复制:构造函数和析构函数哪个能写成虚函数,为什么?C++数据结构内存排列结构体和类占用的内存:==虚函数和虚表的原理==虚函数虚表(Vtable)虚函数和虚表的实现细节==内存泄漏==指针的工作原理函数的传值和传址new和delete与malloc和freeC++内存区域划分C++11新特性C++常见新特性==智能指针
WebMagic:强大的Java爬虫框架解析与实战
Aaron_945
Java java 爬虫 开发语言
文章目录引言官网链接WebMagic原理概述基础使用1.添加依赖2.编写PageProcessor高级使用1.自定义Pipeline2.分布式抓取优点结论引言在大数据时代,网络爬虫作为数据收集的重要工具,扮演着不可或缺的角色。Java作为一门广泛使用的编程语言,在爬虫开发领域也有其独特的优势。WebMagic是一个开源的Java爬虫框架,它提供了简单灵活的API,支持多线程、分布式抓取,以及丰富的
博客网站制作教程
2401_85194651
java maven
首先就是技术框架:后端:Java+SpringBoot数据库:MySQL前端:Vue.js数据库连接:JPA(JavaPersistenceAPI)1.项目结构blog-app/├──backend/│├──src/main/java/com/example/blogapp/││├──BlogApplication.java││├──config/│││└──DatabaseConfig.java
《 C++ 修炼全景指南:十 》自平衡的艺术:深入了解 AVL 树的核心原理与实现
Lenyiin
C++ 修炼全景指南 技术指南 c++ 数据结构 stl
摘要本文深入探讨了AVL树(自平衡二叉搜索树)的概念、特点以及实现细节。我们首先介绍了AVL树的基本原理,并详细分析了其四种旋转操作,包括左旋、右旋、左右双旋和右左双旋,阐述了它们在保持树平衡中的重要作用。接着,本文从头到尾详细描述了AVL树的插入、删除和查找操作,配合完整的代码实现和详尽的注释,使读者能够全面理解这些操作的执行过程。此外,我们还提供了AVL树的遍历方法,包括中序、前序和后序遍历,
进销存小程序源码 PHP网络版ERP进销存管理系统 全开源可二开
摸鱼小号
php
可直接源码搭建部署发布后使用:一、功能模块介绍该系统模板主要有进,销,存三个主要模板功能组成,下面将介绍各模块所对应的功能;进:需要将产品采购入库,自动生成采购明细台账同时关联财务生成付款账单;销:是指对客户的销售订单记录,汇总生成产品销售明细及回款计划;存:库存的日常盘点与统计,库存下限预警、出入库台账、库存位置等。1.进购管理采购订单:采购下单审批→由上级审批通过采购入库;采购入库:货品到货>
非对称加密算法原理与应用2——RSA私钥加密文件
私语茶馆
云部署与开发 架构及产品灵感记录 RSA2048 私钥加密
作者:私语茶馆1.相关章节(1)非对称加密算法原理与应用1——秘钥的生成-CSDN博客第一章节讲述的是创建秘钥对,并将公钥和私钥导出为文件格式存储。本章节继续讲如何利用私钥加密内容,包括从密钥库或文件中读取私钥,并用RSA算法加密文件和String。2.私钥加密的概述本文主要基于第一章节的RSA2048bit的非对称加密算法讲述如何利用私钥加密文件。这种加密后的文件,只能由该私钥对应的公钥来解密。
为什么学生不喜欢上学
虾虾说
图片发自App《为什么学生不喜欢上学》作者是丹尼尔·威林厄姆。本书从认知心理学角度,结合大量实证案例,阐释了大脑工作的基本原理,回答了关于学习过程的一系列问题。为什么学生不喜欢上学?——大脑工作的基本原理思考是缓慢的、费力的、不可靠的。思考有三个要素,环境、工作记忆和长期记忆。环境是信息来源;长期记忆是知识、经验的巨型仓库,随时可以调取;工作记忆是中央处理器,是加工信息素材的中央厨房,也是思考过程
补充元象二面
Redstone Monstrosity
前端 面试
1.请尽可能详细地说明,防抖和节流的区别,应用场景?你的回答中不要写出示例代码。防抖(Debounce)和节流(Throttle)是两种常用的前端性能优化技术,它们的主要区别在于如何处理高频事件的触发。以下是防抖和节流的区别和应用场景的详细说明:防抖和节流的定义防抖:在一段时间内,多次执行变为只执行最后一次。防抖的原理是,当事件被触发后,设置一个延迟定时器。如果在这个延迟时间内事件再次被触发,则重
《 C++ 修炼全景指南:四 》揭秘 C++ List 容器背后的实现原理,带你构建自己的双向链表
Lenyiin
技术指南 C++ 修炼全景指南 c++ list 链表 stl
本篇博客,我们将详细讲解如何从头实现一个功能齐全且强大的C++List容器,并深入到各个细节。这篇博客将包括每一步的代码实现、解释以及扩展功能的探讨,目标是让初学者也能轻松理解。一、简介1.1、背景介绍在C++中,std::list是一个基于双向链表的容器,允许高效的插入和删除操作,适用于频繁插入和删除操作的场景。与动态数组不同,list允许常数时间内的插入和删除操作,支持双向遍历。这篇文章将详细
AI大模型的架构演进与最新发展
季风泯灭的季节
AI大模型应用技术二 人工智能 架构
随着深度学习的发展,AI大模型(LargeLanguageModels,LLMs)在自然语言处理、计算机视觉等领域取得了革命性的进展。本文将详细探讨AI大模型的架构演进,包括从Transformer的提出到GPT、BERT、T5等模型的历史演变,并探讨这些模型的技术细节及其在现代人工智能中的核心作用。一、基础模型介绍:Transformer的核心原理Transformer架构的背景在Transfo
【算法练习】IDEA集成leetcode插件实现快速刷
2401_84102892
2024年程序员学习 算法 intellij-idea leetcode
============点击右侧边leetcode->设置->配置地址、用户名、密码、存放目录、文件模板用户名要登录后在账号信息里看模板代码1.codefilename!velocityTool.camelC
[实践应用] 深度学习之优化器
YuanDaima2048
深度学习 工具使用 pytorch 深度学习 人工智能 机器学习 python 优化器
文章总览:YuanDaiMa2048博客文章总览深度学习之优化器1.随机梯度下降(SGD)2.动量优化(Momentum)3.自适应梯度(Adagrad)4.自适应矩估计(Adam)5.RMSprop总结其他介绍在深度学习中,优化器用于更新模型的参数,以最小化损失函数。常见的优化函数有很多种,下面是几种主流的优化器及其特点、原理和PyTorch实现:1.随机梯度下降(SGD)原理:随机梯度下降通过
vue render 函数详解 (配参数详解)
你的眼睛會笑
vue2 vue.js javascript 前端
vuerender函数详解(配参数详解)在Vue3中,`render`函数被用来代替Vue2中的模板语法。它接收一个h函数(或者是`createElement`函数的别名),并且返回一个虚拟DOM。render函数的语法结构如下:render(h){returnh('div',{class:'container'},'Hello,World!')}在上面的示例中,我们使用h函数创建了一个div元素
【高阶数据结构】并查集
椿融雪
数据结构与算法 数据结构 并查集
文章目录一、并查集原理二、并查集实现三、并查集应用一、并查集原理在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集合的运算。适合于描述这类问题的抽象数据类型称为并查集(union-findset)。比如:某公司今年校招全国总共招生10人,西安招4人,成都招3人,
解线性方程组
qiuwanchi
package gaodai.matrix;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner scanner = new Sc
在mysql内部存储代码
annan211
性能 mysql 存储过程 触发器
在mysql内部存储代码
在mysql内部存储代码,既有优点也有缺点,而且有人倡导有人反对。
先看优点:
1 她在服务器内部执行,离数据最近,另外在服务器上执行还可以节省带宽和网络延迟。
2 这是一种代码重用。可以方便的统一业务规则,保证某些行为的一致性,所以也可以提供一定的安全性。
3 可以简化代码的维护和版本更新。
4 可以帮助提升安全,比如提供更细
Android使用Asynchronous Http Client完成登录保存cookie的问题
hotsunshine
android
Asynchronous Http Client是android中非常好的异步请求工具
除了异步之外还有很多封装比如json的处理,cookie的处理
引用
Persistent Cookie Storage with PersistentCookieStore
This library also includes a PersistentCookieStore whi
java面试题
Array_06
java 面试
java面试题
第一,谈谈final, finally, finalize的区别。
final-修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在声明时给定初值,而在以后的引用中只能
网站加速
oloz
网站加速
前序:本人菜鸟,此文研究总结来源于互联网上的资料,大牛请勿喷!本人虚心学习,多指教.
1、减小网页体积的大小,尽量采用div+css模式,尽量避免复杂的页面结构,能简约就简约。
2、采用Gzip对网页进行压缩;
GZIP最早由Jean-loup Gailly和Mark Adler创建,用于UNⅨ系统的文件压缩。我们在Linux中经常会用到后缀为.gz
正确书写单例模式
随意而生
java 设计模式 单例
单例模式算是设计模式中最容易理解,也是最容易手写代码的模式了吧。但是其中的坑却不少,所以也常作为面试题来考。本文主要对几种单例写法的整理,并分析其优缺点。很多都是一些老生常谈的问题,但如果你不知道如何创建一个线程安全的单例,不知道什么是双检锁,那这篇文章可能会帮助到你。
懒汉式,线程不安全
当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样
单例模式
香水浓
java
懒汉 调用getInstance方法时实例化
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(null == ins
安装Apache问题:系统找不到指定的文件 No installed service named "Apache2"
AdyZhang
apache http server
安装Apache问题:系统找不到指定的文件 No installed service named "Apache2"
每次到这一步都很小心防它的端口冲突问题,结果,特意留出来的80端口就是不能用,烦。
解决方法确保几处:
1、停止IIS启动
2、把端口80改成其它 (譬如90,800,,,什么数字都好)
3、防火墙(关掉试试)
在运行处输入 cmd 回车,转到apa
如何在android 文件选择器中选择多个图片或者视频?
aijuans
android
我的android app有这样的需求,在进行照片和视频上传的时候,需要一次性的从照片/视频库选择多条进行上传
但是android原生态的sdk中,只能一个一个的进行选择和上传。
我想知道是否有其他的android上传库可以解决这个问题,提供一个多选的功能,可以使checkbox之类的,一次选择多个 处理方法
官方的图片选择器(但是不支持所有版本的androi,只支持API Level
mysql中查询生日提醒的日期相关的sql
baalwolf
mysql
SELECT sysid,user_name,birthday,listid,userhead_50,CONCAT(YEAR(CURDATE()),DATE_FORMAT(birthday,'-%m-%d')),CURDATE(), dayofyear( CONCAT(YEAR(CURDATE()),DATE_FORMAT(birthday,'-%m-%d')))-dayofyear(
MongoDB索引文件破坏后导致查询错误的问题
BigBird2012
mongodb
问题描述:
MongoDB在非正常情况下关闭时,可能会导致索引文件破坏,造成数据在更新时没有反映到索引上。
解决方案:
使用脚本,重建MongoDB所有表的索引。
var names = db.getCollectionNames();
for( var i in names ){
var name = names[i];
print(name);
Javascript Promise
bijian1013
JavaScript Promise
Parse JavaScript SDK现在提供了支持大多数异步方法的兼容jquery的Promises模式,那么这意味着什么呢,读完下文你就了解了。
一.认识Promises
“Promises”代表着在javascript程序里下一个伟大的范式,但是理解他们为什么如此伟大不是件简
[Zookeeper学习笔记九]Zookeeper源代码分析之Zookeeper构造过程
bit1129
zookeeper
Zookeeper重载了几个构造函数,其中构造者可以提供参数最多,可定制性最多的构造函数是
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd, boolea
【Java命令三】jstack
bit1129
jstack
jstack是用于获得当前运行的Java程序所有的线程的运行情况(thread dump),不同于jmap用于获得memory dump
[hadoop@hadoop sbin]$ jstack
Usage:
jstack [-l] <pid>
(to connect to running process)
jstack -F
jboss 5.1启停脚本 动静分离部署
ronin47
以前启动jboss,往各种xml配置文件,现只要运行一句脚本即可。start nohup sh /**/run.sh -c servicename -b ip -g clustername -u broatcast jboss.messaging.ServerPeerID=int -Djboss.service.binding.set=p
UI之如何打磨设计能力?
brotherlamp
UI ui教程 ui自学 ui资料 ui视频
在越来越拥挤的初创企业世界里,视觉设计的重要性往往可以与杀手级用户体验比肩。在许多情况下,尤其对于 Web 初创企业而言,这两者都是不可或缺的。前不久我们在《右脑革命:别学编程了,学艺术吧》中也曾发出过重视设计的呼吁。如何才能提高初创企业的设计能力呢?以下是 9 位创始人的体会。
1.找到自己的方式
如果你是设计师,要想提高技能可以去设计博客和展示好设计的网站如D-lists或
三色旗算法
bylijinnan
java 算法
import java.util.Arrays;
/**
问题:
假设有一条绳子,上面有红、白、蓝三种颜色的旗子,起初绳子上的旗子颜色并没有顺序,
您希望将之分类,并排列为蓝、白、红的顺序,要如何移动次数才会最少,注意您只能在绳
子上进行这个动作,而且一次只能调换两个旗子。
网上的解法大多类似:
在一条绳子上移动,在程式中也就意味只能使用一个阵列,而不使用其它的阵列来
警告:No configuration found for the specified action: \'s
chiangfai
configuration
1.index.jsp页面form标签未指定namespace属性。
<!--index.jsp代码-->
<%@taglib prefix="s" uri="/struts-tags"%>
...
<s:form action="submit" method="post"&g
redis -- hash_max_zipmap_entries设置过大有问题
chenchao051
redis hash
使用redis时为了使用hash追求更高的内存使用率,我们一般都用hash结构,并且有时候会把hash_max_zipmap_entries这个值设置的很大,很多资料也推荐设置到1000,默认设置为了512,但是这里有个坑
#define ZIPMAP_BIGLEN 254
#define ZIPMAP_END 255
/* Return th
select into outfile access deny问题
daizj
mysql txt 导出数据到文件
本文转自:http://hatemysql.com/2010/06/29/select-into-outfile-access-deny%E9%97%AE%E9%A2%98/
为应用建立了rnd的帐号,专门为他们查询线上数据库用的,当然,只有他们上了生产网络以后才能连上数据库,安全方面我们还是很注意的,呵呵。
授权的语句如下:
grant select on armory.* to rn
phpexcel导出excel表简单入门示例
dcj3sjt126com
PHP Excel phpexcel
<?php
error_reporting(E_ALL);
ini_set('display_errors', TRUE);
ini_set('display_startup_errors', TRUE);
if (PHP_SAPI == 'cli')
die('This example should only be run from a Web Brows
美国电影超短200句
dcj3sjt126com
电影
1. I see. 我明白了。2. I quit! 我不干了!3. Let go! 放手!4. Me too. 我也是。5. My god! 天哪!6. No way! 不行!7. Come on. 来吧(赶快)8. Hold on. 等一等。9. I agree。 我同意。10. Not bad. 还不错。11. Not yet. 还没。12. See you. 再见。13. Shut up!
Java访问远程服务
dyy_gusi
httpclient webservice get post
随着webService的崛起,我们开始中会越来越多的使用到访问远程webService服务。当然对于不同的webService框架一般都有自己的client包供使用,但是如果使用webService框架自己的client包,那么必然需要在自己的代码中引入它的包,如果同时调运了多个不同框架的webService,那么就需要同时引入多个不同的clien
Maven的settings.xml配置
geeksun
settings.xml
settings.xml是Maven的配置文件,下面解释一下其中的配置含义:
settings.xml存在于两个地方:
1.安装的地方:$M2_HOME/conf/settings.xml
2.用户的目录:${user.home}/.m2/settings.xml
前者又被叫做全局配置,后者被称为用户配置。如果两者都存在,它们的内容将被合并,并且用户范围的settings.xml优先。
ubuntu的init与系统服务设置
hongtoushizi
ubuntu
转载自:
http://iysm.net/?p=178 init
Init是位于/sbin/init的一个程序,它是在linux下,在系统启动过程中,初始化所有的设备驱动程序和数据结构等之后,由内核启动的一个用户级程序,并由此init程序进而完成系统的启动过程。
ubuntu与传统的linux略有不同,使用upstart完成系统的启动,但表面上仍维持init程序的形式。
运行
跟我学Nginx+Lua开发目录贴
jinnianshilongnian
nginx lua
使用Nginx+Lua开发近一年的时间,学习和实践了一些Nginx+Lua开发的架构,为了让更多人使用Nginx+Lua架构开发,利用春节期间总结了一份基本的学习教程,希望对大家有用。也欢迎谈探讨学习一些经验。
目录
第一章 安装Nginx+Lua开发环境
第二章 Nginx+Lua开发入门
第三章 Redis/SSDB+Twemproxy安装与使用
第四章 L
php位运算符注意事项
home198979
位运算 PHP &
$a = $b = $c = 0;
$a & $b = 1;
$b | $c = 1
问a,b,c最终为多少?
当看到这题时,我犯了一个低级错误,误 以为位运算符会改变变量的值。所以得出结果是1 1 0
但是位运算符是不会改变变量的值的,例如:
$a=1;$b=2;
$a&$b;
这样a,b的值不会有任何改变
Linux shell数组建立和使用技巧
pda158
linux
1.数组定义 [chengmo@centos5 ~]$ a=(1 2 3 4 5) [chengmo@centos5 ~]$ echo $a 1 一对括号表示是数组,数组元素用“空格”符号分割开。
2.数组读取与赋值 得到长度: [chengmo@centos5 ~]$ echo ${#a[@]} 5 用${#数组名[@或
hotspot源码(JDK7)
ol_beta
java HotSpot jvm
源码结构图,方便理解:
├─agent Serviceab
Oracle基本事务和ForAll执行批量DML练习
vipbooks
oracle sql
基本事务的使用:
从账户一的余额中转100到账户二的余额中去,如果账户二不存在或账户一中的余额不足100则整笔交易回滚
select * from account;
-- 创建一张账户表
create table account(
-- 账户ID
id number(3) not null,
-- 账户名称
nam