投身烈火
Keep Walking

vue源码阅读笔记(2)

啊,又到deadline了……话说因为之前一直在整理上一篇笔记,所以过了这么长时间一直都没怎么读新的,真是惭愧啊……那么这次就读两个简单的模块吧,把简单的解决了,后续再啃硬骨头。

解析单文件组件 sfc

之前在说目录结构的时候说过,sfc模块是用来解析.vue文件的,sfc貌似就是single file component的意思……( ̄. ̄;) 整个模块只有一个文件,但是逻辑却不简单呢……另外在官方文档中对单文件组件的描述也只有短短一个章节,所以感觉这部分通过源码能更充分的了解细节呢,那么先看下代码逻辑:

parser.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// ...
// 用来匹配换行的正则表达式
const splitRE = /\r?\n/g
// 定义了一个判断标签是否是script、style、template标签的函数
const isSpecialTag = makeMap('script,style,template', true)
// 解析单个.vue文件的函数
export function parseComponent (
// 这里特意没有去掉flow的语法,顺便记录下flow的用法
// 参数后面加(: 类型)表示参数的类型
content: string,
// 参数后面加(?: 类型)表示可省略参数和类型, (= {}) 是es6语法,表示参数的默认值……
options?: Object = {}
// (function(): 类型)表示函数的返回值类型,
// 这里的SFCDescriptor是一个自定义类型,定义在flow/complier.js里面
// 感觉有点像结构体(struct),另外flow还支持interface和class的定义
): SFCDescriptor {
// 最后导出的sfc对象,分为template、script、style和自定义块四部分
// 其中style和自定义块允许多个,template和script只允许一个
const sfc: SFCDescriptor = {
template: null,
script: null,
styles: [],
customBlocks: []
}
// 因为最后是使用compiler/parser/html-parser模块进行解析的,而html-parser模块会根据dom结构进行递归解析的,
// 所以每个代码块都有自己的深度,这里的depth就是用来标记深度的,从后续的代码中可以看出,sfc/parser模块不会处理嵌套的块,只处理一层,这个变量被用来做锁了……
let depth = 0
// 当前处理的代码块
let currentBlock: ?(SFCBlock | SFCCustomBlock) = null
// 稍微调整了下代码的顺序,这个parseHTML的调用本来是放到最后的,这样其实也不会报错吧~
// 这个调用也是整个parseComponent函数最核心的部分,使用compiler/parser/html-parser模块的能力来解析模板
// parseHTML函数接收一个options参数(第二个),里面可以设置匹配到标签开始和结束时的钩子,通过钩子来获取自己写想要的内容,
// 就是说,比如有个<tag>xxx</tag>这样的内容,匹配到<tag>时,执行start,匹配到</tag>时,执行end,
// 如果匹配到<tab/>,就只执行start,貌似是这样吧,我只粗略的看了下parseHTML,后续详细看发现错了再纠正。
// 其实直接运行parseHTML不加options也是可以的,只不过不会返回任何的内容,是完全无意义的操作呢……╮( ̄▽ ̄)╭
parseHTML(content, {
start,
end
})
// 匹配到标签开始时的钩子,主要是对标签的属性进行处理
function start (
tag: string,
attrs: Array<Attribute>,
unary: boolean,
start: number,
end: number
) {
// 根据当前解析深度进行判断
if (depth === 0) {
// 如果当前深度是0,也就是说不是嵌套的标签,则进行处理
// 先缓存当前块的信息
currentBlock = {
type: tag,
content: '',
// 这里的start是用来标记标签内的内容的起点的
start: end,
// 设置属性列表这个地方有意思,通过函数定义的flow里面可以看到arrts是个数组,这里通过数组的reduce特性直接把一个[{"key","value"}...]形式的数组转换为{"key":"value"...}形式的对象了
attrs: attrs.reduce((cumulated, { name, value }) => {
cumulated[name] = value || true
return cumulated
}, Object.create(null))
}
// 判断是否是特殊标签
if (isSpecialTag(tag)) {
// 是特殊标签,则检查标签属性,并对特殊属性进行处理
checkAttrs(currentBlock, attrs)
// 检查是否是style标签
if (tag === 'style') {
// 是style标签就推进队列
sfc.styles.push(currentBlock)
} else {
// 不是就直接赋值
sfc[tag] = currentBlock
}
} else {
// 不是特殊标签,则推进自定义标签的队列
sfc.customBlocks.push(currentBlock)
}
}
// 这个属性是根据parseHTML接收的options.isUnaryTag返回的,因为在调用parseHTML时没传,所以unary总是false
if (!unary) {
// 上锁
depth++
}
}
// 检查标签的特殊属性,以便做特殊的处理,特殊属性的使用方法貌似文档中都没有写呢,
// 貌似这些属性都是给style标签用的,我是从https://github.com/vuejs-templates/webpack 这个项目中看到的相关例子
function checkAttrs (block: SFCBlock, attrs: Array<Attribute>) {
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i]
// 可以用lang标签设置style标签内用的预处理语法,less,sass之类的
if (attr.name === 'lang') {
block.lang = attr.value
}
// 如果设置了scoped属性,那么这个标签就只对当前组件有作用
if (attr.name === 'scoped') {
block.scoped = true
}
// 没找到这个属性是干嘛使的……╮( ̄▽ ̄)╭有可能是为了配合src来用的,module作为src的前缀,
// 具体参考 https://github.com/vuejs-templates/webpack/blob/17351f5e3b1306a117aaa80b7d575b9aa3144866/docs/static.md#asset-resolving-rules URLs prefixed with 这一小节的说明。
if (attr.name === 'module') {
block.module = attr.value || true
}
// 用src属性设置内容对应的文件
if (attr.name === 'src') {
block.src = attr.value
}
}
}
// 匹配到标签结束时调用的钩子,主要是对标签里的内容进行处理
function end (tag: string, start: number, end: number) {
// 检查锁的状态,并且标签不是不对称标签(不是<tag/>这样的)
if (depth === 1 && currentBlock) {
// end标记的是标签内的内容结束的位置
currentBlock.end = start
// 去除标签内的缩进,deindent是尤大大专门为了去除缩进开发的模块……
let text = deindent(content.slice(currentBlock.start, currentBlock.end))
// 判断是不是template标签,不是统一都要加pad,目的是在lint报错时,报错信息行数能对应上……
if (currentBlock.type !== 'template' && options.pad) {
text = padContent(currentBlock) + ext
}
// 给content属性赋值
currentBlock.content = text
// 至空currentBlock的引用,currentBlock其实已经保存在sfc的属性的引用上了,currentBlock其实只是个临时变量,这里充分的运用了js对象都是引用类型的特性呢……
currentBlock = null
}
// 解锁
depth--
}
// 用来生成能跟.vue文件行数对应上的内容用的……用来对应lint软件或者预编译软件的报错信息的行数……
// 话说做框架可真不容易呢,不止要实现功能,连报错信息能不能对应上都要考虑……
function padContent (block: SFCBlock | SFCCustomBlock) {
// 获取当前这段代码到底在多少行
const offset = content.slice(0, block.start).split(splitRE).length
// 根据不同的块使用不同的换行……
const padChar = block.type === 'script' && !block.lang
? '//\n'
: '\n'
// 最后返回对应行数的换行
return Array(offset).join(padChar)
}
// 最后返回实例
return sfc
}

总结

怎么说呢,虽然sfc这个模块很短,但是详细分析起来还是挺费时间的……连读带写花了我4个小时啊……总的来说作用就是将文本解析成对象,话说如果要是自己也准备写dom类文件解析的,可以参考这部分的功能呢。看完这部分我觉得比较有趣的点有:

  • flow真是好用啊,特别是看函数的时候,有了参数类型和返回类型的标注,一下就能理解用途了,连文档都省了,这么看来typescript也没那么那接受了的说……有机会一定要在项目中实践一下
  • 使用reduce把数组转成对象。这个还真是开眼了以前没见过这种方法呢,以后可以借鉴到自己开发中
  • 解析dom文本的方法,话说如果自己也要写解析dom操作的话(比如要写爬虫或者写要读xml、svg之类的?),可以借鉴这部分的代码呢,因为用了钩子的形式来扩展自定义操作,所以扩展性还挺强的
  • 为了对应lint报错而使用pad这种方法……怎么说呢,为了良好的开发体验作者还挺下功夫的呢,如果以后自己也要开发框架,借鉴这类细节肯定会给自己的作品大大加分的~

以上就是我的感悟,那么你的感悟又是什么呢?有想法就留言告诉我吧,欢迎各路灌水拍砖~( ̄▽ ̄)

好的那么由于时间不足本次的博客就到这里,话说我发觉笔记这种东西就得每天读每天写效果才更好,所以我觉得要不要就改成日更算了~所以如果不出意外的话,大概可能maybe也许明天就会更新了呢~!能不能准时更新,就全看米娜桑点赞打赏转发安利发评论的热情啦~!

白了个白~!