likes
comments
collection
share

拆解Vue2核心模块实现(2)

作者站长头像
站长
· 阅读数 47

前言

在上篇 拆解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>

总结

本篇基于有限状态机实现了一个简单的模板解析器,在 Compiler 中通过模板解析动态生成DOM,这样就不用直接在html中直接写死了,同时也为接下来的组件化提供了前提条件

上一篇: 拆解Vue2核心模块实现(1)

转载自:https://juejin.cn/post/7188714270471749692
评论
请登录