中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

用原生 JS 實現 innerHTML 功能實例詳解

發布時間:2020-09-01 06:16:17 來源:腳本之家 閱讀:202 作者:劉新瓊 欄目:web開發

都知道瀏覽器和服務端是通過 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

  • 開始標簽:<p
  • 屬性標簽:class="a"
  • 文本節點:測試元素
  • 結束標簽:</p>

狀態機的原理,將整個 HTML 字符串進行遍歷,每次讀取一個字符,都要進行一次決策(下一個字符處于哪個狀態),而且這個決策是和當前狀態有關的,這樣一來,讀取的過程就會得到一個又一個完整的 token,記錄到我們最終需要的 tokens 中。

萬事開頭難,我們首先要確定起初可能處于哪種狀態,也就是確定一個 start 函數,在這之前,對詞法分析類進行簡單的封裝,具體如下

function HTMLLexicalParser(htmlString, tokenHandler) {
  this.token = [];
  this.tokens = [];
  this.htmlString = htmlString
  this.tokenHandler = tokenHandler
}

簡單解釋下上面的每個屬性

  • token:token 的每個字符
  • tokens:存儲一個個已經得到的 token
  • htmlString:待處理字符串
  • tokenHandler:token 處理函數,我們每得到一個 token 時,就已經可以進行流式解析

我們可以很容易的知道,字符串要么以普通文本開頭,要么以 < 開頭,因此 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 用來處理開始標簽,這里有三種情形

  • 如果接下來的字符是字母,則認定依舊處于開始標簽態
  • 遇到空格,則認定開始標簽態結束,接下來是處理屬性了
  • 遇到>,同樣認定為開始標簽態結束,但接下來是處理新的節點信息
  • endTagState用來處理結束標簽,結束標簽不存在屬性,因此只有兩種情形
  • 如果接下來的字符是字母,則認定依舊處于結束標簽態
  • 遇到>,同樣認定為結束標簽態結束,但接下來是處理新的節點信息

邏輯上面說的比較清楚了,代碼也比較簡單,看看就好啦

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 字符串,輸出結果為

 用原生 JS 實現 innerHTML 功能實例詳解

看上去結果很 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 中處理。具體邏輯如下

  • 如果是 startTag token,直接 push 一個新 element
  • 如果是 endTag token,則表示當前節點處理完成,此時出棧一個節點,同時將該節點歸入棧頂元素節點的 childNodes 屬性,這里需要做個判斷,如果出棧之后棧空了,表示整個節點處理完成,考慮到可能有平行元素,將元素 push 到 stacks。
  • 如果是 attr token,直接寫入棧頂元素的 attr 屬性
  • 如果是 text token,由于文本節點的特殊性,不存在有子節點、屬性等,就認定為處理完成。這里需要做個判斷,因為文本節點可能是根級別的,判斷是否存在棧頂元素,如果存在直接壓入棧頂元素的 childNodes 屬性,不存在 push 到 stacks。

代碼如下

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)
    }
  }
}

簡單測試如下:

用原生 JS 實現 innerHTML 功能實例詳解

沒啥大問題哈

解釋執行

對于上述語法分析的結果,可以理解成 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 功能實例詳解,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網站的支持!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

井冈山市| 周宁县| 永济市| 微山县| 桐城市| 阿尔山市| 长治市| 绥江县| 永城市| 青岛市| 东乌| 东海县| 张家口市| 洛隆县| 彰武县| 简阳市| 扎鲁特旗| 定西市| 库伦旗| 阜宁县| 虞城县| 广元市| 交口县| 湖州市| 江山市| 城步| 五家渠市| 普兰店市| 道真| 和硕县| 郁南县| 修文县| 安义县| 林西县| 贵港市| 卫辉市| 容城县| 醴陵市| 秦皇岛市| 芦山县| 关岭|