拆解Vue2核心模块实现(2)
前言
在上篇 拆解Vue2核心模块实现(1)中实现了基本的数据双向绑定功能以及 v-model
指令,这篇则增加对<template>
的支持。
<template>
模板解析
要实现 <template>
模板解析就需要对应的 parser
vue2 的 parser是基于正则处理的,parser
得到的 AST
会生成对应的虚拟 DOM
,最终根据DOM diff
进行对应的 patch
不过Vue3
则是改用 有限状态机
来处理 AST
,选择有限状态机
的好处是易于拓展和维护
这里也选择基于有限状态机
实现对应的 parser
基于有限状态机实现一个简单的 parser
为了实现方便,这里约定输入的<template>
内容有如下要求:
- 标签仅包含字母标签和h1 - h6,其他如注释标签不处理
- 标签属性无复杂属性
- 包含文本节点
- Vue组件(约定标签首字母大写开头的为Vue组件)
- 默认用户输入是规范的,也就是说不会做容错处理
状态设计
简单说就是遍历模板内容(每次只读取一个子节内容),根据设定好的状态去处理每个状态需要处理的流程,同时每个状态之间根据模板内容有其对应流转关系
按照标签解析主要有以下3个大的状态,每个大的状态又有对应的状态流程,通过 Token 状态流转
标签名
,无论是开标签还是闭标签都是用该流程,只是 Token 的类型不同
<,匹配到标签开始
/^[(a-zA-Z)|(1-6)]$/
parseLiteral
openTag
tagName
标签属性
,解析完 tagName 后就开始 Attribute 的处理,当然也可能没有Attribute,所以需要增加 beforeAttributeName 状态,然后是 AttributeValue 由于 AttributeValue 可能用双引号
也可能用单引号
,所以分别有 doubleQuotedAttributeValue 和 singQuotedAttributeValue 状态
>,<div>aa</div>
匹配到tab、换行、禁止符、空格
有内容,如<div aa='a'>
=
解析到双引号
解析到单引号
>,<div>aa</div>
自等属性
/,自闭合,<div /> <div aa='a'/>
/,自闭合,如<div /> <div aa='a'/>
>
beforeAttributeName
parseLiteral
tagName
attributeName
afterAttributeName
beforeAttributeValue
doubleQuotedAttributeValue
afterQuotedAttributeValue
singQuotedAttributeValue
UnquotedAttributeValue
selfClosingStartTag
标签内容
,遇到这个,说明前面是开标签,有对应的 Token 流程且流程未走完,所以需要把该 Token 流程入栈流程,待处理完内容再继续处理,这里可以当作入口
<,匹配到开始标签
匹配到文本内容
收录文本内容
parseLiteral
openTag
开始标签名解析流程
currentTextNode
可以看到整个状态流转从 parseLiteral
开始解析,围绕 标签名
、标签属性
、标签内容(分为标签节点、文本节点)
的状态流转的
初始化
let AST = [];
let state;
let currentToken;
let currentTextNode;
let currentAttribue;
const EOF = Symbol("EOF");
const Space = "\u0000";
// 正常标签匹配,包括h1 - h6
const tagRex = /^[(a-zA-Z)|(1-6)]$/;
// tab、换行、禁止符、空格
const attrRex = /^[\t\n\f ]$/;
// token流程状态
export const TokenKind = {
startTag: "startTag",
text: "text",
endText: "endText",
endTag: "endTag",
};
function init() {
// 预设一个空标签,相当于有个 <></>
// 这样 template 可以直接并列写内容
AST = [
{
__type: "fragment",
children: [],
attributes: [],
tagName: "fragment",
},
];
state = parseLiteral;
}
export function parser(template) {
init();
for (const c of template) {
state = state(c);
}
// 把预设的根节点返回
return AST[0];
}
parseLiteral
解析字面量,也就是上一个状态处理完了,需要开启下一个状态的token任务
/**
* 逐个解析文本字面量
*/
function parseLiteral(c) {
if (c === "<") {
emitToken({
__type: TokenKind.endText,
});
return openTag;
} else if (c === EOF) {
// 文档结束符
emitToken({
__type: "EOF",
});
return;
} else {
// 内容
emitToken({
__type: TokenKind.text,
content: c,
});
return parseLiteral;
}
}
emitToken
function emitToken(token) {
// 取出当前token的父节点
const top = AST[AST.length - 1];
switch (token.__type) {
case TokenKind.startTag:
// 新标签
let element = {
__type: token.tagName,
children: [],
attributes: [],
tagName: token.tagName,
};
for (const k in token) {
if (!["type", "tagName"].includes(k)) {
element.attributes.push({
name: k,
value: token[k],
});
}
}
// 收入父级的children
top.children.push(element);
// 栈收录,待遇到标签结束时再出栈
AST.push(element);
break;
case TokenKind.text:
// 文本节点
if (!currentTextNode) {
currentTextNode = {
__type: TokenKind.text,
content: "",
};
// 有文本节点说明上一个token是标签,这个是他的文本内容
top.children.push(currentTextNode);
}
currentTextNode.content += token.content;
break;
case TokenKind.endText:
// 结束正在处理文本节点
if (currentTextNode) {
currentTextNode = null;
}
break;
case TokenKind.endTag:
if (token.tagName !== top.__type) {
throw new Error(`标签未闭合:${token.tagName} - ${top.type}`);
} else {
// 当前这个Token流程处理完了,出栈
AST.pop();
}
currentTextNode = null;
break;
default:
break;
}
}
标签名
function openTag(c) {
if (c === "/") {
return closeTag;
} else if (c.match(tagRex)) {
// 正确匹配到内容,初始化Token
currentToken = {
__type: TokenKind.startTag,
tagName: "",
};
// 转入tagName状态
return tagName(c);
}
}
function tagName(c) {
if (c.match(attrRex)) {
//开始属性处理前
return beforeAttributeName;
} else if (c === "/") {
// 自闭合标签
return selfClosingStartTag;
} else if (c.match(tagRex)) {
// 继续tagName
currentToken.tagName += c;
return tagName;
} else if (c === ">") {
emitToken(currentToken);
return parseLiteral;
} else {
return tagName;
}
}
Attribute
function beforeAttributeName(c) {
// tab、换行、禁止符、空格
if (c.match(attrRex)) {
return beforeAttributeName;
} else if (c === "/") {
emitToken(currentToken);
return selfClosingStartTag;
} else if (c === ">") {
emitToken(currentToken);
return parseLiteral;
} else if (c === "=") {
return beforeAttributeName;
} else {
currentAttribue = {
name: "",
value: "",
};
return attributeName(c);
}
}
function attributeName(c) {
if (c.match(attrRex) || c === "/" || c === ">") {
return afterAttributeName(c);
} else if (c === "=") {
return beforeAttributeValue;
} else {
currentAttribue.name += c;
return attributeName;
}
}
function afterAttributeName(c) {
if (c.match(attrRex)) {
currentAttribue.value = currentAttribue.name;
currentToken[currentAttribue.name] = currentAttribue.value;
return beforeAttributeName;
} else if (c === "/") {
return selfClosingStartTag;
} else if (c === ">") {
emitToken(currentToken);
return parseLiteral;
}
}
function beforeAttributeValue(c) {
if (c.match(attrRex) || c === "/" || c === ">" || c === EOF) {
return beforeAttributeValue(c);
} else if (c === '"') {
return doubleQuotedAttributeValue;
} else if (c === "'") {
return singQuotedAttributeValue;
} else {
return UnquotedAttributeValue(c);
}
}
function doubleQuotedAttributeValue(c) {
if (c === '"') {
currentToken[currentAttribue.name] = currentAttribue.value;
return afterQuotedAttributeValue;
} else {
currentAttribue.value += c;
return doubleQuotedAttributeValue;
}
}
function singQuotedAttributeValue(c) {
if (c === "'") {
currentToken[currentAttribue.name] = currentAttribue.value;
return afterQuotedAttributeValue;
} else {
currentAttribue.value += c;
return singQuotedAttributeValue;
}
}
function UnquotedAttributeValue(c) {
if (c.match(attrRex)) {
currentToken[currentAttribue.name] = currentAttribue.value;
return beforeAttributeName;
} else if (c === "/") {
currentToken[currentAttribue.name] = currentAttribue.value;
return selfClosingStartTag;
} else if (c === ">") {
currentToken[currentAttribue.name] = currentAttribue.value;
emitToken(currentToken);
return parseLiteral;
} else {
currentAttribue.value += c;
return UnquotedAttributeValue;
}
}
// AttributeValue处理结束
function afterQuotedAttributeValue(c) {
if (c.match(attrRex)) {
// 可能是下一个属性
return beforeAttributeName;
} else if (c === "/") {
// 自闭合标签
return selfClosingStartTag;
} else if (c === ">") {
// 开闭合标签,当前Token入栈
currentToken[currentAttribue.name] = currentAttribue.value;
emitToken(currentToken);
return parseLiteral;
} else {
currentAttribue.value += c;
return doubleQuotedAttributeValue;
}
}
标签闭合
function selfClosingStartTag(c) {
if (c === ">") {
// 设置token类型为endTag
currentToken.__type = TokenKind.endTag;
//通知收尾
emitToken(currentToken);
// 重新开始字面量匹配
return parseLiteral;
}
}
function closeTag(c) {
// 这主要是处理 <></>,不过本次不涉及
if (c.match(tagRex)) {
currentToken = {
__type: TokenKind.endTag,
tagName: "",
};
return tagName(c);
}
}
parser 整体实现
let AST = [];
let state;
let currentToken;
let currentTextNode;
let currentAttribue;
const EOF = Symbol("EOF");
const Space = "\u0000";
// 正常标签匹配,包括h1 - h6
const tagRex = /^[(a-zA-Z)|(1-6)]$/;
// tab、换行、禁止符、空格
const attrRex = /^[\t\n\f ]$/;
// token流程状态
export const TokenKind = {
startTag: "startTag",
text: "text",
endText: "endText",
endTag: "endTag",
};
/**
* 处理token
*/
function emitToken(token) {
// 取出当前token的父节点
const top = AST[AST.length - 1];
switch (token.__type) {
case TokenKind.startTag:
let element = {
__type: token.tagName,
children: [],
attributes: [],
tagName: token.tagName,
};
for (const k in token) {
if (!["type", "tagName"].includes(k)) {
element.attributes.push({
name: k,
value: token[k],
});
}
}
top.children.push(element);
AST.push(element);
break;
case TokenKind.text:
// 文本节点
if (!currentTextNode) {
currentTextNode = {
__type: TokenKind.text,
content: "",
};
top.children.push(currentTextNode);
}
currentTextNode.content += token.content;
break;
case TokenKind.endText:
// 结束正在处理文本节点
if (currentTextNode) {
currentTextNode = null;
}
break;
case TokenKind.endTag:
if (token.tagName !== top.__type) {
throw new Error(`标签未闭合:${token.tagName} - ${top.type}`);
} else {
// 当前这个Token流程处理完了,出栈
AST.pop();
}
currentTextNode = null;
break;
default:
break;
}
}
/**
* 逐个解析文本字面量
*/
function parseLiteral(c) {
if (c === "<") {
emitToken({
__type: TokenKind.endText,
});
return openTag;
} else if (c === EOF) {
// 文档结束符
emitToken({
__type: "EOF",
});
return;
} else {
// 内容
emitToken({
__type: TokenKind.text,
content: c,
});
return parseLiteral;
}
}
function openTag(c) {
if (c === "/") {
return closeTag;
} else if (c.match(tagRex)) {
// 正确匹配到内容,初始化Token
currentToken = {
__type: TokenKind.startTag,
tagName: "",
};
// 转入tagName状态
return tagName(c);
}
}
function tagName(c) {
if (c.match(attrRex)) {
//开始属性处理前
return beforeAttributeName;
} else if (c === "/") {
// 自闭合标签
return selfClosingStartTag;
} else if (c.match(tagRex)) {
// 继续tagName
currentToken.tagName += c;
return tagName;
} else if (c === ">") {
emitToken(currentToken);
return parseLiteral;
} else {
return tagName;
}
}
function beforeAttributeName(c) {
// tab、换行、禁止符、空格
if (c.match(attrRex)) {
return beforeAttributeName;
} else if (c === "/") {
emitToken(currentToken);
return selfClosingStartTag;
} else if (c === ">") {
emitToken(currentToken);
return parseLiteral;
} else if (c === "=") {
return beforeAttributeName;
} else {
currentAttribue = {
name: "",
value: "",
};
return attributeName(c);
}
}
function attributeName(c) {
if (c.match(attrRex) || c === "/" || c === ">") {
return afterAttributeName(c);
} else if (c === "=") {
return beforeAttributeValue;
} else {
currentAttribue.name += c;
return attributeName;
}
}
function afterAttributeName(c) {
if (c.match(attrRex)) {
currentAttribue.value = currentAttribue.name;
currentToken[currentAttribue.name] = currentAttribue.value;
return beforeAttributeName;
} else if (c === "/") {
return selfClosingStartTag;
} else if (c === ">") {
emitToken(currentToken);
return parseLiteral;
}
}
function beforeAttributeValue(c) {
if (c.match(attrRex) || c === "/" || c === ">" || c === EOF) {
return beforeAttributeValue(c);
} else if (c === '"') {
return doubleQuotedAttributeValue;
} else if (c === "'") {
return singQuotedAttributeValue;
} else {
return UnquotedAttributeValue(c);
}
}
function doubleQuotedAttributeValue(c) {
if (c === '"') {
currentToken[currentAttribue.name] = currentAttribue.value;
return afterQuotedAttributeValue;
} else {
currentAttribue.value += c;
return doubleQuotedAttributeValue;
}
}
function singQuotedAttributeValue(c) {
if (c === "'") {
currentToken[currentAttribue.name] = currentAttribue.value;
return afterQuotedAttributeValue;
} else {
currentAttribue.value += c;
return singQuotedAttributeValue;
}
}
function UnquotedAttributeValue(c) {
if (c.match(attrRex)) {
currentToken[currentAttribue.name] = currentAttribue.value;
return beforeAttributeName;
} else if (c === "/") {
currentToken[currentAttribue.name] = currentAttribue.value;
return selfClosingStartTag;
} else if (c === ">") {
currentToken[currentAttribue.name] = currentAttribue.value;
emitToken(currentToken);
return parseLiteral;
} else {
currentAttribue.value += c;
return UnquotedAttributeValue;
}
}
function afterQuotedAttributeValue(c) {
if (c.match(attrRex)) {
return beforeAttributeName;
} else if (c === "/") {
return selfClosingStartTag;
} else if (c === ">") {
currentToken[currentAttribue.name] = currentAttribue.value;
emitToken(currentToken);
return parseLiteral;
} else {
currentAttribue.value += c;
return doubleQuotedAttributeValue;
}
}
function selfClosingStartTag(c) {
if (c === ">") {
currentToken.__type = TokenKind.endTag;
emitToken(currentToken);
return parseLiteral;
}
}
function closeTag(c) {
if (c.match(tagRex)) {
currentToken = {
__type: TokenKind.endTag,
tagName: "",
};
return tagName(c);
}
}
function init() {
// 顶部相当于空标签 <></>
// 这样 template 可以直接并列写内容
AST = [
{
__type: "fragment",
children: [],
attributes: [],
tagName: "fragment",
},
];
state = parseLiteral;
}
export function parser(template) {
init();
for (const c of template) {
state = state(c);
}
return AST[0];
}
测试
- 首先将相关代码改造下
index.js 增加 template 内容
// index.js
import Vue from "./lib";
new Vue({
el: "#app",
template: `
<h1>template解析</h1>
静态文本节点
<div>{{msg}}</div>
<input v-model="msg" type="text" />
<Component />`,
data: {
msg: "msg",
},
});
index.html 则删除 id="app"
标签里的内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>toy vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/index.js"></script>
</body>
</html>
- 运行结果如下:
在线查看地址:code.juejin
总结
本篇基于有限状态机
实现了一个简单的模板解析器,在 Compiler
中通过模板解析动态生成DOM,这样就不用直接在html
中直接写死了,同时也为接下来的组件化
提供了前提条件
转载自:https://juejin.cn/post/7188714270471749692