《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 局部:
- String.prototype.match()
首先用.match()
办法,提取字符串中的关键词,例如标签名div
,Mustache标签对应的变量msg
等。
用 1 行代码就能说明确:
'<div>{{msg}}</div>'.match(/\{\{((?:.|\r?\n)+?)\}\}/)
// ["{{msg}}", "msg"]
这个示例用/\{\{((?:.|\r?\n)+?)\}\}/
正则表达式,提取了字符串'<div>{{msg}}</div>'
中的Mustache标签
。
临时不必了解该正则的含意,会用即可。
如果想要了解正则,能够试一试正则在线调试工具:Vue.js 开始标签正则,可能可视化的查看上述正则表达式
取得了"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
。
html = `<div` // 仅遍历一遍,提取开始标签
const advance = (n) => {
html = html.substring(n)
}
while (html) {
match = html.match(/^<([a-zA-Z_]*)/)
// ["<div", "div"]
if (match) {
advance(match[0].length)
// html = '' 跳出循环
}
}
了解了这2局部逻辑,就了解了字符串模板template
编译为 render()
函数的原理,就是这么简略!
具体步骤
0. 基于class
语法封装
咱们用 JS 的class
语法对代码进行简略的封装、模块化,具体来说就是申明 3 个类:
// 将 Vue 实例的字符串模板 template 编译为 AST
class HTMLParser {}
// 基于 AST 生成渲染函数;
class VueCompiler {
HTMLParser = new HTMLParser()
}
// 基于渲染函数生成虚构节点和实在 DOM
class Vue {
compiler = new VueCompiler()
}
问:为什么要生成
AST
?间接把template
字符串模板编译为实在 DOM 有什么问题?答:没有问题,技术上也能够实现,
Vue.js 以及泛滥编译器都采纳 AST 做为编译的中间状态,集体了解是为了编译过程中做「转化」(Transform),
例如v-if
属性转化为 JS 的if-else
判断,有了 AST 做为中间状态,有助于更便捷的实现v-if
到if-else
1. 开始遍历template
字符串模板
基于咱们上述提到的while()
和html = html.substring(n)
,
咱们能够实现一套一边解析模板字符串、一边删除已解析局部,直到全副解析实现的逻辑。
很简略,只有几行代码,
咱们为class HTMLParser
减少一个parseHTML(html, options)
办法:
parseHTML(html, options) {
const advance = (n) => {
html = html.substring(n)
}
while(html) {
const startTag = parseStartTag() // TODO 下一步实现 parseStartTag
if (startTag) {
advance(startTag[0].length)
continue
}
}
}
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)
删除。
const parseStartTag = () => {
let start = html.match(this.startTagOpen);
// ['<div', 'div']
if (start) {
const match = {
tagName: start[1],
attrs: [],
}
advance(start[0].length)
}
}
留神,startTagOpen
只匹配了开始标签的局部'<div'
,还须要一次正则匹配,找到开始标签的完结符号>
。
找到完结符号后,也要删除对应已遍历的局部。
const end = html.match(this.startTagClose)
debugger
if (end) {
advance(end[0].length)
}
return match
两局部组合后,就能残缺遍历、解析出模板字符串中的<div>
开始标签了。
3. 解析文本内容{{msg}}
下一步咱们把模板字符串中的文本内容{{msg}}
提取进去,依然是字符串遍历 && 正则匹配
持续补充 while 循环:
while (html) {
debugger
// 程序对逻辑有影响,startTag 要先于 text,endTag 要先于 startTag
const startTag = parseStartTag()
if (startTag) {
handleStartTag(startTag)
continue
}
let text
let textEnd = html.indexOf('<')
if (textEnd >= 0) {
text = html.substring(0, textEnd)
}
if (text) {
advance(text.length)
}
}
因为删除了已解析局部、并且各局部有解析程序,所以咱们只有检测下一个<
标签的地位即可取得文本内容在 html 中的完结下标:html.indexOf('<')
。
之后就能取得残缺的文本内容{{msg}}
:text = html.substring(0, textEnd)
。
最初,别忘了,删除曾经遍历的文本内容:advance(text.length)
4. 解析闭合标签</div>
到这一步,html 字符串曾经只剩下'</div>'
了,咱们持续用遍历&&正则解析:
while (html) {
debugger
// 程序对逻辑有影响,startTag 要先于 text,endTag 要先于 startTag
let endTagMatch = html.match(this.endTag)
if (endTagMatch) {
advance(endTagMatch[0].length)
continue
}
}
咱们临时不须要从闭合标签中提取信息,所以只须要遍历、匹配后,删除它即可。
解析实现总结
到目前为止,咱们曾经根本实现了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)
办法。
class VueCompiler {
HTMLParser = new HTMLParser()
constructor() {}
parse(template) {}
}
AST 是什么?
先不必去了解艰涩的概念,在 Vue.js 的实现中,AST 就是一般的 JS object,记录了标签名、父元素、子元素等属性:
createASTElement (tag, parent) {
return { type: 1, tag, parent, children: [] }
}
咱们把createASTElement
办法也增加到class VueCompiler
中。
并减少parse
办法中this.HTMLParser.parseHTML()
的调用
parse(template) {
const _this = this
let root
let currentParent
this.HTMLParser.parseHTML(template, {
start(tag) {},
chars (text) {},
})
debugger
return root
}
start(tag) {}
就是咱们提取开始标签对应 AST 节点的回调,
其承受一个参数tag
,调用_this.createASTElement(tag, currentParent)
来生成 AST 节点。
start(tag) {
let element = _this.createASTElement(tag, currentParent)
if (!root) {
root = element
}
currentParent = element
},
调用start(tag)
的地位在class HTMLParser
中的parseHTML(html, options)
办法:
const handleStartTag = (match) => {
if (options.start) {
options.start(match.tagName)
}
}
while(html) {
const startTag = parseStartTag()
if (startTag) {
handleStartTag(startTag)
continue
}
}
当咱们通过parseStartTag()
获取了{tagName: 'div'}
,就传给options.start(match.tagName)
,从而生成 AST 的根节点:
// root
'{"type":1,"tag":"div","children":[]}'
咱们把根节点保留到root
变量中,用于最终返回整个AST的援用。
6. 为 AST 减少子节点
除了根节点,咱们还须要持续为 AST 这棵树增加子节点:文本内容节点
依然是用回调的模式(options.char(text)
),提取出文本内容节点所需的信息,
欠缺VueCompiler.parse()
中的chars(text)
办法
chars(text) {
debugger
const res = parseText(text)
const child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
if (currentParent) {
currentParent.children.push(child)
}
},
在parseHTML(html, options)
的循环中增加options.chars(text)
调用:
while (html) {
// ...省略其余标签的解析
let text
let textEnd = html.indexOf('<')
// ...
if (options.chars && text) {
options.chars(text)
}
}
解析文本内容的Mustache标签
语法
options.chars(text)
接管的text值为字符串'{{msg}}'
,咱们还须要从中剔除{{}}
,拿到msg
字符串。
依然是用相熟的正则匹配:
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/
function parseText(text) {
let tokens = []
let rawTokens = []
const match = defaultTagRE.exec(text)
const exp = match[1]
tokens.push(("_s(" + exp + ")"))
rawTokens.push({ '@binding': exp })
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
后果将是:
{
expression: "_s(msg)",
tokens: {
@binding: "msg"
}
}
临时不用理解expression, tokens
及其内容的具体含意,后续到运行时阶段咱们会再具体介绍。
7. 遍历template
字符串实现,返回 AST
残缺示例:DEMO -《8分钟学会 Vue.js 原理》:一、template 字符串编译为形象语法树 AST – JSBin
通过以上步骤,咱们将 template 字符串解析后失去这样一个对象:
// root ===
{
"type": 1,
"tag": "div",
"children": [
{
"type": 2,
"expression": "_s(msg)",
"tokens": [
{
"@binding": "msg"
}
],
"text": "{{msg}}"
}
]
}
这就是 Vue.js 的 AST,实现就是这么简略,示例中的代码都间接来自 Vue.js 的源码(compiler 局部)
后续咱们将基于 AST 生成render()
函数,并最终渲染出实在 DOM。
<hr/>
《8分钟学会 Vue.js 原理》系列,共计5局部:
- 一、template 字符串编译为形象语法树 AST
- 二、AST 编译 render() 实现原理
- 三、执行渲染函数 render() 生成虚构节点 vnode
- 四、虚构节点 vnode 生成实在DOM
- 五、数据驱动 DOM 更新 – Watcher Observer 和 Dep
正在热气腾腾更新中,欢送交换~ 欢送催更~
发表回复