您好,登錄后才能下訂單哦!
都知道瀏覽器和服務端是通過 HTTP 協議進行數據傳輸的,而 HTTP 協議又是純文本協議,那么瀏覽器在得到服務端傳輸過來的 HTML 字符串,是如何解析成真實的 DOM 元素的呢,也就是我們常說的生成 DOM Tree,最近了解到狀態機這樣一個概念,于是就萌生一個想法,實現一個 innerHTML 功能的函數,也算是小小的實踐一下。
函數原型
我們實現一個如下的函數,參數是 DOM 元素和 HTML 字符串,將 HTML 字符串轉換成真實的 DOM 元素且 append 在參數一傳入的 DOM 元素中。
function html(element, htmlString) { // 1. 詞法分析 // 2. 語法分析 // 3. 解釋執行 }
在上面的注釋我已經注明,這個步驟我們分成三個部分,分別是詞法分析、語法分析和解釋執行。
詞法分析
詞法分析是特別重要且核心的一部分,具體任務就是:把字符流變成 token 流。
詞法分析通常有兩種方案,一種是狀態機,一種是正則表達式,它們是等效的,選擇你喜歡的就好。我們這里選擇狀態機。
首先我們需要確定 token 種類,我們這里不考慮太復雜的情況,因為我們只對原理進行學習,不可能像瀏覽器那樣有強大的容錯能力。除了不考慮容錯之外,對于自閉合節點、注釋、CDATA 節點暫時均不做考慮。
接下來步入主題,假設我們有如下節點信息,我們會分出哪些 token 來呢。
<p class="a" data="js">測試元素</p>
對于上述節點信息,我們可以拆分出如下 token
狀態機的原理,將整個 HTML 字符串進行遍歷,每次讀取一個字符,都要進行一次決策(下一個字符處于哪個狀態),而且這個決策是和當前狀態有關的,這樣一來,讀取的過程就會得到一個又一個完整的 token,記錄到我們最終需要的 tokens 中。
萬事開頭難,我們首先要確定起初可能處于哪種狀態,也就是確定一個 start 函數,在這之前,對詞法分析類進行簡單的封裝,具體如下
function HTMLLexicalParser(htmlString, tokenHandler) { this.token = []; this.tokens = []; this.htmlString = htmlString this.tokenHandler = tokenHandler }
簡單解釋下上面的每個屬性
我們可以很容易的知道,字符串要么以普通文本開頭,要么以 < 開頭,因此 start 代碼如下
HTMLLexicalParser.prototype.start = function(c) { if(c === '<') { this.token.push(c) return this.tagState } else { return this.textState(c) } }
start 處理的比較簡單,如果是 < 字符,表示開始標簽或結束標簽,因此我們需要下一個字符信息才能確定到底是哪一類 token,所以返回 tagState 函數去進行再判斷,否則我們就認為是文本節點,返回文本狀態函數。
接下來分別展開 tagState 和 textState 函數。 tagState 根據下一個字符,判斷進入開始標簽狀態還是結束標簽狀態,如果是 / 表示是結束標簽,否則是開始標簽, textState 用來處理每一個文本節點字符,遇到 < 表示得到一個完整的文本節點 token,代碼如下
HTMLLexicalParser.prototype.tagState = function(c) { this.token.push(c) if(c === '/') { return this.endTagState } else { return this.startTagState } } HTMLLexicalParser.prototype.textState = function(c) { if(c === '<') { this.emitToken('text', this.token.join('')) this.token = [] return this.start(c) } else { this.token.push(c) return this.textState } }
這里初次見面的函數是 emitToken 、 startTagState 和 endTagState 。
emitToken 用來將產生的完整 token 存儲在 tokens 中,參數是 token 類型和值。
startTagState 用來處理開始標簽,這里有三種情形
邏輯上面說的比較清楚了,代碼也比較簡單,看看就好啦
HTMLLexicalParser.prototype.emitToken = function(type, value) { var res = { type, value } this.tokens.push(res) // 流式處理 this.tokenHandler && this.tokenHandler(res) }
HTMLLexicalParser.prototype.startTagState = function(c) { if(c.match(/[a-zA-Z]/)) { this.token.push(c.toLowerCase()) return this.startTagState } if(c === ' ') { this.emitToken('startTag', this.token.join('')) this.token = [] return this.attrState } if(c === '>') { this.emitToken('startTag', this.token.join('')) this.token = [] return this.start } }
HTMLLexicalParser.prototype.endTagState = function(c) { if(c.match(/[a-zA-Z]/)) { this.token.push(c.toLowerCase()) return this.endTagState } if(c === '>') { this.token.push(c) this.emitToken('endTag', this.token.join('')) this.token = [] return this.start } }
最后只有屬性標簽需要處理了,也就是上面看到的 attrState 函數,也處理三種情形
代碼如下
HTMLLexicalParser.prototype.attrState = function(c) { if(c.match(/[a-zA-Z'"=]/)) { this.token.push(c) return this.attrState } if(c === ' ') { this.emitToken('attr', this.token.join('')) this.token = [] return this.attrState } if(c === '>') { this.emitToken('attr', this.token.join('')) this.token = [] return this.start } }
最后我們提供一個 parse 解析函數,和可能用到的 getOutPut 函數來獲取結果即可,就不啰嗦了,上代碼
HTMLLexicalParser.prototype.parse = function() { var state = this.start; for(var c of this.htmlString.split('')) { state = state.bind(this)(c) } } HTMLLexicalParser.prototype.getOutPut = function() { return this.tokens }
接下來簡單測試一下,對于 <p class="a" data="js">
測試并列元素的</p><p class="a" data="js">
測試并列元素的</p> HTML 字符串,輸出結果為
看上去結果很 nice,接下來進入語法分析步驟
語法分析
首先們需要考慮到的情況有兩種,一種是有多個根元素的,一種是只有一個根元素的。
我們的節點有兩種類型,文本節點和正常節點,因此聲明兩個數據結構。
function Element(tagName) { this.tagName = tagName this.attr = {} this.childNodes = [] } function Text(value) { this.value = value || '' }
目標:將元素建立起父子關系,因為真實的 DOM 結構就是父子關系,這里我一開始實踐的時候,將 childNodes 屬性的處理放在了 startTag token 中,還給 Element 增加了 isEnd 屬性,實屬愚蠢,不但復雜化了,而且還很難實現。
仔細思考 DOM 結構,token 也是有順序的,合理利用棧數據結構,這個問題就變的簡單了,將 childNodes 處理放在 endTag 中處理。具體邏輯如下
代碼如下
function HTMLSyntacticalParser() { this.stack = [] this.stacks = [] } HTMLSyntacticalParser.prototype.getOutPut = function() { return this.stacks } // 一開始搞復雜了,合理利用基本數據結構真是一件很酷炫的事 HTMLSyntacticalParser.prototype.receiveInput = function(token) { var stack = this.stack if(token.type === 'startTag') { stack.push(new Element(token.value.substring(1))) } else if(token.type === 'attr') { var t = token.value.split('='), key = t[0], value = t[1].replace(/'|"/g, '') stack[stack.length - 1].attr[key] = value } else if(token.type === 'text') { if(stack.length) { stack[stack.length - 1].childNodes.push(new Text(token.value)) } else { this.stacks.push(new Text(token.value)) } } else if(token.type === 'endTag') { var parsedTag = stack.pop() if(stack.length) { stack[stack.length - 1].childNodes.push(parsedTag) } else { this.stacks.push(parsedTag) } } }
簡單測試如下:
沒啥大問題哈
解釋執行
對于上述語法分析的結果,可以理解成 vdom 結構了,接下來就是映射成真實的 DOM,這里其實比較簡單,用下遞歸即可,直接上代碼吧
function vdomToDom(array) { var res = [] for(let item of array) { res.push(handleDom(item)) } return res } function handleDom(item) { if(item instanceof Element) { var element = document.createElement(item.tagName) for(let key in item.attr) { element.setAttribute(key, item.attr[key]) } if(item.childNodes.length) { for(let i = 0; i < item.childNodes.length; i++) { element.appendChild(handleDom(item.childNodes[i])) } } return element } else if(item instanceof Text) { return document.createTextNode(item.value) } }
實現函數
上面三步驟完成后,來到了最后一步,實現最開始提出的函數
function html(element, htmlString) { // parseHTML var syntacticalParser = new HTMLSyntacticalParser() var lexicalParser = new HTMLLexicalParser(htmlString, syntacticalParser.receiveInput.bind(syntacticalParser)) lexicalParser.parse() var dom = vdomToDom(syntacticalParser.getOutPut()) var fragment = document.createDocumentFragment() dom.forEach(item => { fragment.appendChild(item) }) element.appendChild(fragment) }
三個不同情況的測試用例簡單測試下
html(document.getElementById('app'), '<p class="a" data="js">測試并列元素的</p><p class="a" data="js">測試并列元素的</p>') html(document.getElementById('app'), '測試<div>你好呀,我測試一下沒有深層元素的</div>') html(document.getElementById('app'), '<div class="div"><p class="p">測試一下嵌套很深的<span class="span">p的子元素</span></p><span>p同級別</span></div>')
聲明:簡單測試下都沒啥問題,本次實踐的目的是對 DOM 這一塊通過詞法分析和語法分析生成 DOM Tree 有一個基本的認識,所以細節問題肯定還是存在很多的。
總結
其實在了解了原理之后,這一塊代碼寫下來,并沒有太大的難度,但卻讓我很興奮,有兩個成果吧
代碼已經基本都列出來了,想跑一下的童鞋也可以 clone 這個 repo: domtree
總結
以上所述是小編給大家介紹的用原生 JS 實現 innerHTML 功能實例詳解,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網站的支持!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。