You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
parseHTML(html,options){constadvance=(n)=>{html=html.substring(n)}while(html){conststartTag=parseStartTag()// TODO 下一步实现 parseStartTagif(startTag){advance(startTag[0].length)continue}}}
《8分钟学会 Vue.js 原理》:一、template 字符串编译为抽象语法树 AST
Vue.js 并没有什么神秘的魔法,模板渲染、虚拟
DOM diff
,也都是一行行代码基于 API 实现的。本文将用几分钟时间,分章节讲清楚 Vue.js 2.0 的
<template>
渲染为 HTML DOM 的原理。有任何疑问,欢迎通过评论联系我~~
本节目标
将 Vue.js 的字符串模板
template
编译为抽象语法树 AST;完整示例:DEMO -《8分钟学会 Vue.js 原理》:一、template 字符串编译为抽象语法树 AST - JSBin
极其简单的核心逻辑
实现「字符串模板
<template>
编译为render()
函数」的核心逻辑极其简单,只有 2 部分:首先用
.match()
方法,提取字符串中的关键词,例如标签名div
,Mustache标签对应的变量msg
等。用 1 行代码就能说明白:
这个示例用
/\{\{((?:.|\r?\n)+?)\}\}/
正则表达式,提取了字符串'<div>{{msg}}</div>'
中的Mustache标签
。暂时不用理解该正则的含义,会用即可。
获得了
"msg"
标签对应变量的名称,我们就能在后续拼接出渲染DOM所需要的_vm.msg
。即从我们声明的实例
new Vue({data() {return {msg: 'hi'}}})
中提取出msg: 'hi'
,渲染为 DOM 节点。其次,因为
<template>
本质上是一段有大量HTML标签的字符串,通常内容较长,为了不遗漏地获取到其中的所有标签、属性,我们需要遍历。实现方式也很简单,用
while(html)
循环,不断的html.match()
提取模板中的信息。(html变量即template字符串)每提取一段,再用
html = html.substring(n)
删除掉n
个已经遍历过的字符。直到
html
字符串为空,表示我们已经遍历、提取了全部的template
。理解了这2部分逻辑,就理解了字符串模板
template
编译为render()
函数的原理,就是这么简单!具体步骤
0. 基于
class
语法封装我们用 JS 的
class
语法对代码进行简单的封装、模块化,具体来说就是声明 3 个类:1. 开始遍历
template
字符串模板基于我们上述提到的
while()
和html = html.substring(n)
,我们可以实现一套一边解析模板字符串、一边删除已解析部分,直到全部解析完成的逻辑。
很简单,只有几行代码,
我们为
class HTMLParser
增加一个parseHTML(html, options)
方法:html
参数即初始化 Vue 实例中的template: '<div>{{msg}}</div>',
属性,在遍历 html 过程中,我们每解析出一个关键词,就调用
advance() { html.substring(n) }
,删去这部分对应的字符串,示例中我们调用
parseStartTag()
(下一步实现)解析出开始标签<div>
对应的字符串后,就从 html 删除了<div>
这5个字符。2. 解析开始标签
<div>
接下来让我们实现
parseStartTag()
,解析出字符串中的开始标签<div>
。也非常简单,用
html.match(regularExp)
即可,我们可以从Vue.js 源码中的 html-parser.js找到对应的正则表达式startTagOpen
。源码的正则中非常复杂,因为要兼容生产环境的各种标签,我们暂时不考虑,精简后就是:
/^<([a-zA-Z_]*)/
。用这个正则调用
html.match()
,即可得到['<div', 'div']
这个数组。提取出需要的信息后,就把已经遍历的字符调用
advance(start[0].length)
删除。注意,
startTagOpen
只匹配了开始标签的部分'<div'
,还需要一次正则匹配,找到开始标签的结束符号>
。找到结束符号后,也要删除对应已遍历的部分。
两部分组合后,就能完整遍历、解析出模板字符串中的
<div>
开始标签了。3. 解析文本内容
{{msg}}
下一步我们把模板字符串中的文本内容
{{msg}}
提取出来,仍然是字符串遍历 && 正则匹配
继续补充 while 循环:
因为删除了已解析部分、并且各部分有解析顺序,所以我们只要检测下一个
<
标签的位置即可获得文本内容在 html 中的结束下标:html.indexOf('<')
。之后就能获得完整的文本内容
{{msg}}
:text = html.substring(0, textEnd)
。最后,别忘了,删除已经遍历的文本内容:
advance(text.length)
4. 解析闭合标签
</div>
到这一步,html 字符串已经只剩下
'</div>'
了,我们继续用遍历&&正则解析:我们暂时不需要从闭合标签中提取信息,所以只需要遍历、匹配后,删除它即可。
解析完成总结
到目前为止,我们已经基本实现了
class HTMLParser {}
,多次用正则解析提取出了 template 字符串中的 3 部分信息:'<div', '>'
'{{msg}}'
'</div>'
这部分的完整代码,可以访问DEMO -《8分钟学会 Vue.js 原理》:一、template 字符串编译为抽象语法树 AST - JSBin查看。
但为了获得 AST,我们还需要基于这些信息,做一些简单的拼接。
5. 初始化抽象语法树 AST 的根节点
我们继续参考Vue.js 源码中拼接 AST 的实现
完善
class VueCompiler
,添加HTMLParser = new HTMLParser()
实例,以及parse(template)
方法。AST 是什么?
先不用去理解晦涩的概念,在 Vue.js 的实现中,AST 就是普通的 JS object,记录了标签名、父元素、子元素等属性:
我们把
createASTElement
方法也添加到class VueCompiler
中。并增加
parse
方法中this.HTMLParser.parseHTML()
的调用start(tag) {}
就是我们提取开始标签对应 AST 节点的回调,其接受一个参数
tag
,调用_this.createASTElement(tag, currentParent)
来生成 AST 节点。调用
start(tag)
的位置在class HTMLParser
中的parseHTML(html, options)
方法:当我们通过
parseStartTag()
获取了{tagName: 'div'}
,就传给options.start(match.tagName)
,从而生成 AST 的根节点:我们把根节点保存到
root
变量中,用于最终返回整个AST的引用。6. 为 AST 增加子节点
除了根节点,我们还需要继续为 AST 这棵树添加子节点:文本内容节点
仍然是用回调的形式(
options.char(text)
),提取出文本内容节点所需的信息,完善
VueCompiler.parse()
中的chars(text)
方法在
parseHTML(html, options)
的循环中添加options.chars(text)
调用:解析文本内容的
Mustache标签
语法options.chars(text)
接收的text值为字符串'{{msg}}'
,我们还需要从中剔除{{}}
,拿到msg
字符串。仍然是用熟悉的正则匹配:
结果将是:
暂时不必了解
expression, tokens
及其内容的具体含义,后续到运行时阶段我们会再详细介绍。7. 遍历
template
字符串完成,返回 AST完整示例:DEMO -《8分钟学会 Vue.js 原理》:一、template 字符串编译为抽象语法树 AST - JSBin
经过以上步骤,我们将 template 字符串解析后得到这样一个对象:
这就是 Vue.js 的 AST,实现就是这么简单,示例中的代码都直接来自 Vue.js 的源码(compiler 部分)
后续我们将基于 AST 生成
render()
函数,并最终渲染出真实 DOM。《8分钟学会 Vue.js 原理》系列,共计5部分:
正在热火朝天更新中,欢迎交流~ 欢迎催更~
The text was updated successfully, but these errors were encountered: