最近需要寫一個編輯擴展組件,主要功能類似于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.www897cc.com/showinfo-26-80883-0.html代碼是如何被被編譯的?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 你們單測覆蓋率是如何統計的?原理是什么?
下一篇: 如何僅使用CSS創建一個環形進度條?