從零實現一個React:Luster(一):JSX解析器

前言

這是以前在掘金髮的兩條沸點,懶得寫了,直接複製過來做爲前言了。而後這個項目可能以後還會繼續寫,增長一些路由或者模板引擎的指令什麼的,可是再過沒多久寒假就有大塊時間了就可能不摸這個魚去開其它坑了,隨緣吧。因此先寫JSX的解析器吧,這個部分也比較獨立html

掘金沸點裏有一些代碼截圖,就不發在markdown裏前端

算是利用期末考這段碎片時間摸一個水項目吧git

項目地址:github

  1. jsx-parser算法

  2. lusterjson

12.21

最近心情比較低落,摸魚也摸到恐慌,而後昨天就想着隨便寫點東西吧。而後就先選了用JavaScript寫,就順便想到了React。因此有了這個小破輪子,一個前端算是view層的框架吧,算是一個乞丐弱智版的React吧,只有兩百多行。數組

而後又想着居然都造輪子了,那乾脆JSX語法的轉譯也不用babel了,因此今天就摸了一個jsx的解析器,也只有兩百多行babel

算是一個學習的過程吧,雖然之後也不打算幹前端,也都看看markdown

反正也快期末考了,沒大塊時間了,就繼續摸這個項目吧,可能會再加上state和dom diff之類的吧,再作點創新?數據結構

代碼很水)不是前端)玩具而已)大佬輕噴)

12.22

繼上一條,這個乞丐版React昨天又增長了setState和dom-diff算法。成功的實現了功能,而後把代碼寫成了一坨💩,估計還有我還沒發現的bug。因此下面可能會稍微重構一下代碼,而後寫一下路由和模板引擎的指令?

這兩天可能去找找有沒有更好玩的能夠寫,不過這兩天最大的收穫就是清楚的瞭解了工整的代碼變成💩堆的過程

Jsx到JavaScript對象

其實這個JavaScript對象就是虛擬dom,最後咱們再根據這個虛擬dom進行渲染,後面的dom-diff也是根據這個數據結構來計算的。咱們解析器的目標就是把下面這一段JSX轉換成相應的JavaScript對象。

<div name="{{jsx-parse}}" class="{{fuck}}" id="1">
    Life is too difficult
    <span name="life" like="rape">
        <p>Life is like rape</p>
    </span> 
    <div>
        <span name="live" do="{{gofuck}}">
            <p>Looking away, everything is sad</p>
        </span> 
        <Counter me="excellent">
            I am awesome
        </Counter>
    </div>  
</div>
複製代碼
{
  "type": "div",
  "props": {
    "childrens": [
      {
        "type": "span",
        "props": {
          "childrens": [
            {
              "type": "p",
              "props": {
                "childrens": [],
                "text": "Life is like rape"
              }
            }
          ],
          "name": "life",
          "like": "rape"
        }
      },
      {
        "type": "div",
        "props": {
          "childrens": [
            {
              "type": "span",
              "props": {
                "childrens": [
                  {
                    "type": "p",
                    "props": {
                      "childrens": [],
                      "text": "Looking away, everything is sad"
                    }
                  }
                ],
                "name": "live",
                "do": "{{gofuck}}"
              }
            },
            {
              "type": "Counter",
              "props": {
                "childrens": [],
                "me": "excellent",
                "text": "I am awesome"
              }
            }
          ]
        }
      }
    ],
    "name": "{{jsx-parse}}",
    "class": "{{fuck}}",
    "id": "1",
    "text": "Life is too difficult"
  }
}
複製代碼

詞法分析

其實這個解析器一共也就是240多行,就只要簡單詞法分析,而後直接遞歸降低生成了

若是簡單的區分,Jsx裏,咱們也能夠說成html吧。就是就只有兩種token,開始標籤、結束標籤和文本,而後開始標籤裏面有各類屬性。

let token = {
    startTag: 'startTag',
    endTag: 'endTag',
    text: 'text',
    eof: 'eof'
}
複製代碼

詞法分析的主體邏輯就在lex()方法裏,其實這個對於以前寫的C語言的編譯器,一對比就很是簡單,沒有什麼狀況好考慮的

只有這幾種狀況:

  • 若是是<開頭的話,那只有兩種狀況,要麼是開始標籤,要麼是結束標籤,因此直接再進一步判斷有沒有斜槓就能夠知道是開始標籤仍是結束標籤
  • 像回車製表符這些直接跳過就能夠了
  • 若是是空格的話還須要判斷是否是在當前的文本里

而後就交由各個函數處理了

lex() {
    let text = ''
    while (true) {
        let t = this.advance()
        let token = ''
        switch (t) {
            case '<':
                if (this.lookAhead() === '/') {
                    token = this.handleEndTag()
                } else {
                    token = this.handleStartTag()
                }
                break
            case '\n':
                break
            case ' ':
                if (text != '') {
                    text += t
                } else {
                    break
                }
            case undefined:
                if (this.pos >= this.string.length) {
                    token = [this.token['eof'], 'eof', []]
                }
                break
            default:
                text += t
                token = this.handleTextTag(text)
                break
        }
        this.string = this.string.slice(this.pos)
        this.pos = 0
        if (token != '') {
            return token
        }
    }
}
複製代碼

處理開始標籤

處理開始標籤也很是簡單,比較複雜的是須要處理開始標籤裏的屬性

  • 首先是先處理標籤名
  • 而後是處理開始標籤裏的屬性
handleStartTag() {
    let idx = this.string.indexOf('>')
    if (idx == -1) {
        throw new Error('parse err! miss match '>'')
    }
    let str = this.string.slice(this.pos, idx)
    let s = ''
    if (str.includes(' ')) {
        s = this.string.split(' ').filter((str) => {
            return str != ''
        })[0]
    } else {
        s = this.string.split('>')[0]
    }
    let type = s.slice(1)
    this.pos += type.length
    let props = this.handlePropTag()
    this.advance()
    return [token.startTag, type, props]
}
複製代碼

處理開始標籤的屬性

處理屬性也很簡單,每個屬性的鍵值對都是用空格分隔的,因此直接用split獲取每一個鍵值對,最後返回一個鍵值對數組

這裏上面注意token返回的格式,開始標籤token的返回是一個數組,第一個元素是token類型,第二個元素是這個標籤的類型,第三個元素就是這個開始標籤的屬性

handlePropTag() {
    let idx = this.string.indexOf('>')
    if (idx == -1) {
        throw new Error('parse err! miss match '>'')
    }
    let string = this.string.slice(this.pos, idx)
    let pm = []
    if (string != ' ')  {
        let props = string.split(' ')
        pm = props.filter((props) => {
            return props != ''
        }).map((prop) => {
            let kv = prop.split('=')
            let o = {}
            o[kv[0]] = this.trimQuotes(kv[1])
            return o
        })
        this.pos += string.length
    }
    
    return pm
}
複製代碼

處理結束標籤

結束標籤很是簡單,直接進行字符串的切割就完事了

handleEndTag() {
    this.advance()
    let idx = this.string.indexOf('>')
    let type = this.string.slice(this.pos, idx)
    this.pos += type.length
    if (this.advance() != '>') {
        throw new Error('parse err! miss match '>'')
    }
    return [token.endTag, type, []]
}
複製代碼

處理文本節點

文本節點須要稍微處理一下,須要判斷後面的是否是<來判斷文本是否是結束了

handleTextTag(text) {
    let t = text.trim()
    if (this.lookAhead() == '<') {
        return [this.token['text'], t, []]
    } else {
        return ''
    }
}
複製代碼

語法分析生成JavaScript對象

這個過程其實就是一個遞歸降低的過程,若是碰到語法不正確的時間拋出異常就結束了

先定義一下這個JavaScript對象的結構,其實就和上面的json對象是一致的

class Jsx {
    constructor(type, props) {
        this.type = type
        this.props = props
    }
}
複製代碼

入口函數

  • 首先就是先拿到詞法分析傳過來的token的三個屬性
  • 而後就是根據不一樣的token類型調用不一樣的處理函數
parse() {
    this.currentToken = this.lexer.lex()
    let type = this.currentToken[0]
    let tag = this.currentToken[1]
    let props = this.mergeObj(this.currentToken[2])
    let func = this.parseMap[type]
    if (func != undefined) {
        func(tag, props)
    } else {
        this.parseMap['error']()
    }

    if (this.tags.length > 0) {
        throw new Error('parse error! Mismatched start and end tags')
    }

    return this.jsx
}
複製代碼

處理開始標籤

  • 首先開始先要判斷這個tags的長度,由於咱們能夠注意到咱們轉換的JavaScript對象實際上是一個嵌套結構,可是內部的結構並非很一致,因此就須要一些特殊處理。(這裏這樣寫不太好)
  • 最後把這個標籤名放到一個棧裏,這裏須要注意,由於jsx的標籤是能夠無限嵌套的,因此須要維護一個棧來判斷開始結束標籤是否匹配。
parseStart(tag, props) {
    let len = this.tags.length
    let jsx = this.jsx
    if (len >= 1) {
        for (let i = 0; i < len; i++) {
            if (len >= 2 && i >= 1) {
                jsx = jsx[jsx.length - 1]['props']['childrens']
            } else {
                jsx = jsx.props['childrens']
            }
        }
        this.currentJsx = new Jsx(tag, {
            'childrens': []
        })
        Object.assign(this.currentJsx['props'], props)
        jsx.push(this.currentJsx)
    } else {
        this.currentJsx = jsx = new Jsx(tag, {
            'childrens': []
        })
        Object.assign(jsx['props'], props)
        this.jsx = jsx
    }
    this.tags.push(tag)
    this.parse()
}
複製代碼

處理結束標籤

結束標籤的處理就很是簡單了,只要彈出對應的前一個開始標籤,用來後面判斷開始結束標籤是否匹配

parseEnd(tag) {
    if (tag == this.tags[this.tags.length - 1]) {
        this.tags.pop()
    }
    this.parse()
}
複製代碼

處理文本節點

處理文本節點就只要簡單的把對應的文本內容放到對象的childrens屬性中就能夠了

parseText(tag) {
    this.currentJsx['props']['text'] = tag
    this.parse()
}
複製代碼

小結

又水了一篇博客:)

這個系列的下一篇啥時候寫呢?我也不知道,先去摸會魚。看是否是去稍微重構一下這個項目的代碼,由於從一開始簡單的只有渲染功能,再到後面加入類組件、setState、dom-diff後代碼就變成了XXX了,雖然寫的時候知道這樣很差,可是仍是想偷懶,因此如今就看看能不能改一改了

相關文章
相關標籤/搜索