最近需要写一个编辑扩展组件,主要功能类似于Excel的单元格编辑框,主要针对单元格输入内容的处理。要知道在Excel中,每个单元格除了可以输入文本内容(包括字符、数字、日期等)外,还有包括函数。 那么在输入函数时,如果聚焦到函数(比如IF VLOOKUP)或单元格(比如A1 A2)上,都会有对应的响应,那么我们输入的文本,是如何识别到其中的函数,常量,单元格的呢?并且在被聚焦后能够做出对应的响应?
基于这样的需求,经过分析后发现真正需要做的事情,其实是将输入的内容进行解析,然后根据解析的结果,进行相应的处理,最终生成一段HTML内容,而所有的点击响应都是对DOM元素的事件监听而已, 而这里面的关键则是如何将输入的内容,解析成对应的AST,后面生成HTML的过程也就简单了。
现在有这样的一个Excel公式,调用了IF、VLOOKUP函数,引用了单元格与外部sheet区域,包括操作符号,常量等等元素:
IF(VLOOKUP(A1,"sheet1"!$A$1:$C$50,1,2) > 3, 4, 5)
接下来,我们需要做的是将上面的文本转换成下面的存储结构,也就是AST(抽象语法树):
{ type: 'Program', body: [{ type: 'function', name: 'IF', params: [ { type: 'expression, body: [ { type: 'function', name: 'VLOOKUP', params: [ { type: 'cell', value: 'A1', }, { type: 'range', ref: 'sheet1' value: '$A$1:$C$50', }, { type: 'number', value: '1' }, { type: 'number', value: '2' } ] }, { type: 'operator', value: '>' }, { type: 'number', value: '3' } ] }, { type: 'number', value: '4', }, { type: 'NumberLiteral', value: '5', } ] }]}
基于结构化的树,可以很放标转换成类似下面的HTML形式,要知道对html的操作是非常方便的:
<div class="editor"> <span class="function">IF</span> <span>(</span> <span class="exception"> <span>VLOOKUP</span> <span>(</span> <span class="cell">A1</span> <span>,</span> <span class="ref">sheet1!</span> <span class="range">"sheet"!$A$1:$C$50</span> <span>,</span> <span class="number">1</span> <span>,</span> <span class="number">2</span> <span>)</span> <span class="operator">></span> <span class="number">3</span> </span> <span class="number">4</span> <span>,</span> <span class="number">5</span> <span>)</span></div>
最后为为上面的html内容添加样式,并为不同类型的元素绑定事件与处理逻辑,这样不仅可以对输入内容高亮,在点击函数、单元格时,还可以给出对应的提示信息或响应。
上面的内容仅仅是实现一个功能的思考与假设,实际的实现可能有所差异,但是实现的过程是类似的。
在我们使用的所有编程语言,比如Java、C、javascript等,我们都会编写文本格式的源代码,编译器或解释器会将源代码按照语言语法解析成对应的语法结构树,基于该结构一来可以实现语法检查、代码高亮等功能,同时针对特定代码块可以有很多其他操作。
通过上面的分析,可以看到核心要实现的是将文本内容解析成语法树的过程。在完成这部分功能的过程中,看到一个基于javascript实现的类似功能的例子the-super-tiny-compiler.js,通过几百行的代码,把这这一过程清晰地表现出来。
其过程主要包括下面几个步骤:
下面是具体的实现过程:
function tokenizer(input) { let current = 0; let tokens = []; while (current < input.length) { let char = input[current]; if (char === '(') { tokens.push({ type: 'paren', value: '(', }); current++; continue; } if (char === ')') { tokens.push({ type: 'paren', value: ')', }); current++; continue; } let WHITESPACE = //s/; if (WHITESPACE.test(char)) { current++; continue; } let NUMBERS = /[0-9]/; if (NUMBERS.test(char)) { let value = ''; while (NUMBERS.test(char)) { value += char; char = input[++current]; } tokens.push({ type: 'number', value }); continue; } if (char === '"') { let value = ''; char = input[++current]; while (char !== '"') { value += char; char = input[++current]; } char = input[++current]; tokens.push({ type: 'string', value }); continue; } let LETTERS = /[a-z]/i; if (LETTERS.test(char)) { let value = ''; while (LETTERS.test(char)) { value += char; char = input[++current]; } tokens.push({ type: 'name', value }); continue; } throw new TypeError('I dont know what this character is: ' + char); } return tokens;}
function parser(tokens) { let current = 0; function walk() { let token = tokens[current]; if (token.type === 'number') { current++; return { type: 'NumberLiteral', value: token.value, }; } if (token.type === 'string') { current++; return { type: 'StringLiteral', value: token.value, }; } if ( token.type === 'paren' && token.value === '(' ) { token = tokens[++current]; let node = { type: 'CallExpression', name: token.value, params: [], }; token = tokens[++current]; while ( (token.type !== 'paren') || (token.type === 'paren' && token.value !== ')') ) { node.params.push(walk()); token = tokens[current]; } current++; return node; } throw new TypeError(token.type); } let ast = { type: 'Program', body: [], }; while (current < tokens.length) { ast.body.push(walk()); } return ast;}
function traverser(ast, visitor) { function traverseArray(array, parent) { array.forEach(child => { traverseNode(child, parent); }); } function traverseNode(node, parent) { let methods = visitor[node.type]; if (methods && methods.enter) { methods.enter(node, parent); } switch (node.type) { case 'Program': traverseArray(node.body, node); break; case 'CallExpression': traverseArray(node.params, node); break; case 'NumberLiteral': case 'StringLiteral': break; default: throw new TypeError(node.type); } if (methods && methods.exit) { methods.exit(node, parent); } } traverseNode(ast, null);}
function transformer(ast) { let newAst = { type: 'Program', body: [], }; ast._context = newAst.body; traverser(ast, { NumberLiteral: { enter(node, parent) { parent._context.push({ type: 'NumberLiteral', value: node.value, }); }, }, StringLiteral: { enter(node, parent) { parent._context.push({ type: 'StringLiteral', value: node.value, }); }, }, CallExpression: { enter(node, parent) { let expression = { type: 'CallExpression', callee: { type: 'Identifier', name: node.name, }, arguments: [], }; node._context = expression.arguments; if (parent.type !== 'CallExpression') { expression = { type: 'ExpressionStatement', expression: expression, }; } parent._context.push(expression); }, } }); return newAst;}
function codeGenerator(node) { switch (node.type) { case 'Program': return node.body.map(codeGenerator) .join('/n'); case 'ExpressionStatement': return ( codeGenerator(node.expression) + ); case 'CallExpression': return ( codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator) .join(', ') + ')' ); case 'Identifier': return node.name; case 'NumberLiteral': return node.value; case 'StringLiteral': return '"' + node.value + '"'; default: throw new TypeError(node.type); }}
function compiler(input) { let tokens = tokenizer(input); let ast = parser(tokens); let newAst = transformer(ast); let output = codeGenerator(newAst); return output;}
const input = '(add 2 (subtract 4 2))';compiler(input);
感兴趣的可以查看相关的源代码,上面的解释比代码还多,逐行都有非常详细的描述。虽然只是一段简单的示例,但是将一段代码的编译过程都清晰地展现出来,对理解编译过程非常有帮助。像Vue React这样的框架,其编译过程也是基于这个思路,通过一系列的处理流程,最终生成带有语言特性的内容。
本篇文章主要基于目前遇到的的一个需求,结合自己分析与思考来了解需求的本质,最后通过一个javascript的示例,帮助我们理解这个过程。
本文链接:http://www.28at.com/showinfo-26-80883-0.html代码是如何被被编译的?
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 你们单测覆盖率是如何统计的?原理是什么?
下一篇: 如何仅使用CSS创建一个环形进度条?