Svelte的响应式相对Vue来说还是比较简单的,其难点在于根据我们写的代码,编译生成正确的代码,因此通过手写一些可以运行的代码,而不是像前一篇文章只是复制源码,来加深对Svelte的理解。
ast语法树
<script>
let name = 'world'
</script>
<h1>Hello {name} </h1>
上面的代码会被编译成如下图所示的ast。
我们先关注上面代码中的html部分,这个组件中的html部分会被编译4种ast节点。
- Fragment:所有节点的祖先节点。
- Element: html节点,上面的h1就会对应一个element。
- Text: 文本节点,上面script和h1之间的换行符(\n),Hello 文本和{name}后面的空格都会编译成一个Text节点。
- MustacheTag:{}之间的内容
每个节点都是用object来表示的,其中的属性意义如下表所示。
| 属性 | 意义 |
|---|---|
| start | 字符串开始位置 |
| end | 字符串结束位置 |
| type | 节点类型有四种(Fragement,text,Element,MustacheTag) |
| raw | 文本内容,text节点独有 |
| chidlren | 子节点,Fragement和Element独有 |
| expression | {}中的表达式,MstacheTag独有 |
| attributes | html属性,elemnt独有 |
构建语法树
<h1>hello {name}</h1>解析这样子的字符串,(状态机)我们的思路是用四个函数来分别表示四种节点,然后再一个循环依次读取每一个字符,遇到字符'<'就调用解析element的函数,遇到'{'就调用解析MstacheTag函数。
let state = fragment
class Parser{
index = 0 // 代码字符串的下标
constructor(template) {
this.template = template
while (this.index < this.template.length) {
state = state(this) || fragment
}
}
match(c) {
return this.template[this.index] === c
}
}
function fragment(parser) {}
function element(parser) {}
function text(parser) {}
function mstancheTag(parser) {}
现在我们来解释一下这一段代码的具体意思,Parser接受代码字符串输入,每一个字符都调用对应的函数进行处理,每个函数都要消耗一个字符,并且根据是否要把state设置成为其他状态,还是保持在本状态里面。 我们先来实现fragment函数。
function fragment(parser) {
if (parser.match('<')) {
return element // 如果是字符'<',通过返回element函数,使得全局变量的值变为element,从而进入解析element。
}
if (parser.match('{')) {
return mstancheTag // 使状态进入mstancheTag
}
return text // 使状态进入text
}
接下来我们来实现一下text函数。进入text函数只要不是字符'<'和字符字符'{'我们都保持在text函数里面。
function text(parser) {
const start = parser.index // text节点开始的位置.
let data = '' // 文本的内容,初始化为空字符.
while (
parser.index < parser.template.length && // 不能超过输入代码字符的长度.
!parser.match('<') && // 预读一个字符,不能为'<', 注意此时不能消耗字符.
!parser.match('{') //
) {
data += parser.template[parser.index] // 读取文本
parser.index++ // 注意此时一定要消耗一个字符,否则进入死循环中去。
}
// 此时我们已经得到文本节点的全部内容了,可以构建文本节点了。
const node = {
type: 'text',
start,
end: start + data.length - 1,
raw: data
}
// 不返回任何状态,使得进入默认的fragment进行重新判断。
}
text函数做的事情上面的注释已经解释的很清楚了,接下我们来看一下element函数的实现。 elment函数实现起来就没有那么简单了,我们要解析的要attribute,Svelte自定义的tag,还有是否是注释节点等等,因此目前我们只读取tag的名字即可,往后的内容在慢慢补充。
function element(parser) {
// fragment函数没有消耗<,因此此时我们要消耗掉字符<
const start = parser.index++
const tagName = readTagName(parser)
// 构建element元素
const element = {
type: 'Element',
start,
name: tagName,
end: null, // 以后再填充
children: [],
attribute: []
}
}
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/
function readTagName(parser) {
/**
* 读取名字中断在三种情况。
* 1. 碰到空格,这种情况有可能name后面有attribute,或者直接结束。
* 2. 名字后面直接是字符'/',是自闭标签。
* 3. 名字后面直接跟字符'>',说明标签的开始部分已经结束了。
*/
const name = parser.read_util(regex_whitespace_or_slash_or_closing_tag)
return name
}
Parser的成员函数实现如下。
class Parser {
// 其他的代码,如上。
read_util(pattern) {
if (this.index > this.template.index) {
throw Error('超出了template的长度了')
}
const start = this.index
const match = pattern.exec(this.template.slice(start))
if (match) {
// 消耗的字符为匹配的长度加上初始长度
this.index = start + match.index
return this.template.slice(start, this.index)
}
// 不匹配情况,直接消耗完整个字符串,提前退出循环
this.index = this.template.length
return this.template.slice(start)
}
}
接下来我们来实现mstancheTag,在此之前我们往Parser添加一个新的成员函数skip_white_space
class Parser {
// 其他代码如上
skip_white_space() {
while (
this.index < this.template.length &&
regex_whitespace.test(this.template[this.index])
) {
this.index++
}
}
}
现在我们来实现mstancheTag
function mstancheTag(parser) {
// 同理先消耗字符'{'
const start = parser.index++
parser.skip_white_space()
const expression = readExpression(parser)
parser.skip_white_space()
parser.expect('}')
const node = {
type: 'MstancheTag',
start,
end: parser.index,
expression
}
}
// 暂时先{}中的内容当做字符串来处理,实际中间的内容是表达式
function readExpression(parser) {
let data = ''
while (
parser.index < parser.template &&
parser.template[parser.index] !== '}'
) {
data += parser.template[parser.index++]
}
return data
}
至此构建ast节点的内容我们已经完成了,接下我们要把这些节点连接成为一颗ast树。
首先我们在Parser的construct中新建一个节点代表所有节点的祖先节点。又因为html子节点有可能嵌套多层,因此我们在Parser声明一个栈,栈顶的元素永远是当前解析节点的父节点。
class Parser {
stack = []
construnctor(tempalte) {
// 其他代码如上
this.html = { // 代表ast节点的根节点
type: 'Fragement',
start: 0,
end: template.length,
children: []
}
this.stack.push(this.html)
while (this.index < this.template.length) {
state = state(this) || fragment
}
}
// 获取栈顶元素
current () {
return this.stack[this.stack.length - 1]
}
}
接下来我们把text节点添加到ast树中去。
function text(parser) {
// 省略其他代码
parser.current().children.push(node)
}
mstancheTag节点也是同理用一句简单的parser.current().children.push(node)即可把mstancheTag添加到Ast树当中去。
element节点我们有一些特殊的情况要处理一下,除了用成员函数current()获取父元素,从而添加到父元素的children数组中去。我们还要当前新创建的元素压入stack中去,这是因为当前元素可能还是有子元素,而且关闭标签的时候如,我们要栈顶元素弹出,并且检查站定元素的name是否与我们是否与关闭标签的名字一样,如果不一样的话说明标签是没有关闭的我们要报错。
通过以上的分析我们就可以写下以下代码。
function element(parser) {
const start = parser.index++
const parent = parser.current() // 拿到栈顶元素
// <字符后面跟着是字符/情况下,j解析到了标签的关闭部分,比如 </button>
const is_closing_tag = parser.expect('/') //
const tagName = readTagName(parser)
// 构建element元素
const element = {
type: 'Element',
start,
name: tagName,
end: null, // 以后再填充
children: [],
attribute: []
}
if (is_closing_tag) {
// 关闭标签,解析完名字之后就剩字符'>'
// 例如</button>名字button解析之后,后面应该跟>,否则就报错。
parser.expect('>', true)
if (
parent.type !== element.type ||
parent.name !== element.name
) {
throw Error('标签没有正确关闭')
}
parent.end = parser.index
parser.stack.pop() // 此时一对标签已经正确解析了
return
}
parser.current().children.push(element)
// 如果不是关闭标签,解析完名字之后,我们要看一下是否是自关闭标签。
parser.skip_white_space()
const self_close = parser.expect('/')
parser.expect('>', true)
if (self_close) {
element.end = parser.index
} else {
parser.stack.push(element)
}
}
同时我们在向Parser添加新的成员函数expect。
class Parser {
// 省略其他代码
expect(c, require) {
if (
this.index < this.template.length &&
c === this.template[this.index]
) {
this.index++
return true
}
if (require) {
throw Error(`期待字符${c},但是却是字符${this.template[this.index]}`
}
return false
}
}
现在我们来写一些测试用例,来看一下我们的实现是否正确。
console.info('test unit 1')
const test1 = '<button>hello {name}</button>'
const parser = new Parser(test1)
console.info(parser.html.type === 'Fragment')
console.info(parser.html.children.length === 1)
console.info(parser.html.children[0].type === 'Element')
console.info(parser.html.children[0].name === 'button')
const children = parser.html.children[0].children
console.info(children.length === 2)
console.info(children[0].type === 'Text')
console.info(children[0].raw === 'hello ')
console.info(children[1].type === 'MstancheTag')
console.info(children[1].expression === 'name')
console.info('test unit 2')
const test2 = '<p><button>test</button></p>'
const parser2 = new Parser(test2)
console.info(parser2.html.children.length === 1)
console.info(parser2.html.children[0].type === 'Element')
console.info(parser2.html.children[0].name === 'p')
console.info(parser2.html.children[0].children[0].type === 'Element')
console.info(parser2.html.children[0].children[0].name === 'button')
console.info(parser2.html.children[0].children[0].children[0].type === 'Text')
console.info(parser2.html.children[0].children[0].children[0].raw === 'test')
我们暂时不用专门的测试框架,先用console肉眼看一下是否正确,如果你运行以上代码的话,你会看见控制台输出的全部是true。
未完待续!