├── screenshots ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png └── 8.png ├── demo.html ├── README.md └── vue-ast.js /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwStar/vue-ast/HEAD/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwStar/vue-ast/HEAD/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwStar/vue-ast/HEAD/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwStar/vue-ast/HEAD/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwStar/vue-ast/HEAD/screenshots/5.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwStar/vue-ast/HEAD/screenshots/6.png -------------------------------------------------------------------------------- /screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwStar/vue-ast/HEAD/screenshots/7.png -------------------------------------------------------------------------------- /screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwStar/vue-ast/HEAD/screenshots/8.png -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 7 | 8 |
117 |
118 |
119 | 接着把match返回出去 作为调用handleStartTag的参数
120 |
121 | ```
122 | var startTagMatch = parseStartTag(); //处理后得到match
123 | if (startTagMatch) {
124 | handleStartTag(startTagMatch);
125 | continue
126 | }
127 | ```
128 |
129 | 接下来看handleStartTag这个函数:
130 |
131 | ```
132 | function handleStartTag(match) {
133 | var tagName = match.tagName;
134 | var unary = isUnaryTag$$1(tagName) //判断是否为闭合标签
135 | var l = match.attrs.length;
136 | var attrs = new Array(l);
137 | for (var i = 0; i < l; i++) {
138 | var args = match.attrs[i];
139 | var value = args[3] || args[4] || args[5] || '';
140 | attrs[i] = {
141 | name: args[1],
142 | value: value
143 | };
144 | }
145 | if (!unary) {
146 | stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs});
147 | lastTag = tagName;
148 | }
149 | if (options.start) {
150 | options.start(tagName, attrs, unary, match.start, match.end);
151 | }
152 | }
153 | ```
154 | 函数中分为3部分 第一部分是for循环是对attrs进行转化,我们从上一步的parseStartTag()得到的match对象中的attrs属性如图
155 |
156 |
157 |
158 | 当时attrs是上面图这样子滴 我们通过这个循环把它转化为只带name 和 value这2个属性的对象 如图:
159 |
160 |
161 |
162 | 接着判断如果不是自闭合标签,把标签名和属性推入栈中(注意 这里的stack这个变量在parseHTML中定义,作用是为了存放标签名 为了和结束标签进行匹配的作用。)接着调用最后一步 options.start 这里的options就是我们在parse函数中 调用parseHTML是传进来第二个参数的那个对象(包含start end chars 3个方法函数) 这里开始看options.start这个函数的作用:
163 |
164 | ```
165 | start: function start(tag, attrs, unary) {
166 | var element = {
167 | type: 1,
168 | tag: tag,
169 | attrsList: attrs,
170 | attrsMap: makeAttrsMap(attrs),
171 | parent: currentParent,
172 | children: []
173 | };
174 | processAttrs(element);
175 | if (!root) {
176 | root = element;
177 | }
178 | if(currentParent){
179 | currentParent.children.push(element);
180 | element.parent = currentParent;
181 | }
182 | if (!unary) {
183 | currentParent = element;
184 | stack.push(element);
185 | }
186 | }
187 | ```
188 | 这个函数中 生成element对象 再连接元素的parent 和 children节点 最终push到栈中
189 | 此时栈中第一个元素生成 如图:
190 |
191 |
192 |
193 |
194 |
195 | 完成了while循环的第一次执行,进入第二次循环执行,这个时候html变成`{{message}}` 接着截取到 处理过程和第一次一致 经过这次循环stack中元素如图:
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | 接着继续执行第三个循环 这个时候是处理文本节点了 {{message}}
204 |
205 | ```
206 | // 初始化为undefined 这样安全且字符数少一点
207 | var text = (void 0), rest = (void 0), next = (void 0);
208 | if (textEnd >= 0) { // 截取<字符索引 => 这里截取到闭合的<
209 | rest = html.slice(textEnd); //截取闭合标签
210 | // 处理文本中的<字符
211 | // 获取中间的字符串 => {{message}}
212 | text = html.substring(0, textEnd); //截取到闭合标签前面部分
213 | advance(textEnd); //切除闭合标签前面部分
214 | }
215 | // 当字符串没有<时
216 | if (textEnd < 0) {
217 | text = html;
218 | html = '';
219 | }
220 | // 另外一个函数
221 | if (options.chars && text) {
222 | options.chars(text);
223 | }
224 | ```
225 | 这里的作用就是把文本提取出来 调用options.chars这个函数 接下来看options.chars
226 |
227 | ```
228 | chars: function chars(text) {
229 | if (!currentParent) { //如果没有父元素 只是文本
230 | return
231 | }
232 |
233 | var children = currentParent.children; //取出children
234 | // text => {{message}}
235 | if (text) {
236 | var expression;
237 | if (text !== ' ' && (expression = parseText(text))) {
238 | // 将解析后的text存进children数组
239 | children.push({
240 | type: 2,
241 | expression: expression,
242 | text: text
243 | });
244 | } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
245 | children.push({
246 | type: 3,
247 | text: text
248 | });
249 | }
250 | }
251 | }
252 | })
253 | ```
254 | 这里的主要功能是判断文本是{{xxx}}还是简单的文本xxx,如果是简单的文本 push进父元素的children里面,type设置为3,如果是字符模板{{xxx}},调用parseText转化。如这里的`{{message}}`转化为 `_s(message)`(加上_s是为了AST的下一步转为render函数,本文中暂时不会用到。) 再把转化后的内容push进children。
255 |
256 |
257 |
258 |
259 | 又走完一个循环了,这个时候html = `` 剩下2个结束标签进行匹配了
260 |
261 | ```
262 | var endTagMatch = html.match(endTag);
263 | if (endTagMatch) {
264 | var curIndex = index;
265 | advance(endTagMatch[0].length);
266 | parseEndTag(endTagMatch[1], curIndex, index);
267 | continue
268 | }
269 | ```
270 | 接下来看parseEndTag这个函数 传进来了标签名 开始索引和结束索引
271 |
272 | ```
273 | function parseEndTag(tagName, start, end) {
274 | var pos, lowerCasedTagName;
275 | if (tagName) {
276 | lowerCasedTagName = tagName.toLowerCase();
277 | }
278 | // Find the closest opened tag of the same type
279 | if (tagName) { // 获取最近的匹配标签
280 | for (pos = stack.length - 1; pos >= 0; pos--) {
281 | // 提示没有匹配的标签
282 | if (stack[pos].lowerCasedTag === lowerCasedTagName) {
283 | break
284 | }
285 | }
286 | } else {
287 | // If no tag name is provided, clean shop
288 | pos = 0;
289 | }
290 |
291 | if (pos >= 0) {
292 | // Close all the open elements, up the stack
293 | for (var i = stack.length - 1; i >= pos; i--) {
294 | if (options.end) {
295 | options.end(stack[i].tag, start, end);
296 | }
297 | }
298 |
299 | // Remove the open elements from the stack
300 | stack.length = pos;
301 | lastTag = pos && stack[pos - 1].tag;
302 | }
303 | ```
304 | 这里首先找到栈中对应的开始标签的索引pos,再从该索引开始到栈顶的所以元素调用options.end这个函数
305 |
306 | ```
307 | end: function end() {
308 | // pop stack
309 | stack.length -= 1;
310 | currentParent = stack[stack.length - 1];
311 | },
312 | ```
313 | 把栈顶元素出栈,因为这个元素已经匹配到结束标签了,再把当前父元素更改。终于走完了,把html的内容循环完,最终return root 这个root就是我们所要得到的AST
314 |
315 |
316 |
317 | 这只是Vue的冰山一角,文中有什么不对的地方请大家帮忙指正,本人最近也一直在学习Vue的源码,希望能够拿出来与大家一起分享经验,接下来会继续更新后续的源码,如果觉得有帮忙请给个Star哈
318 |
319 | ## 项目运行
320 | ```
321 | git clone https://github.com/zwStar/vue-ast
322 |
323 | 打开demo.html
324 |
325 | F12打开控制台 然后在input中输入template 控制台可以看到转换后的AST树
326 |
327 | ```
328 |
329 |
--------------------------------------------------------------------------------
/vue-ast.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | (global.Parse = factory())
3 | }(this, (function () {
4 | var ncname = '[a-zA-Z_][\\w\\-\\.]*';
5 | var qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')';
6 | var startTagOpen = new RegExp('^<' + qnameCapture); //匹配开始标签的 <
7 | var startTagClose = /^\s*(\/?)>/; //匹配 开始标签的 >
8 | var endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>'); //匹配结束标签
9 |
10 | // Regular Expressions for parsing tags and attributes
11 | var singleAttrIdentifier = /([^\s"'<>/=]+)/;
12 | var singleAttrAssign = /(?:=)/;
13 | var singleAttrValues = [
14 | // attr value double quotes
15 | /"([^"]*)"+/.source,
16 | // attr value, single quotes
17 | /'([^']*)'+/.source,
18 | // attr value, no quotes
19 | /([^\s"'=<>`]+)/.source
20 | ];
21 | var attribute = new RegExp(
22 | '^\\s*' + singleAttrIdentifier.source +
23 | '(?:\\s*(' + singleAttrAssign.source + ')' +
24 | '\\s*(?:' + singleAttrValues.join('|') + '))?'
25 | );
26 |
27 | var defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g; //匹配 {{xxxx}}
28 | function makeMap(str,
29 | expectsLowerCase //true
30 | ) {
31 | var map = Object.create(null); //创建一个对象
32 | var list = str.split(',');
33 | for (var i = 0; i < list.length; i++) {
34 | map[list[i]] = true;
35 | }
36 | return expectsLowerCase
37 | ? function (val) {
38 | return map[val.toLowerCase()];
39 | }
40 | : function (val) {
41 | return map[val];
42 | }
43 | }
44 |
45 | var isUnaryTag = makeMap( // 自闭合标签
46 | 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +
47 | 'link,meta,param,source,track,wbr'
48 | );
49 |
50 | function Parse(template) {
51 | return this._init(template);
52 | }
53 | function parseHTML(html, options) {
54 | var stack = [];
55 | var isUnaryTag$$1 = isUnaryTag; //判断是否为自闭合标签
56 | var index = 0;
57 | var last, lastTag; //lastTag 为了匹配结束标签 因为执行一次后 lastTag会被赋值tagName
58 | while (html) {
59 | // 第一次进入while循环时,由于字符串以<开头,所以进入startTag条件,并进行AST转换,最后将对象弹入stack数组中
60 | // 而这一次,字符串开头为{,所以会继续执行下面的代码。代码将{{message}}作为text抽离出来,并调用了参数中另外一个函数:options
61 | last = html;
62 | var textEnd = html.indexOf('<');
63 | if (textEnd === 0) { // 此时字符串是不是以<开头
64 | // End tag:
65 | var endTagMatch = html.match(endTag);
66 | if (endTagMatch) {
67 | var curIndex = index;
68 | advance(endTagMatch[0].length);
69 | parseEndTag(endTagMatch[1], curIndex, index);
70 | continue
71 | }
72 |
73 | // Start tag: // 匹配起始标签
74 | var startTagMatch = parseStartTag(); //处理后得到match
75 | if (startTagMatch) {
76 | handleStartTag(startTagMatch);
77 | continue
78 | }
79 | }
80 |
81 | // 初始化为undefined 这样安全且字符数少一点
82 | var text = (void 0), rest = (void 0), next = (void 0);
83 | if (textEnd >= 0) { // 截取<字符索引 => 这里截取到闭合的<
84 | rest = html.slice(textEnd); //截取闭合标签
85 | // 处理文本中的<字符
86 | // 获取中间的字符串 => {{message}}
87 | text = html.substring(0, textEnd); //截取到闭合标签前面部分
88 | advance(textEnd); //切除闭合标签前面部分
89 |
90 | }
91 | // 当字符串没有<时
92 | if (textEnd < 0) {
93 | text = html;
94 | html = '';
95 | }
96 | // // 另外一个函数
97 | if (options.chars && text) {
98 | options.chars(text);
99 | }
100 | }
101 |
102 | // 该函数将函数局部变量index往前推 并切割字符串
103 | function advance(n) {
104 | index += n;
105 | html = html.substring(n);
106 | }
107 |
108 | function parseStartTag() { //返回匹配对象
109 | var start = html.match(startTagOpen); // 正则匹配
110 | if (start) {
111 | var match = {
112 | tagName: start[1], // 标签名(div)
113 | attrs: [], // 属性
114 | start: index // 游标索引(初始为0)
115 | };
116 | advance(start[0].length);
117 | var end, attr; // 进行属性的正则匹配
118 | // startTagClose匹配/>或> attribute匹配属性 正则太长 没法讲 本例中attr匹配后 => ['id=app','id','=','app']
119 | while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { //如果不是 > 标签 并且是attribute 比如标签的文本*/
247 | if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
248 | element.children.pop();
249 | }
250 | // pop stack
251 | stack.length -= 1;
252 | currentParent = stack[stack.length - 1];
253 | },
254 | chars: function chars(text) {
255 | if (!currentParent) { //如果没有父元素 只是文本
256 | return
257 | }
258 |
259 | var children = currentParent.children; //取出children
260 | // text => {{message}}
261 | if (text) {
262 | var expression;
263 | if (text !== ' ' && (expression = parseText(text))) {
264 | // 将解析后的text弄进children数组
265 | children.push({
266 | type: 2,
267 | expression: expression,
268 | text: text
269 | });
270 | } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
271 | children.push({
272 | type: 3,
273 | text: text
274 | });
275 | }
276 | }
277 | }
278 | })
279 | return root
280 | }
281 | // 在最后,调用processAttrs对动态绑定的属性(v-,@,:)进行处理,代码如下:
282 | function processAttrs(el) {
283 | // {name:'id',value:'app'}
284 | /*获取元素属性列表*/
285 | var list = el.attrsList;
286 | var i, l, name, rawName, value, modifiers, isProp;
287 | for (i = 0, l = list.length; i < l; i++) {
288 | // 属性名
289 | name = rawName = list[i].name;
290 | // 属性值
291 | value = list[i].value;
292 | addAttr(el, name, JSON.stringify(value)); // 添加了个attrs属性 /*将属性放入ele的attr属性中*/
293 | }
294 | }
295 |
296 | function addAttr(el, name, value) {
297 | (el.attrs || (el.attrs = [])).push({name: name, value: value});
298 | }
299 | function parseText(text, //对Text进行解析
300 | delimiters) {
301 | var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE; // 如果delimiters为false defaultTagRE 为匹配{{xxx}}的正则
302 | if (!tagRE.test(text)) { // /\{\{((?:.|\n)+?)\}\}/g 在这里调用test方法后lasatIndex会变化
303 | return
304 | }
305 | var tokens = [];
306 | var lastIndex = tagRE.lastIndex = 0;
307 | var match, index;
308 |
309 | // 0:"{{message}}"
310 | // 1:"message"
311 | // index:0
312 | // input:"{{message}}"
313 |
314 | // 匹配到中间的文本
315 | while ((match = tagRE.exec(text))) {
316 | index = match.index;
317 | // push text token
318 | // 将{{message}}之前的文本push进去
319 | if (index > lastIndex) {
320 | tokens.push(JSON.stringify(text.slice(lastIndex, index)));
321 | }
322 | // tag token
323 | // 该方法对特殊字符进行处理
324 | var exp = (match[1].trim());
325 | tokens.push(("_s(" + exp + ")"));
326 | lastIndex = index + match[0].length;
327 | }
328 | if (lastIndex < text.length) { // push}}后面的文本
329 | tokens.push(JSON.stringify(text.slice(lastIndex)));
330 | }
331 | return tokens.join('+')
332 | }
333 |
334 | function makeAttrsMap(attrs) {
335 | var map = {};
336 | for (var i = 0, l = attrs.length; i < l; i++) {
337 | map[attrs[i].name] = attrs[i].value;
338 | }
339 | return map
340 | }
341 | Parse.prototype._init = function(template){
342 | return parse(template)
343 | }
344 | return Parse;
345 | })));
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
--------------------------------------------------------------------------------