Vue.js 源码(9) —— 解析器

这是我参与更文挑战的第9天,活动详情查看: 更文挑战

前言

通过前面的学习,我们了解了模板编译的三个模块。本文,我们将一起继续深入学习其中的编译器。

解析器的作用

我们只有将模板解析成 AST 后,才能基于 AST 做优化或者生成代码字符串,那么解析器是如何将模板解析成 AST 的呢?

举个简单的例子:

<div>
    <p>{{name}}</p>
</div>
复制代码

转换成 AST 后的样子:

{
    tag: "div",
    type: 1,
    staticRoot: false,
    sttatic: false,
    plain: true,
    parent: undefined,
    attrsList: [],
    atttrsMap: {},
    children: [
        {
            tag: "p",
            type: 1,
            staticRoot: false,
            static: false,
            plain: true,
            parent: {tag:"div",...},
            atttrsList: [],
            attrsMap: {},
            children: [
                {
                    type: 2,
                    text: "{{name}}",
                    static: false,
                    expression: "_s(name)"
                }
            ]
        }
    ]
}
复制代码

AST 并不是什么高深的东西,它也是使用 Javascript 中的普通对象来描述一个节点。每个对象中的属性都保存了节点所需的各种数据。比如,parent属性保存了父节点的描述对象,children属性是一个数组,里面保存了一些子节点的描述对象。再比如,type属性表示一个节点的类型等。当很多个独立的节点通过parent属性和children属性连在一起时,就变成了一个树,而这样一个用对象描述的节点树其实就是 AST

内部运行原理

解析器内部也分了好几个子解析器,其中最主要的是HTML解析器。

HTML解析器的作用是解析HTML,它在解析HTML的过程中会不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数结束标签钩子函数文本钩子函数以及注释钩子函数

parseHTML(template, {
    start(tag, attrs, unary, start, end) {
        // 解析到标签的开始位置,触发该函数
    },
    end(tag, start, end){
        // 解析标签的结束位置,触发该函数
    },
    chars(text, start, end){
        // 解析道纯文本,触发该函数
    },
    comment(text, start, end){
        // 解析到注释,触发该函数
    }
})
复制代码

以一个简单例子为例:

<div><p>hello world</p></div>
复制代码

在解析上面的模板时,会依次触发 start(<div>)、start(<p>)、chars(hello world)、end(</p>)、end(</div>)。

如果触发了 start 钩子,我们就创建一个元素节点,加到栈的顶部。

为什么使用 栈 来存储节点?

因为first-in-last-out 的特点,它的典型运用是括号匹配问题。用在这里,有两个好处:

  1. 校验元素匹配的问题。
  2. 当栈为空时,表明模板已经处理完毕。

start钩子——创建元素节点

const stack = []; // 临时存储节点
let currentParent; // 父节点

function createASTElement(tag, attrs, parent){
    return {
        type: 1,
        tag,
        attrsList: attrs,
        parent,
        children
    }
}
parseHTML(template, {
    start(tag, attrs, unary, start, end) {
        let element = createASTElement(tag, attrs, currentParent);
        currentParent = element;
        stack.push(element)
    },
})
复制代码

end钩子——弹出元素节点

parseHTML(template, {
    end(tag, start, end) {
        const element = stack[stack.length-1];
        stack.length -= 1;
        currentParent = stack[statkc.length-1];
    }
})
复制代码

chars———创建文本节点

parseHTML(template, {
    chars(text, start, end){
        let element = { type: 3, text };
        let children = currentParent.chilren;
        children.push(element);
    }
})
复制代码

comment——创建注释节点

parseHTML(template, {
    comment(text){
        let element = { type: 3, text, isComment: true}
        let children = currentParent.chilren;
        children.push(element);
    }
})
复制代码

HTML 解析器

运行原理

解析HTML简单来说就是用 HTML 模板字符串来循环,每轮循环都从 HTML 模板中截取一小段字符串,然后重复以上过程,直到 HTML 模板被截成一个空字符串时结束循环,解析完毕。

以之前的一个例子作为示例:

`<div>
    <p>{{name}}</p>
</div>`
复制代码
  1. 第一轮循环,取出 <div>,触发 start 钩子函数,截取后的结果
`
    <p>{{name}}</p>
</div>`
复制代码
  1. 第二轮循环,取出一段字符串,触发 chars 钩子函数
`
   `
复制代码

截取后的结果为:

`<p>{{name}}</p>
</div>`
复制代码
  1. 第三轮循环,取出 <p>,触发 start 钩子函数,截取后的结果
`{{name}}</p>
</div>`
复制代码
  1. 第四轮循环,取出 {{name}} 字符串,触发 chars 钩子函数,截取后的结果:
`</p>
</div>`
复制代码
  1. 第五轮循环,取出 </p>,触发 end 钩子函数,截取后的结果:
 `
</div>`
复制代码
  1. 第六轮循环,取出一段字符串:
`
`
复制代码

截取后的结果:

`</div>`
复制代码
  1. 第七轮循环,取出 </div>,触发 end 钩子函数,截取后的结果:
``
复制代码

总结

本文,我们学习了解析器的基本工作流程,以及 HTML 解析器的基本原理。后面我们还会继续学习 HTML 解析器的具体实现,敬请期待…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享