概览

Vue.js作为目前最流行的前端框架之一,一些概念和原理还是需要我们前端开发人员了解与深入理解的。

Vue.js涉及的知识点很多,一些重要概念,例如:如何使用proxy实现响应式effect,虚拟DOMDiff算法及演变过程,渲染器原理的实现,编译器、解析器的工作原理,动态节点、静态提升等等;

现在重点采用图解步骤分析一下编译器的简单工作原理;

编译器概念

编译器其实就是一段JavaScript代码程序,它将一种语言(A)编译成另外一种语言(B),其中前者A通常被叫做源代码,后者B通常被叫做为目标代码。例如我们vue的前端项目的.vue文件一般即为源代码,而编译后dist文件里的.js文件即为目标代码;这个过程就被称为编译(compile)

关键概念

主要涉及的概念:

  • DSL 领域特定语言
  • AST 抽象语法树(Abstract Syntax Tree)
  • 有限状态机
  • 深度优先算法

简单流程

一个标准的编译器流程如下图所示:

Vue.js作为DSL,其编译流程会与上图有所不同,对于Vue.js来说,源代码就是组件的模板代码,而目标代码就是能够在浏览器(或其他平台)平台上运行的JavaScript代码。

Vue的编译器

Vue.js的目标代码其实就是渲染函数(render函数)。概况而言,Vue.js编译器首先对模板进行词法分析、语法分析,然后得到模板的抽象语法树(AST)。随后将模板AST转换成JavaScript AST,最后再转换成JavaScript代码,及渲染函数。一个简单的Vue.js模板编译器的工作流如下:

简单如下:
模板代码

<div><h1 id="vue">vue_compiler</h1>
</div>

目标的AST

const ast = {type: 'Root',children: [{type: 'Element',tag: 'div',children: [{type:'Element',tag: 'h1',props: [{type: 'Attribute',name: 'id',content: 'vue'}],children: [{type: 'Text',content: 'vue_compiler'}]}]}]
}

目标代码

function render() {return h('div', [h('h1', {id: 'vue'}, 'vue_compiler')])
}

由以上代码可以看出,AST其实就是一个具有层级结构的对象,模板的AST与模板具有相同的嵌套结构。每一颗AST都有一个逻辑上的根节点,其类型为Root,而模板中真正的根节点则作为Root节点的children存在。

观察AST可知:

  • 不同类型的节点是通过节点的type属性进行区分的。
  • 标签节点的子节点存储在其children数组中。
  • 标签节点的属性节点会存储在props数组中。
  • 不同类型的节点会使用不同的对象属性进行描述。

编译过程

parse函数

Vue.js通过封装parse函数,实现对模板的词法分析和语法分析,最终得到模板的AST。parse函数接收模板字符串作为参数,并将解析后的AST作为返回值返回;

const template = `<div><h1>vue<h1></div>
`
const templateAst = parse(template)

解析器是如何对模板字符串进行分割的呢,此处就需要用到有限状态自动机。指的是在有限个状态之间,随着字符的输入,解析器会自动地在不同的状态之间进行切换。(实际上有限状态机是可以使用正则表达式来实现的)。

简单的状态机流程图:

通过有限状态机原理,可一帮助我们完成对模板的标记,最终将得到一系列Token(词法标记号)。

假设有如下代码:

const template = `<div><span>Vue</span><p>Vue Compiler</p></div>` // 模板字符串// 通过有限状态机原理实现词法分解得到三个Token
// 开始标签 <div>
// 文本节点 vue
// 结束标签 </div>

状态机的执行过程:状态机始于“初始状态 1”。

  • 在“初始状态 1”下,读取模板的第一个字符<,状态机会进入下一个状态,即“标签开始状态 2”。
  • 在“标签开始状态 2”下,读取下一个字符div。由于字符d是字母,所以状态机会进入“标签名称状态3”。
  • 在“标签名称状态 3”下,读取下一个字符>,此时状态机会从“标签名称状态3”迁程回“初始状态1”,并记录在“标签名称状态”下产生的标签名称div
  • 在“初始状态 1”下,读取下一个字符<,状态机会进入下一个状态,即“标签开始状态 2”
  • 在“标签开始状态 2”下,读取下一个字符span。由于字符s是字母,所以状态机会进入“标签名称状态3”。
  • 在“标签名称状态 3”下,读取下一个字符>,此时状态机会从“标签名称状态3”迁程回“初始状态1”,并记录在“标签名称状态”下产生的标签名称span
  • 在“初始状态 1”下,读取下一个字符V,此时状态机会进入“文本状态 4”。
  • 在“文本状态 4”下,继续读取后续字符,直到遇到字符<时,状态机会再次进入“标签开始状态 2”,并记录在“文本状态 4”下产生的文本内容,即字符串“Vue”。
  • 在“标签开始状态2”下,读取下一个字符1,状态机会进入“结束标签状态 5”。
  • …循环读取…
  • 在“结束标签名称状态6”下,读取最后一个字符>,它是结束标签的闭合字符,于是状态机迁移回“初始状态 1”,并记录在“结束标签名称状态 6”下生成的结束标签名称。
    经过这样一系列的状态迁移过程之后,我们最终就能够得到相应的Token了。以上就是一个简单的状态机的执行过程。
// 最终值为
const tokens = tokenize(template);
// [
//     {//         type: 'tag', name: 'div'
//     },
//     {//         type: 'tag', name: 'span'
//     },
//     {//         type: 'text', name: 'Vue'
//     },
//     {//         type: 'tagEnd', name: 'span'
//     },
//     {//         type: 'tag', name: 'p'
//     },
//     {//         type: 'text', name: 'Vue Compiler'
//     },
//     {//         type: 'tagEnd', name: 'p'
//     },
//     {//         type: 'tagEnd', name: 'div'
//     }
// ]// 此代码需要生成的AST应为
const ast = {type: 'Root',children: [{// 实际的根节点type: 'Element',tag:: 'div',children: [{type: 'Element',tag:: 'span',children: [{type: 'Text',content: 'Vue'}]},{type: 'Element',tag:: 'p',children: [{type: 'Text',content: 'Vue Compiler'}]}]}]
}

以上代码生成的AST数据结构HTML结构相同,都是树状结构

接下来要做的就是将生成的tokens转换成AST,在转换过程中需要维护一个Stack,这个栈将用来维护元素间的父子关系。每到遇到一个开始标签,就创建一个Element类型的AST节点,并将其压入栈内,类似的,每当遇到一个结束标签节点,我们就将当前栈顶的节点弹出。这样栈顶的节点将始终充当父节点的角色。转换过程中的所有节点,都将作为当前栈顶节点的子节点,并添加到栈顶节点的children属性下。流程如下图示:

最初节点只有根节点Root

当扫描到第一个标签是开始节点是,因此我们创建一个类型为Element的AST节点Element(div),并将该节点作为当前节点的子节点。由于当前的栈顶节点是Root节点,所以新创建的Element(div)节点作为Root节点的子节点被添加到AST中,最后将新建的Element(div)节点压入栈中。

由于第二个节点也是一个开始标签,所以流程同上一步,只不过当前的栈顶节点为Element(div),所以将当前的节点Element(span)作为其子节点添加到AST中,最后将Element(div)节点压入栈中。

接下来的节点是一个文本节点,所以需要创建一个Text类型的AST节点,并将其作为栈顶节点Element(span)的子节点加入到AST中,不同的时,当前接待不是Element类型,所以不需要压入栈中;

下面是一个结束标签节点,根据规则,则需要将当前栈顶的节点弹出。

后面的流程此处就不在累述

最终完成后的效果如下:

现在我们来实现parse函数

function parse(str) {// 对模板进行词法分析,得到节点listconst tokens = okenize(template);// 创建跟节点const root = {type: 'Root',children: []};// 创建节点栈,root节点作为栈的根节点const stack = [root];while(tokens.length) {const parent = stack[stack.lenth - 1];const token = tokens[0] // 从第一个点开始switch(t.type) {case 'tag':const eleNode = {type: 'Element',tag: t.name,children: []}parent.children.push(eleNode);stack.push(eleNode);break;case 'text':const textNode = {type: 'Text',content: t.content}parent.children.push(textNode);break;case 'tagEnd':// 结束标签,将栈顶节点弹出栈stack.pop();break;}// 消费掉已处理的节点tokens.shift()}return root
}

以上就是一个简版的parse函数的实现,当然相对于Vue.js的源码还有很多差异,但基本原理大致相同。

下面关于transform函数和generate函数仅做了简要说明,具体实现原理敬请期待;

transform函数

const template = `<div><h1>vue<h1></div>
`
const templateAst = parse(template)
const jsAst = transform(templateAst)

generate函数

const template = `<div><h1>vue<h1></div>
`
const templateAst = parse(template)
const jsAst = transform(templateAst)
const code = generate(jsAst)

完整流程

以上就是Vue模板编译器的基本结构和工作流程,它主要有三个部分组成:

  • 用来将模板字符串解析为模板AST的解析器(parser);
  • 用来将模板AST解析成JavaScript AST的转换器(transformer);
  • 用来根据JavaScript AST生成渲染函数代码的生成器(generator);

本文章主要讨论了parser的基本实现原理(实际上Vue.js的真正实现要复杂的多,比如正则解析、Vue语法解析v-ifv-show、内插值{{}}等等),以及如何使用有限状态自动机来构造一个词法分析器,其过程就是状态机在不同的状态之间进行迁移的过程,并生成一个Token列表集合。然后使用Token列表集合和顶节点元素栈来构造一个可以用来描述模板的AST,最后使用模板AST来解析成JavaScript AST和渲染函数。

参考

Vue.js源码;

Vue.js设计与实现;

图解 Vue3.0 编译器核心原理(Vue3.0源码解析)相关推荐

  1. Hystrix核心原理和断路器源码解析

    Hystrix运行原理 构造一个HystrixCommand或HystrixObservableCommand对象 执行命令. 检查是否已命中缓存,如果命中直接返回. 检查断路器开关是否打开,如果打开 ...

  2. MySQL核心参数含义的源码解析

    引言 你访问的网站,大部分使用Apache服务器;你访问的网站,大部分使用Linux或BSD操作系统:你访问的网站,大部分使用MySQL数据库;你提交DNS域名查询请求大多由BIND服务器分析处理;你 ...

  3. 稀疏多项式的运算用链表_用最简单的大白话聊一聊面试必问的HashMap原理和部分源码解析...

    HashMap在面试中经常会被问到,一定会问到它的存储结构和实现原理,甚至可能还会问到一些源码 今天就来看一下HashMap 首先得看一下HashMap的存储结构和底层实现原理 如上图所示,HashM ...

  4. 社区发现算法原理与louvain源码解析

    前言 社区发现(community detection),或者社区切分,是一类图聚类算法,它主要作用是将图数据划分为不同的社区,社区内的节点都是连接紧密或者相似的,而社区与社区之间的节点连接则是稀疏的 ...

  5. SpringBoot四大核心之自动装配——源码解析

    四大核心 1.自动装配:简单配置甚至零配置即可运行项目 2.Actuator:springboot程序监控器 3.starter:jar包的引入,解决jar版本冲突问题 4.CLI:命令行 初学体验 ...

  6. Laravel开发:Laravel核心——Ioc服务容器源码解析(服务器绑定)

    服务容器的绑定 bind 绑定 bind 绑定是服务容器最常用的绑定方式,在 上一篇文章中我们讨论过,bind 的绑定有三种: 绑定自身 绑定闭包 绑定接口 今天,我们这篇文章主要从源码上讲解 Ioc ...

  7. Scrapy分布式原理及Scrapy-Redis源码解析(待完善)

    1 Scrapy分布式原理 2 队列用什么维护 首先想到的可能是一些特定数据结构, 数据库, 文件等等. 这里推荐使用Redis队列. 3 怎样来去重 保证Request队列每个request都是唯一 ...

  8. Dubbo 实现原理与源码解析系列 —— 精品合集

    摘要: 原创出处 http://www.iocoder.cn/Dubbo/good-collection/ 「芋道源码」欢迎转载,保留摘要,谢谢! 1.[芋艿]精尽 Dubbo 原理与源码专栏 2.[ ...

  9. Vue2.0源码解析——编译原理

    Vue2.0源码解析--编译原理 前言:本篇文章主要对Vue2.0源码的编译原理进行一个粗浅的分析,其中涉及到正则.高阶函数等知识点,对js的考察是非常的深的,因此我们来好好啃一下这个编译原理的部分. ...

最新文章

  1. 队列判空_数据结构与算法——队列的C语言实现
  2. leetcode算法题--搜索旋转排序数组
  3. NOIP2017大爆炸
  4. python sklearn.learning_curve 什么是学习曲线?
  5. WPF-21:WPF实现仿安卓的图案密码键盘(初级)
  6. java排队系统模型,MMC排队系统模型
  7. 面试官:不会看SQL执行计划,简历也敢写精通SQL优化?
  8. 互联网把农业推向“科技仙境”
  9. 零基础应该先学习 java、php、前端 还是 python?
  10. php 检测函数是否为对象,php如何查看对象方法
  11. repeater中分页aspnetpager是遇到的问题
  12. libmesh 思维导图(类接口设计)
  13. 前端页面调试、抓包工具——spy-debugger
  14. 两级运放积分器的带宽分析
  15. 利用python批量查询企业信息_用Python批量查询域名(并行化,附源代码)
  16. python结果不能全部显示_numpy矩阵数值太多不能全部显示的解决
  17. 百度地图Web API Python模块
  18. 利用vbs维护qtp的虚拟对象的坐标
  19. 正则表达式验证中文或者英文
  20. 计算机术语dump是什么意思?

热门文章

  1. 刚子扯谈:标题木有啊
  2. OSChina 周四乱弹 ——让狗狗拿什么证明来爱你
  3. IDEA中引入框架并配置artifact后,启动tomcat无法访问项目
  4. AI:人工智能领域算法思维导图集合之有监督学习/无监督学习/强化学习类型的具体算法简介(预测函数/优化目标/求解算法)、分类/回归/聚类/降维算法模型选择思路、11类机器学习算法详细分类之详细攻略
  5. wordpress 网站模板-免费wordpress 网站模板以及插件中心
  6. 美国跳过5G直抢6G市场,华为会让他如愿吗?
  7. flutter图片上传
  8. PHP报错:Declaration of ... should be compatible with ... 的解决方法
  9. c++ 数据结构——二叉树的构建及其应用,实现左右子树交换并输出前序递归结果
  10. 好记性不如烂笔头之 App widgets(二)