编译原理学习笔记-基于less实践探究(一)

1,200 阅读5分钟

场景

  • 编译原理这本书不用多说,别名龙书是程序员的圣经宝典。我一年之前就看过一点,也就是单纯的看过,现在可以说是基本毫无印象,一是没有做读书笔记,二是没有去实践,还是那句话纸上得来终觉浅,绝知此事要躬行。

fuqian.jpeg

  • 为什么又想起来去重读龙书呢?还是项目痛点,项目跑起来实在是太卡了,随着工程扩大,启动项目变成一个极其漫长的事情!我在想为什么一定要用node作为前端的工程基础呢?为什么不用其他语言呢?为什么不用golang去做呢?用golang可以啊,怎么编译呢?看龙书!

  • 突发奇想一个理想的前端工程应该是怎么样的?一个高效的编译开发体验,一个简单文件目录就包含一个可执行文件加前端代码资源,一个容器化磨平差异不用担心windows,mac平台不同。

jiling.jpeg

思路起源

  • esbuild从去年过年后刚知道的时候,就用它来跑react项目虽然结果失败了,但使用go作为编译工具却在我脑海中埋下了种子。
  • go真的比nodejs快吗?事实胜于雄辩,脚本语言慢真的是天生的。下面是nodejs和go做做100000以内的求和实验
//nodejs代码
console.time("test");
var sum = 0;
var target = 100000;
for (let i = 0; i < target; i++) {
  sum += i;
}
console.log("JS:sum:", sum);
console.timeEnd("test");
//golang代码
package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()
	var sum = 0
	var target = 100000
	for i := 0; i < target; i++ {
		sum += i
	}
	fmt.Println("GO:sum:", sum)
	cost := time.Since(start)
	fmt.Println("Runtime:", cost)
}

jieguo.png

语言执行时间
nodejs13.074ms
nodejs15.89ms
nodejs14.844ms
nodejs13.337ms
nodejs13.316ms
平均耗时14.1448ms
语言执行时间
golang67.087µs
golang66.343µs
golang73.034µs
golang71.219µs
golang68.233µs
平均耗时69.1832µs
  • 这还用比吗?单位都不是一个单位,nodejs时间单位是ms而go是µs。
  • 衍生一下,golang 并发一定比顺序执行快吗?留下一个小疑问!

wenti.jpeg

编译原理学习分享-进入正题

学习方法论

  • 本来已经写了一部分类似读书笔记的博客,自己看了可能都是昏昏欲睡那种!都是书本中核心概念的记录,最终决定还是留下几个关键图。

boke.png

  1. 编译器是一个程序它可以阅读某一种语言并把改程序转换成另一种语言的程序。

graph TD
源程序 --> emperor([编译器]) -->目标程序

编译器主要任务之一就是报告它在翻译过程中的错误

如果目标程序是可执行的机器语言程序,他可以被调用,处理输入并产生输出

graph TD
输入 --> emperor([目标程序]) -->输出
  1. 解析器不通过翻译的方式生成目标程序。直接利用用提供的输入执行源程序的指定的操作
graph TD
源程序 --> emperor([解释器]) -->输出
输入 --> emperor([解释器]) -->输出
  1. 编译器产生的机器语言目标程序通常比解释器快,解释器的错误诊断效果比编译器好,解释器逐个语句执行源程序

  2. 编译构成流程顺序:

graph TD
字符流 --> 语法分析器[(语法分析器)]  --> 符号流 --> 语法分析[(语法分析)] -->语法树 -->  中间代码生成器[(中间代码生成器)]  --> 中间表示形式 -->代码生成器[(代码生成器)]  --> 目标机器语言1[目标机器语言]   --> 机器无关代码优化器[(中间代码生成器)] --> 目标机器语言2[目标机器语言]

学习总结

  • 看了一波上面的图,可能有些不耐烦了,理论很重要就是在于它的指导意义和思想,如果没有这些上去就是干,后面维护性阅读性一定会很差。
  1. 关键知识点输入一段字符,输出另一段可以被机器执行的机器码(例如将less转为css)
  2. 不仅需要实现文本的转换,中间代码的规范提示告警也是必不可少的(例如将width写成了widdth需要提示)
  3. 编译流程如下 :读取字符且去除无效空格,以及无效符号例如";",生成token对象,对token列表进行遍历生成ast对象,将ast对象进行深度优先遍历生成目标机器码

代码实践与理论穿插

代码实践-读取文件

  • 一个库的设计远远没有这么简单,当前只是简单实现,真正库需要配置项和插件机制,还有业务述求例如sourcemap等等。
func main() {
        // 统计计时
	start := time.Now()
        // 读取文件
	file, err := os.Open("test.less")
	if err != nil {
		fmt.Printf("Error: %s\n", err)
		return
	}
	defer file.Close()
        // 缓存
	br := bufio.NewReader(fi)
	for {
                // 按行读取
		line, _, c := br.ReadLine()
		if c == io.EOF {
			break
		}
                // 读取生成token
		pkg.ReadLine1(line)
	}
        //声明ast根对象
	var astData pkg.DataNode
	astData.SelectName = "root"
	astData.Children = make([]pkg.DataNode, 0)
        //根据token遍历生成整个ast对象树
	astData = pkg.GenerateAST(astData)
        //深度优先遍历生成结果字符串
	astDataString := pkg.GenerateChild(astData)
        //判断目标文件是否存在如果存在就删除,保证新文件生成
	if pkg.CheckFileIsExist("test.css") {
		_ = os.Remove("test.css")
	}
        //写入目标文件
	pkg.WriteFile1(astDataString)
        //计算运行耗时
	cost := time.Since(start)
	fmt.Println("Runtime:", cost)
}

代码实践-定义结构体

// token结构体 
// 示例
// {
// TypeName: "Select", // 选择器
// Value: "#video"  // 选择器值
// }
type Token struct {
	TypeName string
	Value    string
}
// Attr 属性结构体 
// 示例
// {
// Name: "width", // 属性名称
// Value: "100px"  // 属性值
// }
type Attr struct {
	Name  string
	Value string
}
// ast 结构体 
type DataNode struct {
	SelectName   string
	Declarations []Attr
	Children     []DataNode
}

代码实践-生成token

// 读取行字节生成token
func ReadLine1(lineData []byte) {
	dataString := string(lineData)
        // less 变量罗例如 @big:100px
	if strings.HasPrefix(dataString, "@") {
                // 变量缓存操作
		variableFormat(dataString)
		return
	}
        // 样式开头行例如 #video {
	if strings.Index(dataString, "{") >= 0 {
		index := strings.Index(dataString, "{")
		selectToken := Token{
			TypeName: "Select",
			Value:    TrimSpace(dataString[:index]),
		}
		Tokens = append(Tokens, selectToken)
	}
        // 样式结尾行例如 }
	if strings.Index(dataString, "}") >= 0 {
		PunctuatorToken := Token{
			TypeName: "Punctuator",
			Value:    "}",
		}
		Tokens = append(Tokens, PunctuatorToken)
	}
        // 属性中间行例如 width:100px;
	if strings.Index(dataString, ":") >= 0 {
                // 样式属性名称 例如 width
		index := strings.Index(dataString, ":")
		before := TrimSpace(dataString[:index])
		attributeToken := Token{
			TypeName: "Attribute",
			Value:    before,
		}
		Tokens = append(Tokens, attributeToken)
		indexEnd := len(dataString) - 1
                // 属性值是不是在less定义的变量中
		value := TrimSpace(dataString[index+1 : indexEnd])
		_, ok := variableMap[value]
                // 样式属性值例如@big或者100px
		if ok {
			attrValue := variableMap[value]
			ValueToken := Token{
				TypeName: "Value",
				Value:    attrValue,
			}
			Tokens = append(Tokens, ValueToken)
		} else {
			ValueToken := Token{
				TypeName: "Value",
				Value:    value,
			}
			Tokens = append(Tokens, ValueToken)
		}
	}
}

理论-生成token

  • 去空:读取文件一行结果其实是像"        width   :  100px;"这样的字符串, 我们可以看到在width字符之前或者 : 符合 前后都存在一定数量的空格,这是需要删除的
  • 去除无效符号:在编译过程中例如";"这样的符号,并没有什么实际意义可以删除(只是在less场景下举例)
  • 终止符号:在读取文件时例如"}"这个符号,就是一个明确的终止符号,可以帮助我们解析token处理ast的逻辑
  • Map表:在less中例如@big这样的字符,是一个变量引用,在下面的解析中需要把@big替换成100px,我们就需要一个这样的缓存空间去缓存这样的数据

代码实践-生成ast

// tokens 列表
// index token的索引
// characterList符号表
func GenerateChildren1(tokens []Token, index int, characterList []string) (children DataNode, i int) {
	child := DataNode{}
	attr := Attr{}
        // less会出现层级嵌套的情况
        //  #body{
        //    #child{
        //     }
        //  }
	var isBodyClose = false
	conNum := -1
	for childIndex := 0; childIndex < len(tokens); childIndex++ {
		if childIndex <= conNum {
			continue
		}
		token := tokens[childIndex]
                //是否是第一层body嵌套
		if token.TypeName == "Select" && !isBodyClose {
			isBodyClose = true
			child = generateChildNode1(token, characterList)
			characterList = append(characterList, token.Value)
			continue
                // 如果是第二层就是child节点进行递归算法
		} else if token.TypeName == "Select" && isBodyClose {
			childTokens := tokens[childIndex:]
			childNode, i := GenerateChildren1(childTokens, childIndex, characterList)
			conNum = i
			child.Children = append(child.Children, childNode)
			continue
                // body体结束
		} else if token.Value == "}" {
			return child, index + childIndex
                // 存入当前选择器的样式属性包括属性名称和属性值        
		} else if token.TypeName == "Attribute" {
			attr.Name = token.Value
			attr.Value = tokens[childIndex+1].Value
			child.Declarations = append(child.Declarations, attr)
			conNum = childIndex + 1
		}
	}
	return children, index
}

理论-生成ast

  • 符号表:例如在less中 #body{ #child{width:100px}} 有这样层级嵌套的场景,需要把上一层的选择器带到下一层,因为生成的结果其实是 #body     #child{width:100px},正如我们看到的 #body     #child 是随着层级的变深不断叠加的,形成这样body->parent->child->grandson这样一个树形链路。在上面的代码中我是用characterList这样的数组去实现的,如果出现新的层级就向数组中添加。
  • 递归:由于子层级的出现就需要我们使用递归的方式将所有的子节点遍历处理
  • 结束体:使用递归必然关注结束,在less中显然"}"这个符号,是我们天然的结束标志

代码实践-写入文件

//child 抽象语法树节点生成字符串
func GenerateChild(child DataNode) string {
	stringLines := child.SelectName + " {" + "\n"
	declarations := child.Declarations
	for _, declaration := range declarations {
		stringLines += "  " + declaration.Name + ": " + declaration.Value + ";\n"
	}
	// 判断body中是否为空属性
	if strings.HasSuffix(stringLines, "{\n") {
		stringLines = ""
	} else {
		stringLines += "}\n"
	}
        // 递归遍历子节点
	for _, childNode := range child.Children {
		stringLines += GenerateChild(childNode)
	}
	return stringLines
}
// 写入文件
func WriteFile1(data string) {
	f, _ := os.Create("test.css")
	f.Close()
	file, err := os.OpenFile("test.css", os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		fmt.Printf("open file error=%v\n", err)
		return
	}
	defer file.Close()
	// _, err = f.Write([]byte("要写入的文本内容"))
	write := bufio.NewWriter(file)
	write.WriteString(data)
	write.Flush()
}

理论-写入文件

  • 抽象语法树:见名知意很明显我们需要把这棵树,使用深度遍历优先的方法,生成每一行css代码
  • 写入效率:bufio的使用可以提升写入效率

总结

  • 当前的代码只是简单的逻辑,还没触及编译原理的核心思想。例如less中的计算等等场景,理论里面的状态机,文法分析,中间代码等等完全没有写,这也是我下一阶段的学习目标与实践场景。
  • 代办事项:场景补齐正在能实现less转css,编译效率提升,使用更好的算法和设计模式
  • go并发是否一定能提升效率,答案是否定的,在场景上我们是不是需要顺序执行,极小携程,有耗时操作,电脑核数等等这些角度去考虑。看个代码示例
func main() {
	start := time.Now()
	var sum = 0
	var target = 100000
        // 并发加锁
	var waitGroup sync.WaitGroup
	var mutex sync.Mutex
	for i := 0; i < target; i++ {
		waitGroup.Add(1)
		go func(val int) {
			mutex.Lock()
			sum += val
			mutex.Unlock()
			waitGroup.Done()
		}(i)
	}
	waitGroup.Wait()
	fmt.Println("GO:sum:", sum)
	cost := time.Since(start)
	fmt.Println("Runtime:", cost)
}

并发.png

  • 由此可见并发在流程上面需要更精细的设计方案才能提升他的性能,单纯的并发并不会提升反而把执行效率降低。
  • 一定要拥抱变化,前端jsp我经历过,三大框架盛行加入nodejs的脚手架我也经历过,前后端分离经历过,nodejs中间层经历过,每一个阶段都需要人去勇于尝试,nodejs是贴近前端,从性能的角度而言nodejs是不是一个合理的选择呢?不管是工具链还是中间服务,还有有人会说生态,nodejs生态难道兴起了很久吗?
  • 关注底层库,关注核心库,提高性能,提升开发体验--加油。