首页 前端知识 2024年前端最新~零基础前端编译原理科普,有问有答,CSS全局样式

2024年前端最新~零基础前端编译原理科普,有问有答,CSS全局样式

2024-08-08 22:08:53 前端知识 前端哥 943 365 我要收藏

性能优化

1.webpack打包文件体积过大?(最终打包为一个js文件)

2.如何优化webpack构建的性能

3.移动端的性能优化

4.Vue的SPA 如何优化加载速度

5.移动端300ms延迟

6.页面的重构

所有的知识点都有详细的解答,我整理成了280页PDF《前端校招面试真题精编解析》。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

后面把树形的 ast 转换为另一个 ast,然后再打印成目标代码的字符串,这是转译器,把 ast 解释执行或者专成线性的中间代码再解释执行,这是解释器,把 ast 转成线性中间代码,然后生成汇编代码,之后做汇编和链接,生成机器码,这是编译器。

编译器是咋处理 AST 的?


昊昊:光哥,那编译器是怎么处理 ast 的啊?

我:有了 ast 之后,计算机就能理解高级语言代码了,但是编译器要产生低级语言,比如汇编代码,直接从 ast 开始距离比较远。因为一个是嵌套的、树形的,一个是线性的、顺序的,所以啊,需要先转成一种线性的代码,再生成低级代码。我觉得 ast 也可以算一种树形 IR,IR 是 immediate representation 中间表示的意思。要先把 AST 转成线性 IR,然后再生成汇编、字节码等。

咋翻译,树形的结构咋变成线性的呢?明显要递归啊,按照语法结构递归 ast,进行每个节点的翻译,这叫做语法制导翻译,用线性 IR 中的指令来翻译 AST 节点的属性。每个节点的翻译方式,if 咋翻译、while 咋翻译等可以去看下相关资料,搜中间代码生成就好了。

但是 ast 不能上来就转中间代码。

昊昊:为啥,ast 不就能表示源码信息了么,为啥不能直接翻译成线性 ir?

我:因为还没做语义检查啊,结构对不一定意思对,就像“昊昊是只猪”,这个符合语法吧,但是语义明显不对啊,这不是骂人么,所以要先做语义检查。还有就是要推导出一些信息来,才能做后续的翻译。

语义分析要检查出语义的错误,比如类型是否匹配、引用的变量是否存在、break 是否在 while 中等,主要要做作用域分析、引用消解、类型推导和检查、正确性检查等。

作用域分析就是分析函数、块等,这些作用域内的变量都有啥,作用域之间的联系是怎样的,其实作用域是一棵树,从顶层作用域到子作用域可以生成一个树形数据结构。我记得有个做 scope 分析的 webpack 插件,他是把模块也给链接起来了,形成了一个大的 scope graph,然后做分析。

作用域中有各种声明,要把它们的类型、初始值、访问修饰符等信息记录下来,保存这个信息的结构叫符号表,这相当于是一个缓存,之后处理这个符号的时候直接去查符号表就行,不用再次从 ast 来找。

引用消解呢就是对每个符号检查下是否都能查找到定义,如果查找不到就报错。类型方面你比较熟,js 的源码中肯定不可能都写类型,很多地方可以直接推导出来,根据 ast 可以得出类型的声明,记录到符号表中,之后遍历 ast,对各种节点取出声明时的类型来进行检查,不一致就报错。还有其他一些琐碎的检查,比如 continue、break 只能出现在 while 中等等一些检查。

昊昊:语义分析我懂了,就是检查错误和记录一些分析出的信息到符号表,那语义分析之后呢?

语义分析之后就代表着程序已经没有语法和语义的错误了,可以放心进行各种后续转换,不会再有开发者的错误。之后先翻译成线性 IR,然后对线性 IR 进行优化,需要优化就是因为自动生成的代码难免有很多冗余,需要把各种没必要的处理去掉。但是要保证语义不变。比如死代码删除、公共子表达式删除、常量传播等等。

线性 IR 的分析要建立流图,就是控制流图,控制流就是根据 if、while、函数调用等导致的程序跳转,把顺序执行的代码和跳转到的代码之间连接起来就是一个图,顺序执行的代码看成一个整体,叫做基本快。之后根据这个流图做数据流分析,也就是分析一个变量流经了那些代码,然后基于这些做各种优化。

这个部分叫做程序分析,或者静态分析,是一个专门的方向,可以用于代码漏洞的静态检查,可以用于编译优化,这个是比较难的。研究这个的博士都比较少。国内只有北大和南大开设程序分析课程。

优化之后的线性 IR 就可以生成汇编代码了,然后通过汇编器转成机器码,再链接一些标准库,比如 v8 目录下可以看到 builtins 目录,这里就是各种编译好的机器码文件,可以静态链接成一个可执行文件。

昊昊:哦,感觉汇编和链接这两步前端接触不到啊。

我:对的,因为 js 是解释型语言,直接从源码解释执行,不要说 js 了,java 的字节码也不需要静态链接。像 c、c++这些生成可执行文件的才需要通过汇编器把代码专成机器码然后链接成一个文件。而且如果目标平台有这些库,那么不需要静态链接到一起,可以动态链接。你可能听过.dll 和.so 这就分别是 windows 和 linux 的用于运行时动态加载的保存机器码的文件。

你说的没错,前端领域基本不需要汇编和链接,就算是 wasm,也是生成 wasm 字节码,之后解释执行。前端主要还是转译器。

转译器是咋处理 AST 的?


昊昊:那转译器在 ast 之后又做了哪些处理呢?

我:转译器的目标代码也是高级语言,也是嵌套的结构,所以从高级语言到高级语言是从树形结构到树形结构,不像翻译成低级的指令方式组织的语言,还得先翻译成线性 IR,高级到高级语言的转换,只需要 ast,对 ast 做各种转换之后,就可以做代码生成了。

昊昊:我说呢,我就没听说 babel 中有线性 IR 的概念。

我:对的,不管是跨语言的转换,比如 ts 转 rust,还是同语言的转换 js 转 js 都不需要线性结构,两棵树的转换要啥线性中间代码啊。所以一般转译器都是 parse、transform、generate 这 3 个阶段。

parse 广义上来说包含词法、语法和语义的分析,狭义的 parse 单指语法分析。这个不必纠结。

transform 就是对 ast 的增删改,之后 generator 再把 ast 打印成字符串,我们解析 ast 的时候把[]{} () 等分隔符去掉了,generate 的时候再把细节加回来。

其实前端领域主要还是转译器,因为主流 js 引擎执行的是源代码,但是这个源代码和我们写的源代码还不太一样,所以前端很多源码到源码的转译器来做这种转换,比如 babel、typescript、terser、eslint、postcss、prettier 等。

babel 是把高版本 es 代码转成低版本的,并且注入 polyfill。typescript 是类型检查和转成 js 代码。eslint 是根据规范检查,但–fix 也可以生成修复后的代码。prettier 也是用于格式化代码的,比 eslint 处理的更多,不只限于 js。postcss 主要是处理 css 的,posthtml 用于处理 html。相信你也用过很多了。taro 这种小程序转译器就是基于 babel 封装的。

解释器是咋处理 AST 的?


昊昊:哦,光哥,我大概知道编译器和转译器都对 ast 做了啥处理了,这俩都是生成代码的,那解释器呢?

我:对,首先转译器也是编译器的一种,只不过比较特殊,叫做 transpiler,一般的编译器叫做 compiler。解释器和编译器的区别确实是是否生成代码,提前编译成机器代码的叫做 AOT 编译器,运行时编译成机器代码的叫做 JIT 编译器,

解释器并不生成机器代码,那它是怎么执行的呢?知道你肯定有疑问。其实解释器是用一门高级语言来解释另一门高级语言,比如 c++,一般都用 c++来写解释器,因为可以做内存管理。用 c++来写 js 解释器,像 v8、spidermonkey 等都是。我们在有了 ast 并且做完语义分析之后就可以遍历 ast,然后用 c++来执行不同的节点了,这种叫做 tree walker 解释器,直接解释执行 ast,v8 引擎在 17 年之前都是这么干的。但是在 17 年之后引入了字节码,因为字节码可以缓存啊,这样下次再直接执行字节码就不需要 parse 了。字节码是种线性结构,也要做 ast 到线性 ir 的转换,之后在 vm 上执行字节码。

一般解释线性代码的比如汇编代码、字节码等这种的程序才叫做虚拟机,因为机器代码就是线性的,其实从 ast 开始就可以解释了,但是却不叫 vm,我觉得就是因为这个,和机器码比较像的线性代码的解释器才叫 vm。

不管是解释 ast 也好,还是转成字节码再解释也好,效率都不会特别高,因为是用别的高级语言来执行当前语言的代码,所以要提高效率还是得编译成机器代码,这种运行时编译就是 JIT 编译器,编译是耗时的,所以也不是啥代码都 JIT,要做热度的统计,到达了阈值才会做 JIT。然后把机器码缓存下来,当然也可能是缓存的汇编代码,用到的时候再用汇编器转成机器码,因为机器代码占的空间比较大。

可以对比 v8 来理解,v8 有 parser、ignation 解释器、turbofan 编译器,还有 gc。ignation 解释器就是把 parse 出的 ast 转成字节码,然后解释执行字节码,热度到达阈值之后会交给 turbofan 编译为汇编代码之后生成机器代码,来加速。gc 是独立的做内存管理的。

turbofan 是涡轮增压器,这个名字就能体现出 JIT 的意义。但 JIT 提升了执行速度,也有缺点,比如会使得 js 引擎体积更大,占用内存更大,所以轻量级的 js 引擎不包含 jit,这就是运行速度和包大小、内存空间之间的权衡。架构设计也经常要做这种两边都可以,但是要做选择的 trade off,我们叫做方案勾兑。

说到权衡,我想起 rn 的 js 引擎 hermes 就改成支持直接执行字节码了,在编译期间把 js 代码编译成字节码,然后直接执行字节码,这就是在跨端领域的 js 引擎的 trade off。

前端领域都有哪些地方用到编译知识?


昊昊:哦,光哥,我明白解释器、编译器、转译器都干啥的了,那前端领域都有那些地方用到编译原理的知识呢?

我:其实你也肯定有个大概的了解了,但是不够明确,我列一下我知道的。

工程化领域各种转译器:babel、typescript、eslint、terser、prettier、postcss、posthtml、taro、vue template compiler 等

js 引擎:v8、javascriptcore、quickjs、hermes 等

wasm:llvm 可以生成 wasm 字节码,所以 c++、rust 等可以转为 llvm ir 的语言都可以做 wasm 开发

ide 的 lsp:编程语言的语法高亮、智能提示、错误检查等通过 language service protocol 协议来通信,而 lsp 服务端主要是基于 parser 对正在编辑的文本做分析

自己如何实现一门语言呢?


昊昊:我学了编译原理可以实现一门语言么?

我:其实编程语言主要还是设计,实现的话首先实现 parser 和语义分析,后面分为两条路,一种是解释执行的解释器配合 JIT 编译器的路,一种是编译成汇编代码码,然后生成机器码再链接成可执行文件的编译器的路。

parser 部分比较繁琐,可以用 antlr 这种 parser 生成器来生成,语义分析要自己写,这个不太难,主要是对 ast 的各种处理。之后如果想做成编译器,可以用 llvm 这种通用的优化器和代码生成器,clang、rust、swift 都是基于它,所以很靠谱,可以直接用。如果做解释器可以写 tree walker 解释器,或者再进一步生成线性字节码,然后写个 vm 来解释字节码。JIT 编译器也可以用 llvm 来做。要把 ast 转成 llvm ir,也是树形结构转线性结构,这个还是编译领域很常见的操作。

其实编译原理只是告诉你怎么去实现,语言设计不关心实现,一门语言可以实现为编译型也可以实现为解释型,也可以做成 java 那种先编译后解释,你看 hermes 不就是先把 js 编译为字节码然后解释执行字节码么。语言不分编译解释,这个概念要有,c 也有解释器,js 也有编译器,我们说一门语言是编译型还是解释型主要是主流的方式是编译还是解释来决定的。

编程语言可以分为 GPL 和 DSL 两种。

GPL 是通用编程语言,它是图灵完备的,也就是能够描述任何可计算问题,像 c++、java、python、go、rust 等这些语言都是图灵完备的,所以一门语言能实现的另一门语言都能实现,只不过实现难度不同。比如 go 语言内置协程实现,那么写高并发程序就简单,java 没有语言级别的协程,那么就要上层来实现。你可能听到过设计模式是对语言缺陷的补充就是这个意思,不同语言设计思路不同,内置的东西也不同,有的时候需要运行时来弥补。、

编程语言有不同的设计思路,大的方向是编程范式,比如命令式、声明式、函数式、逻辑式等,这些大的思路会导致语言的语法,内置的实现都不同,表达能力也不同。这基本确定了语言基调,后续再补也很难,就像 js 里面实现函数式,你又不能限制人家不能用命令式,就很难写出纯粹的函数式代码。

DSL 不是图灵完备的,却换取了某领域的更强的表达能力,比如 html、css、正则表达式,jq 的选择器语法等等,比较像一种伪代码,特定领域的表达能力很强,但是却不是图灵完备的不能描述所有可计算问题。

编译原理是实现编程语言的步骤要学习的,更上层的语言设计还要学很多东西,最好能熟悉多门编程语言的特性。

我该怎么学习编译原理呢?


昊昊:光哥,那我该怎么学习编译原理呢?

我:首先你要理解编译都学什么,看我上面对编译、转译、解释的科普大概能有个印象,然后查下相关资料。知道都可以干啥了之后先写 parser,因为不管啥都要先 parse 成 ast 才能被“理解”和后续处理,学下有限状态机来分词和递归下降构造 ast。推荐看下 wepack 的 wxml transpiler,lexer 部分写的还可以。这种 xml 的 parser 比较简单,适合入门。语言级别的 parser 细节很多,还是得找一个来 debug 看。不过我觉得没太大必要,一般也就写个 html parser,要是语言的,可以用 antlr 生成。转译器肯定要了解 babel,这个是前端领域很不错的转译器。

js 引擎可以尝试用 babel 做 parser,自己做语义分析,解释执行 ast 试试,之后进一步生成字节码或其他线性 ir,然后写个 vm 来解释字节码。

还可以学习 wasm 相关技术,那个是涉及到其他语言编译到 wasm 字节码的过程的。

当你学完了编译原理,就大概知道怎么实现一门编程语言了,之后想深入语言设计可以多学一些其他编程范式的语言,了解下各种语言特性,怎么设计一门表达性强的 gpl 或者 dsl。也可以进一步学习一下操作系统和体系结构,因为编译以后的代码还是要在操作系统上以进程的形式运行的,那么运行时该怎么设计就要了解操作系统了。然后 cpu 指令集是怎么用电路实现的,这个想深入可以去看下计算机体系结构。

最后

如果你已经下定决心要转行做编程行业,在最开始的时候就要对自己的学习有一个基本的规划,还要对这个行业的技术需求有一个基本的了解。有一个已就业为目的的学习目标,然后为之努力,坚持到底。如果你有幸看到这篇文章,希望对你有所帮助,祝你转行成功。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

转载请注明出处或者链接地址:https://www.qianduange.cn//article/15016.html
标签
评论
发布的文章

安装Nodejs后,npm无法使用

2024-11-30 11:11:38

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!