欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

用原生 JS 實現(xiàn) innerHTML 功能實例詳解

 更新時間:2019年04月03日 08:53:43   作者:劉新瓊  
這篇文章主要介紹了用原生 JS 實現(xiàn) innerHTML 功能,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下

都知道瀏覽器和服務(wù)端是通過 HTTP 協(xié)議進(jìn)行數(shù)據(jù)傳輸?shù)模?HTTP 協(xié)議又是純文本協(xié)議,那么瀏覽器在得到服務(wù)端傳輸過來的 HTML 字符串,是如何解析成真實的 DOM 元素的呢,也就是我們常說的生成 DOM Tree,最近了解到狀態(tài)機(jī)這樣一個概念,于是就萌生一個想法,實現(xiàn)一個 innerHTML 功能的函數(shù),也算是小小的實踐一下。

函數(shù)原型

我們實現(xiàn)一個如下的函數(shù),參數(shù)是 DOM 元素和 HTML 字符串,將 HTML 字符串轉(zhuǎn)換成真實的 DOM 元素且 append 在參數(shù)一傳入的 DOM 元素中。

function html(element, htmlString) {
  // 1. 詞法分析

  // 2. 語法分析

  // 3. 解釋執(zhí)行
}

在上面的注釋我已經(jīng)注明,這個步驟我們分成三個部分,分別是詞法分析、語法分析和解釋執(zhí)行。

詞法分析

詞法分析是特別重要且核心的一部分,具體任務(wù)就是:把字符流變成 token 流。

詞法分析通常有兩種方案,一種是狀態(tài)機(jī),一種是正則表達(dá)式,它們是等效的,選擇你喜歡的就好。我們這里選擇狀態(tài)機(jī)。

首先我們需要確定 token 種類,我們這里不考慮太復(fù)雜的情況,因為我們只對原理進(jìn)行學(xué)習(xí),不可能像瀏覽器那樣有強大的容錯能力。除了不考慮容錯之外,對于自閉合節(jié)點、注釋、CDATA 節(jié)點暫時均不做考慮。

接下來步入主題,假設(shè)我們有如下節(jié)點信息,我們會分出哪些 token 來呢。

<p class="a" data="js">測試元素</p>

對于上述節(jié)點信息,我們可以拆分出如下 token

  • 開始標(biāo)簽:<p
  • 屬性標(biāo)簽:class="a"
  • 文本節(jié)點:測試元素
  • 結(jié)束標(biāo)簽:</p>

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

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

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

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

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

我們可以很容易的知道,字符串要么以普通文本開頭,要么以 < 開頭,因此 start 代碼如下

HTMLLexicalParser.prototype.start = function(c) {
  if(c === '<') {
    this.token.push(c)
    return this.tagState
  } else {
    return this.textState(c)
  }
}

start 處理的比較簡單,如果是 < 字符,表示開始標(biāo)簽或結(jié)束標(biāo)簽,因此我們需要下一個字符信息才能確定到底是哪一類 token,所以返回 tagState 函數(shù)去進(jìn)行再判斷,否則我們就認(rèn)為是文本節(jié)點,返回文本狀態(tài)函數(shù)。

接下來分別展開 tagState 和 textState 函數(shù)。 tagState 根據(jù)下一個字符,判斷進(jìn)入開始標(biāo)簽狀態(tài)還是結(jié)束標(biāo)簽狀態(tài),如果是 / 表示是結(jié)束標(biāo)簽,否則是開始標(biāo)簽, textState 用來處理每一個文本節(jié)點字符,遇到 < 表示得到一個完整的文本節(jié)點 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
  }
}

這里初次見面的函數(shù)是 emitToken 、 startTagState 和 endTagState 。

emitToken 用來將產(chǎn)生的完整 token 存儲在 tokens 中,參數(shù)是 token 類型和值。

startTagState 用來處理開始標(biāo)簽,這里有三種情形

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

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

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

最后只有屬性標(biāo)簽需要處理了,也就是上面看到的 attrState 函數(shù),也處理三種情形

  • 如果是字母、單引號、雙引號、等號,則認(rèn)定為依舊處于屬性標(biāo)簽態(tài)
  • 如果遇到空格,則表示屬性標(biāo)簽態(tài)結(jié)束,接下來進(jìn)入新的屬性標(biāo)簽態(tài)
  • 如果遇到>,則認(rèn)定為屬性標(biāo)簽態(tài)結(jié)束,接下來開始新的節(jié)點信息

代碼如下

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 解析函數(shù),和可能用到的 getOutPut 函數(shù)來獲取結(jié)果即可,就不啰嗦了,上代碼

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

 

看上去結(jié)果很 nice,接下來進(jìn)入語法分析步驟

語法分析

首先們需要考慮到的情況有兩種,一種是有多個根元素的,一種是只有一個根元素的。

我們的節(jié)點有兩種類型,文本節(jié)點和正常節(jié)點,因此聲明兩個數(shù)據(jù)結(jié)構(gòu)。

function Element(tagName) {
  this.tagName = tagName
  this.attr = {}
  this.childNodes = []
}

function Text(value) {
  this.value = value || ''
}

目標(biāo):將元素建立起父子關(guān)系,因為真實的 DOM 結(jié)構(gòu)就是父子關(guān)系,這里我一開始實踐的時候,將 childNodes 屬性的處理放在了 startTag token 中,還給 Element 增加了 isEnd 屬性,實屬愚蠢,不但復(fù)雜化了,而且還很難實現(xiàn)。

仔細(xì)思考 DOM 結(jié)構(gòu),token 也是有順序的,合理利用棧數(shù)據(jù)結(jié)構(gòu),這個問題就變的簡單了,將 childNodes 處理放在 endTag 中處理。具體邏輯如下

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

代碼如下

function HTMLSyntacticalParser() {
  this.stack = []
  this.stacks = []
}
HTMLSyntacticalParser.prototype.getOutPut = function() {
  return this.stacks
}
// 一開始搞復(fù)雜了,合理利用基本數(shù)據(jù)結(jié)構(gòu)真是一件很酷炫的事
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)
    }
  }
}

簡單測試如下:

沒啥大問題哈

解釋執(zhí)行

對于上述語法分析的結(jié)果,可以理解成 vdom 結(jié)構(gòu)了,接下來就是映射成真實的 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)
  }
}

實現(xiàn)函數(shù)

上面三步驟完成后,來到了最后一步,實現(xiàn)最開始提出的函數(shù)

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 有一個基本的認(rèn)識,所以細(xì)節(jié)問題肯定還是存在很多的。

總結(jié)

其實在了解了原理之后,這一塊代碼寫下來,并沒有太大的難度,但卻讓我很興奮,有兩個成果吧

  • 了解并初步實踐了一下狀態(tài)機(jī)
  • 數(shù)據(jù)結(jié)構(gòu)的魅力

代碼已經(jīng)基本都列出來了,想跑一下的童鞋也可以 clone 這個 repo: domtree

總結(jié)

以上所述是小編給大家介紹的用原生 JS 實現(xiàn) innerHTML 功能實例詳解,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!

相關(guān)文章

  • three.js快速入門【推薦】

    three.js快速入門【推薦】

    本文主要介紹了three.js的基礎(chǔ)知識。具有一定的參考價值,下面跟著小編一起來看下吧
    2017-01-01
  • JavaScript實現(xiàn)京東秒殺效果

    JavaScript實現(xiàn)京東秒殺效果

    這篇文章主要為大家詳細(xì)介紹了JavaScript實現(xiàn)京東秒殺效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • Js利用Canvas實現(xiàn)圖片壓縮功能

    Js利用Canvas實現(xiàn)圖片壓縮功能

    下面小編就為大家?guī)硪黄狫s利用Canvas實現(xiàn)圖片壓縮功能。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-09-09
  • 在JavaScript中構(gòu)建ArrayList示例代碼

    在JavaScript中構(gòu)建ArrayList示例代碼

    這篇文章主要介紹了在JavaScript中構(gòu)建ArrayList,很實用,需要的朋友可以參考下
    2014-09-09
  • fullpage.js全屏滾動的具體使用方法

    fullpage.js全屏滾動的具體使用方法

    fullPage.js 是一個基于 jQuery 的插件,它能夠很方便、很輕松的制作出全屏網(wǎng)站,本文主要介紹了fullpage.js全屏滾動的具體使用方法,感興趣的可以了解一下
    2021-09-09
  • 深入理解JavaScript創(chuàng)建對象的多種方式以及優(yōu)缺點

    深入理解JavaScript創(chuàng)建對象的多種方式以及優(yōu)缺點

    本篇文章主要介紹了JavaScript創(chuàng)建對象的多種方式以及優(yōu)缺點,主要介紹了5種方式,有興趣的可以了解下
    2017-06-06
  • 淺析javascript的return語句

    淺析javascript的return語句

    這篇文章主要介紹了javascript的return語句,return語句在js中非常的重要,不僅僅具有返回函數(shù)值的功能,還具有一些特殊的用法,感興趣的小伙伴們可以參考一下
    2015-12-12
  • ES6 Generator函數(shù)的應(yīng)用實例分析

    ES6 Generator函數(shù)的應(yīng)用實例分析

    這篇文章主要介紹了ES6 Generator函數(shù)的應(yīng)用,結(jié)合實例形式分析了ES6 Generator函數(shù)異步操作與異常捕獲相關(guān)使用技巧,需要的朋友可以參考下
    2019-06-06
  • js中find、findIndex、indexOf的用法和區(qū)別

    js中find、findIndex、indexOf的用法和區(qū)別

    本文主要介紹了js中find、findIndex、indexOf的用法和區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07
  • JS兼容所有瀏覽器的DOMContentLoaded事件

    JS兼容所有瀏覽器的DOMContentLoaded事件

    這篇文章主要介紹了JS兼容所有瀏覽器的DOMContentLoaded事件的相關(guān)資料,標(biāo)準(zhǔn)瀏覽器中,使用DOMContentLoaded事件即可實現(xiàn)我們的要求,注冊事件處理函數(shù)也極為簡單,感興趣的朋友一起學(xué)習(xí)吧
    2018-01-01

最新評論