Vue原理:parseHTML实现

这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战

VueHTML转译为AST大致过程:通过正则不断的匹配HTML的标签、属性、注释等,根据不同的类型进行不同的处理,比如匹配到是标签类型,那么此时需要创建一个对象,将其标签名赋值给tag属性,设置其type为1等,因为其是一个标签类型,需要匹配其属性,若是存在需要将属性依次存储到attrs中,若是这个标签存在子元素,需要将子的信息存储在childred中,同时标记parent是谁,这样一个HTML标签对应的AST便创建完成了

解析入口

html转换为AST的整个流程封装在parseHTML函数中

function parseHTML(html) {
    let root = null; //  ast树的树根
    let currentParent = null; // 标识当前的parent

    // 定义一个栈数据结构,将解析的标签进行存取
    let stack = [];

    // 将节点类型初始化
    const ELEMENT_TYPE = 1;
    const TEXT_TYPE = 3;
}
复制代码

首先将一些常用的变量进行定义并初始化,接下来便是对html进行正则匹配,可参考下面代码和注释结合理解

// 不停的解析HTML字符串
while (html) {
    let textEnd = html.indexOf("<");

    // 标签相关处理
    if (textEnd === 0) {
        // 匹配到是开始标签
        let startTagMatch = parseeStartTag();
        if (startTagMatch) {
            start(startTagMatch.tagName, startTagMatch.attrs);
            continue; // 如果开始标签匹配结束了 继续下一次匹配
        }

        // 匹配到是结束标签
        let endTagMatch = html.match(endTag);
        if (endTagMatch) {
            advance(endTagMatch[0].length);
            end(endTagMatch[1]);
            continue;
        }
    }

    // 文本相关处理
    let text;
    if (textEnd >= 0) {
        text = html.substring(0, textEnd);
    }

    if (text) {
        advance(text.length);
        chars(text);
    }
}
复制代码

通过代码可得知其就是不断通过正则匹配html进行处理,每次处理结束便将html截取进行下一次的匹配处理,一直到html为空才结束,每次处理开始,使用textEnd判断匹配到的是标签还是文本

标签

若是标签就进入到标签的处理逻辑,标签分为开始标签和结束标签

开始标签

const parseeStartTag = () => {
    let start = html.match(startTagOpen);
    // 匹配到内容
    if (start) {
    let match = {
        tagName: start[1],
        attrs: [],
    };
    advance(start[0].length); // 将标签删除

    let end, attr;

    // 匹配到标签结束 && 有属性
    // 将属性进行解析
    while (
        !(end = html.match(startTagClose)) &&
        (attr = html.match(attribute))
    ) {
        advance(attr[0].length); // 将属性移除
        match.attrs.push({
            name: attr[1],
            value: attr[3] || attr[4] || attr[5],
        });
    }

    // 结束标签
    if (end) {
        // 去掉开始标签的 >
        advance(end[0].length);
        return match;
    }
  }
};
复制代码

因为在开始标签内会填充很多属性,函数内部需要将标签名和属性进行提取并返回,然后对其进行处理,逻辑封装至start函数中

const start = (tagName, attrs) => {
    // 遇到开始标签 创建一个 ast 元素
    let element = createASTElement(tagName, attrs);

    if (!root) {
        root = element;
    }

    // 把当前元素标记为 parent
    currentParent = element;
    stack.push(element); // 将开始标签 存放到栈中
};
复制代码

结束标签

匹配其是到结束标签时,将逻辑封装到end函数中

const end = (tagName) => {
    let element = stack.pop();

    // 标识当前这个元素是属于parent的children
    currentParent = stack[stack.length - 1];

    if (currentParent) {
        element.parent = currentParent;
        currentParent.children.push(element); // 实现一个树的父子关系
    }
};
复制代码

文本节点

对于文本节点的处理比较简单,直接创建对应的AST返回即可,将逻辑封装到chars函数中

const chars = (text) => {
    text = text.replace(/\s/g, "");

    if (text) {
        currentParent.children.push({
            text,
            type: TEXT_TYPE,
        });
    }
};
复制代码

至此parseHTML的核心流程完成,拿到AST之后,接下来便是准备如何根据AST生成render函数

猜你喜欢

转载自juejin.im/post/7032467653037932558