一文搞懂vue編譯器(DSL)原理
什么是DSL
DSL是領(lǐng)域特定語(yǔ)言的縮寫,與JavaScript這種通用語(yǔ)言編譯器相對(duì),它只針對(duì)某一個(gè)特殊應(yīng)用場(chǎng)景工作
類似中英翻譯,它將源代碼翻譯為目標(biāo)代碼,其轉(zhuǎn)換的標(biāo)準(zhǔn)流程過(guò)程包括:詞法分析、語(yǔ)法分析、語(yǔ)義分析、中間代碼生成、優(yōu)化、目標(biāo)代碼生成等,此外,前述流程并非是嚴(yán)格必須的
vue中的DSL
- 詞法+語(yǔ)法+語(yǔ)義分析
- 生成token流
- 生成模板ast
- 將ast轉(zhuǎn)化為js ast
- 將ast轉(zhuǎn)化為render函數(shù)
const code = `` const tokens = tokenize(code) // 詞法+語(yǔ)法+語(yǔ)義分析,生成token流 const tAst = parse(tokens) // 生成ast const jsAst = transform(tAst) // 將ast轉(zhuǎn)化為jsAst const renderCode = generate(jsAst) // 將jsAst轉(zhuǎn)化為render函數(shù)
實(shí)現(xiàn)思路
- ast結(jié)構(gòu)定義
首先我們要明確要生成的ast結(jié)構(gòu)是什么樣的,比如如下的模板,div和h1怎么表示,開(kāi)標(biāo)簽中的屬性怎么區(qū)分,標(biāo)簽的內(nèi)容放在那里等等
<div> <h1 v-if="show">我愛(ài)前端</h1> <div>
我們約定:ast是一個(gè)樹(shù)形結(jié)構(gòu),每一個(gè)節(jié)點(diǎn)對(duì)應(yīng)一個(gè)html元素,該節(jié)點(diǎn)使用ts定義如下:
interface AstNode{
// 元素類型,是html原生還是vue自定義
type:string;
// 元素名稱,是div還是h1
tag:string;
// 子節(jié)點(diǎn),h1是div的子節(jié)點(diǎn)
children:AstNode[];
// 開(kāi)標(biāo)簽屬性內(nèi)容
props:{
type:string;
name?:string;
exp?:{
...
}
...
}[];
}- 詞法、語(yǔ)法、語(yǔ)義分析
在工程化中,webpack或vite會(huì)幫我們把用戶側(cè)的源代碼拉取過(guò)來(lái),我們使用node的readFileSync來(lái)代替這一行為
const fs = require('node:fs')
const code = fs.readFileSync('./vue.txt','utf-8')有了源代碼,接下來(lái)要考慮的就是如何對(duì)源碼進(jìn)行切分,這需要使用到有限狀態(tài)機(jī),即伴隨著源碼的不斷輸入,解析器能夠自動(dòng)的在不同的狀態(tài)間進(jìn)行遷移的過(guò)程,而有限則意味著狀態(tài)的種類是可枚舉完的
1-模擬源碼不斷輸入
使用while+substring每次刪除一個(gè)字符可以模擬字符的輸入
function parse(code){
while(code.length){
code = code.substring(1)
}
}2-狀態(tài)遷移
我們根據(jù)html標(biāo)簽的書(shū)寫規(guī)則來(lái)定義狀態(tài)遷移的條件,當(dāng)遇到<時(shí),將狀態(tài)從開(kāi)始狀態(tài)標(biāo)記為標(biāo)簽開(kāi)始;伴隨著while循環(huán)的執(zhí)行,首次遇到非空字符時(shí),從標(biāo)簽開(kāi)始狀態(tài)切換為標(biāo)簽名稱狀態(tài);當(dāng)遇到>時(shí),再?gòu)臉?biāo)簽名稱狀態(tài)切換為標(biāo)簽初始狀態(tài)。至此形成一個(gè)閉環(huán),我們?cè)谶@一個(gè)閉環(huán)內(nèi)記錄下的狀態(tài)集合則稱之為一個(gè)token,如圖所示

3-代碼實(shí)現(xiàn)
3.1 定義狀態(tài)機(jī)的狀態(tài)
const State = {
// 初始
initial:1,
// 標(biāo)簽開(kāi)始
start:2,
// 標(biāo)簽名稱
startName:3,
// 標(biāo)簽文本
text:4,
// 標(biāo)簽結(jié)束
end:5,
// 標(biāo)簽結(jié)束名稱
endName:6
}3.2 編寫輔助函數(shù),判斷是否是字符
const isAlpha = function(char){
return /[a-zA-Z1-6]/.test(char)
}3.3 實(shí)現(xiàn)tokenize函數(shù)
通過(guò)while循環(huán)依次取得每一個(gè)字符,當(dāng)遇到規(guī)則字符(如<或/或>)時(shí),根據(jù)當(dāng)前所處的狀態(tài)進(jìn)行狀態(tài)遷移,當(dāng)遷移回初始狀態(tài)時(shí)記錄一個(gè)token
function tokenize(code){
let currentState = State.initial
const tokens = []
const chars = []
while(code.length){
const act = code[0]
switch(currentState){
case State.initial:
if(act === '<'){
currentState = State.start
}else if(isAlpha(act)){
currentState = State.text
chars.push(act)
}
break
case State.start:
if(isAlpha(act)){
currentState = State.startName
chars.push(act)
}else if(act === '/'){
currentState = State.end
}
break
case State.startName:
if(isAlpha(act)){
chars.push(act)
}else if(act === '>'){
// 切到初始狀態(tài),形式閉環(huán),記錄token
currentState = State.initial
tokens.push({
type:'tag',
name:chars.join('')
})
chars.length = 0
}
break
case State.text:
/**
* 1.<div></div> act = i
* 2.<div>我愛(ài)前端</div> act = 愛(ài)
*/
if(isAlpha(act)){
chars.push(act)
}else if(act === '<'){
currentState = State.start
tokens.push({
type:'text',
content:chars.join('')
})
chars.length = 0
}
break
case State.end:
// 當(dāng)遇到/才會(huì)切換到結(jié)束標(biāo)簽狀態(tài)
if(isAlpha(act)){
currentState = State.endName
chars.push(act)
}
break
case State.endName:
if(isAlpha(act)){
chars.push(act)
}else if(act === '>'){
currentState = State.initial
tokens.push({
type:'tagEnd',
name:chars.join('')
})
chars.length = 0
}
break
}
code = code.substring(1)
}
return tokens
}運(yùn)行結(jié)果如下:

- 生成tAst
由于vue是在js下實(shí)現(xiàn)的編譯器,并不會(huì)創(chuàng)造新的運(yùn)算符號(hào),所以并不需要進(jìn)行遞歸下降才能實(shí)現(xiàn)ast,我們只需要對(duì)上一步生成的tokens進(jìn)行遍歷掃描即可
1-如何掃描
觀察我們生成的tokens,最先開(kāi)始的div標(biāo)簽,最后結(jié)束,同時(shí),后進(jìn)入的h1標(biāo)簽是div標(biāo)簽的子節(jié)點(diǎn)
因此,我們需要初始化一個(gè)棧,當(dāng)遇到type為tag的標(biāo)簽時(shí)向棧頂壓入一個(gè)ast節(jié)點(diǎn),并將其作為前一個(gè)棧頂節(jié)點(diǎn)的子節(jié)點(diǎn),當(dāng)遇到type為tagEnd時(shí)則從棧頂彈出,標(biāo)識(shí)一次標(biāo)簽的完整匹配
2-代碼實(shí)現(xiàn)
2.1 初始化虛擬根節(jié)點(diǎn)
由于樹(shù)形結(jié)構(gòu)必存在根節(jié)點(diǎn),而html則是多根的,因此我們?cè)诖a里初始化一個(gè)根
const root = {
type:'Root',
children:[]
}
復(fù)制代碼2.2 初始化棧
將虛擬根作為默認(rèn)的棧頂,這樣在掃描實(shí)際的tokens時(shí),就能默認(rèn)作為其子節(jié)點(diǎn)了
const stack = [root] 復(fù)制代碼
2.3 創(chuàng)建節(jié)點(diǎn)
class Node{
constructor(type,tag){
this.type = type
this.tag = tag
this.children = []
}
setContent(content){
this.content = content
}
}
復(fù)制代碼2.4 掃描
依次從tokens中取出,并判斷其type類型,如果是tag則作為子節(jié)點(diǎn)向原棧頂追加,如果是tagEnd則從棧頂彈出
while(tokens.length){
const p = stack[stack.length - 1]
const act = tokens.shift()
switch(act.type){
case 'tag':{
const e = new Node('Element',act.name)
p.children.push(e)
stack.push(e)
break
}
case 'text':{
const e = new Node('text')
e.setContent(act.content)
p.children.push(e)
break
}
case 'tagEnd':{
stack.pop()
}
}
}2.5 生成的ast如下

- transform
現(xiàn)在,我們已經(jīng)完成了模板的ast化,接下來(lái)就是考慮如何將這個(gè)模板的ast轉(zhuǎn)化為js ast,這一過(guò)程我們稱之為transform,它定義了對(duì)ast節(jié)點(diǎn)操作的一系列方法
1-節(jié)點(diǎn)的訪問(wèn)
節(jié)點(diǎn)操作的前提一定是先拿到這個(gè)節(jié)點(diǎn),因此我們需要能夠遍歷到樹(shù)中的每一個(gè)節(jié)點(diǎn)
1.1 深度優(yōu)先遍歷
function transform(tAst){
const children = tAst.children
if(Array.isArray(children)){
for(let i=0;i<children.length;i++){
transform(children[i])
}
}
}1.2 定義訪問(wèn)操作
如果將訪問(wèn)操作的代碼內(nèi)置到transform當(dāng)中,則該函數(shù)一定會(huì)又臭又長(zhǎng),且不易后續(xù)擴(kuò)展,因此我們需要將該操作進(jìn)行提取,ast的訪問(wèn)應(yīng)該算是訪問(wèn)者模式的典型應(yīng)用,不過(guò)為了保持和vue一致,咱們也采用函數(shù)回調(diào)的方式來(lái)實(shí)現(xiàn)
function transform(tAst,ctx){
const act = tAst
const transforms = ctx.nodeTransforms
for(let i=0;i<transforms;i++){
if(typeof transforms[i] === 'function'){
transforms[i](act,ctx)
}
}
......
}2-擴(kuò)展ctx
在進(jìn)行節(jié)點(diǎn)操作之前,我們還需要?jiǎng)討B(tài)的給ctx掛載一些狀態(tài)信息,用以標(biāo)記當(dāng)前transform的運(yùn)行狀態(tài),比如當(dāng)前運(yùn)行的是哪一顆節(jié)點(diǎn)樹(shù)、當(dāng)前的節(jié)點(diǎn)樹(shù)的父節(jié)點(diǎn)是誰(shuí)、當(dāng)前節(jié)點(diǎn)的兄弟節(jié)點(diǎn)是誰(shuí)以及當(dāng)前節(jié)點(diǎn)樹(shù)是父節(jié)點(diǎn)的第幾個(gè)子節(jié)點(diǎn)
function transform(tAst,ctx){
// 當(dāng)前的節(jié)點(diǎn)樹(shù)
ctx.act = tAst
const transforms = ctx.nodeTransforms
for(let i=0;i<transforms.length;i++){
if(typeof transforms[i] === 'function'){
transforms[i](ctx.act,ctx)
}
}
const children = ctx.act.children
if(Array.isArray(children)){
// 當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)
ctx.parent = ctx.act
for(let i=0;i<children.length;i++){
// 當(dāng)前節(jié)點(diǎn)樹(shù)是父節(jié)點(diǎn)的第幾個(gè)子節(jié)點(diǎn)
ctx.index = i
// 當(dāng)前節(jié)點(diǎn)的兄弟節(jié)點(diǎn)
ctx.siblings = [arr[i-1],arr[i+1]].map(v=>v||null)
transform(children[i],ctx)
}
}
}3-節(jié)點(diǎn)替換
至此,我們的transform的主框架就搭好了,要實(shí)現(xiàn)節(jié)點(diǎn)替換就只需要在nodeTransforms中增加處理函數(shù)即可,比如我們將h1標(biāo)簽替換為p標(biāo)簽
function _replaceNode(newNode){
this.act = newNode
this.parent.children[this.index] = newNode
}
function transformElement(node,ctx){
if(node.type === 'Element'){
switch(node.tag){
case 'h1':
_replaceNode.call(ctx,{
type:'Element',
tag:'p',
children:node.children
})
break
}
}
}4-等待子節(jié)點(diǎn)處理完畢
目前的實(shí)現(xiàn)中,在對(duì)當(dāng)前節(jié)點(diǎn)進(jìn)行處理時(shí),其子節(jié)點(diǎn)一定還未被處理,但在實(shí)際需求中,往往需要等子節(jié)點(diǎn)處理完畢后再根據(jù)其執(zhí)行結(jié)果決定如何處理當(dāng)前節(jié)點(diǎn),因此需要對(duì)transform進(jìn)行改進(jìn)
我們?yōu)閚odeTransforms設(shè)計(jì)一個(gè)返回值,該值是一個(gè)函數(shù),當(dāng)正向訪問(wèn)結(jié)束后,使用該返回函數(shù)做反向遍歷即可
function transform(tAst,ctx){
// 退出回調(diào)列表
const cbs = []
...
const cb = transforms[i](ctx.act,ctx)
if(typeof cb === 'function'){
cbs.push(cb)
}
...
// 退出
let i = cbs.length
while(i--){
cbs[i]()
}
}5-生成js ast
由于我們最終的產(chǎn)物是一個(gè)render函數(shù),因此需要將模板ast轉(zhuǎn)換為js ast,以前文的模板為例
<div> <h1>123</h1> </div>
其對(duì)應(yīng)的render函數(shù)如下
function render(){
return h('div',h('h1','123'))
}5.1 ast節(jié)點(diǎn)類型
在模板的ast節(jié)點(diǎn)定義時(shí),我們把一個(gè)元素節(jié)點(diǎn)視為一個(gè)ast節(jié)點(diǎn),而在JavaScript中,則為一條js語(yǔ)句等同于一個(gè)ast節(jié)點(diǎn)
觀察render函數(shù)的js代碼,不難發(fā)現(xiàn),其由函數(shù)定義、函數(shù)參數(shù)和函數(shù)返回值三部分構(gòu)成,同樣的,我們使用type來(lái)標(biāo)記其類型
另外,我們的目標(biāo)代碼是明確的,并非所有的js語(yǔ)句,因此,我們可以定義任何的type名稱來(lái)做專屬標(biāo)識(shí),比如我就想使用Function來(lái)表示render函數(shù),使用ReturenCb來(lái)表示h函數(shù)......
本文,使用FunctionDecl+id.name標(biāo)識(shí)render函數(shù);params標(biāo)識(shí)render函數(shù)的參數(shù);body標(biāo)識(shí)render函數(shù)的函數(shù)體,由于函數(shù)體內(nèi)又可能存在多個(gè)js語(yǔ)句,因此它被設(shè)計(jì)為一個(gè)數(shù)組,最后使用ReturnStatement標(biāo)識(shí)return語(yǔ)句,其返回的是一個(gè)h函數(shù),而參數(shù)使用arguments標(biāo)記
{
type:'FunctionDecl',
id:{
type:"Identifier",
name:"render"
},
params:[],
body:[
{
type:"ReturnStatement",
return:{
type:"CallExpression",
callee:{
type:"Identifier",
name:"h"
},
arguments:[
{
type:"StringLiteral",
value:"div"
},
{
type:"ArrayExpression",
elements:[
//CallExpression類型,
//CallExpression類型,
]
}
]
}
}
]
}5.2 定義類型生成器
編寫一個(gè)newType函數(shù)用于統(tǒng)一處理各種節(jié)點(diǎn)類型的生成
function _newType(type,value,arguments){
const o = {
type,
}
switch(type){
case 'StringLiteral':
o.value = value
break
case 'Identifier':
o.name = value
break
case 'ArrayExpression':
o.elements = value
break
case 'CallExpression':
o.callee = _newType('Identifier',value)
o.arguments = arguments
break
}
return o
}5.3 為tAst添加jsCode屬性收集當(dāng)前節(jié)點(diǎn)的轉(zhuǎn)換結(jié)果
5.4 重新實(shí)現(xiàn)transformElement函數(shù)
對(duì)當(dāng)前語(yǔ)句的處理必須等到子節(jié)點(diǎn)轉(zhuǎn)換完畢,因?yàn)橹挥写藭r(shí)jscode才是可用的
function transformElement(node,ctx){
return ()=>{
if(node.type === 'Element'){
}
}
}從5.1的節(jié)點(diǎn)類型定義可以知道,每一個(gè)節(jié)點(diǎn)本質(zhì)上都是一個(gè)h函數(shù)
const callee = _newType('CallExpression','h',[
_newType('StringLiteral',node.tag)
// 參數(shù)二取決于子節(jié)點(diǎn)的數(shù)量,需要?jiǎng)討B(tài)生成
])生成參數(shù)二
node.children.length === 1
? callee.arguments.push(node.children[0].jsCode)
: callee.arguments.push(node.children.map(v=>v.jsCode))最后將當(dāng)前節(jié)點(diǎn)的轉(zhuǎn)換結(jié)果掛載到j(luò)sCode
node.jsCode = callee
5.5 新增transformRoot函數(shù)
至此,我們已經(jīng)完成了對(duì)實(shí)際模板節(jié)點(diǎn)的轉(zhuǎn)化,即
將
<div> <h1>123</h1> </div>
轉(zhuǎn)為了
h('div',[
h('h1','123')
])因此我們還需要處理生成render函數(shù),而這正好與我們?cè)谝婚_(kāi)始生成的虛擬根節(jié)點(diǎn)相對(duì)應(yīng)
function transformRoot(node){
return ()=>{
if(node.type === 'Root'){
node.jsCode = {
type:"FunctionDecl",
id:_newType("Identifier","render"),
params:[],
body:[
{
type:"ReturnStatement",
return:node.children[0].jsCode
}
]
}
}
}
}6-轉(zhuǎn)換結(jié)果如下

- 生成目標(biāo)代碼
我們?cè)谇耙徊糠忠呀?jīng)為根節(jié)點(diǎn)添加了jsCode屬性,該屬性就是tAst所對(duì)應(yīng)的jsAst,因此我們只需要找到每一個(gè)節(jié)點(diǎn)并將他們轉(zhuǎn)化成字符串進(jìn)行拼接就可以了
1-定義上下文
我們說(shuō)了,代碼生成本質(zhì)上是在做字符串的拼接工作,為此我們將拼接時(shí)出現(xiàn)頻率較大的函數(shù)定義在上下文中以方便復(fù)用,其中的code就是我們最終生成的代碼的容器,而newLine則更多是為了生成代碼的可讀性
const ctx = {
code: "",
append(code) {
this.code += code;
},
newLine(indent = 1) {
this.code += "\n" + " ".repeat(indent * 2);
},
};2-定義類型生成函數(shù)
2.1 首先我們將每一種js節(jié)點(diǎn)類型所對(duì)應(yīng)的生成函數(shù)放到一個(gè)統(tǒng)一的genMap中
const genMap = new Map([
['FunctionDecl',genFunctionDecl],
['ReturnStatement',genReturnStatement],
['StringLiteral',genStringLiteral],
['CallExpression',genCallExpression],
['ArrayExpression',genArrayExpression]
])2.2 對(duì)參數(shù)的遍歷生成單獨(dú)做一個(gè)genParams
function genParams(nodes,ctx) {
nodes.forEach((v,i)=>{
genMap.get(v.type)(v,ctx)
if(i<nodes.length-1){
ctx.append(',')
}
})
}2.3 分別實(shí)現(xiàn)
分別對(duì)函數(shù)名稱、函數(shù)參數(shù)、函數(shù)體做生成,他們都在節(jié)點(diǎn)中有著一一對(duì)應(yīng)的節(jié)點(diǎn)屬性
2.3.1 genFunctionDecl
代碼實(shí)現(xiàn)
function genFunctionDecl(node, ctx) {
ctx.append(`function ${node.id.name}(`);
genParams(node.params, ctx);
ctx.append("){");
ctx.newLine();
node.body.forEach((v) => genMap.get(v.type)(v, ctx));
ctx.newLine(0)
ctx.append('}')
}生成結(jié)果

2.3.2 genReturnStatement
該函數(shù)就是為code拼接return字符,至于真正的函數(shù)體,是由genCallExpression完成的
代碼實(shí)現(xiàn)
function genReturnStatement(node,ctx) {
ctx.append('return ')
genMap.get(node.return.type)(node.return,ctx)
}生成結(jié)果

2.3.3 genCallExpression
代碼實(shí)現(xiàn)
function genCallExpression(node,ctx) {
ctx.append(`${node.callee.name}(`)
genParams(node.arguments,ctx)
ctx.append(')')
}生成結(jié)果

2.3.4 genStringLiteral
代碼實(shí)現(xiàn)
function genStringLiteral(node,ctx) {
ctx.append(`'${node.value}'`)
}生成結(jié)果

2.3.5 genArrayExpression
目前在我們的示例中是不存在該類型的,因此我們將模板源碼做下調(diào)整
<div> <h1>123</h1> <h2>456</h2> </div>
代碼實(shí)現(xiàn)
function genArrayExpression(node,ctx) {
ctx.append('[')
genParams(node.elements,ctx)
ctx.append(']')
}生成結(jié)果

3-最終的完整生成結(jié)果

到此這篇關(guān)于一文搞懂vue編譯器(DSL)原理的文章就介紹到這了,更多相關(guān)vue編譯器DSL內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于vue循環(huán)列表時(shí)點(diǎn)擊跳轉(zhuǎn)頁(yè)面的方法
今天小編就為大家分享一篇基于vue循環(huán)列表時(shí)點(diǎn)擊跳轉(zhuǎn)頁(yè)面的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
vue2實(shí)現(xiàn)搜索結(jié)果中的搜索關(guān)鍵字高亮的代碼
這篇文章主要介紹了vue2實(shí)現(xiàn)搜索結(jié)果中的搜索關(guān)鍵字高亮的代碼,需要的朋友可以參考下2018-08-08
Vue實(shí)現(xiàn)簡(jiǎn)單購(gòu)物車小案例
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)簡(jiǎn)單購(gòu)物車小案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10
axios請(qǐng)求的一些常見(jiàn)操作實(shí)戰(zhàn)指南
axios是一個(gè)輕量的HTTP客戶端,它基于XMLHttpRequest服務(wù)來(lái)執(zhí)行 HTTP請(qǐng)求,支持豐富的配置,支持Promise,支持瀏覽器端和 Node.js 端,下面這篇文章主要給大家介紹了關(guān)于axios請(qǐng)求的一些常見(jiàn)操作,需要的朋友可以參考下2022-09-09
Vue監(jiān)聽(tīng)使用方法和過(guò)濾器實(shí)現(xiàn)
這篇文章主要介紹了Vue監(jiān)聽(tīng)使用方法和過(guò)濾器實(shí)現(xiàn),過(guò)濾器為頁(yè)面中數(shù)據(jù)進(jìn)行強(qiáng)化,具有局部過(guò)濾器和全局過(guò)濾器2022-06-06

