从零实现一个React:Luster(一):JSX解析器

前言

这是以前在掘金发的两条沸点,懒得写了,直接复制过来做为前言了。而后这个项目可能以后还会继续写,增长一些路由或者模板引擎的指令什么的,可是再过没多久寒假就有大块时间了就可能不摸这个鱼去开其它坑了,随缘吧。因此先写JSX的解析器吧,这个部分也比较独立javascript

掘金沸点里有一些代码截图,就不发在markdown里html

算是利用期末考这段碎片时间摸一个水项目吧前端

项目地址:java

  1. jsx-parsergit

  2. lustergithub

12.21

最近心情比较低落,摸鱼也摸到恐慌,而后昨天就想着随便写点东西吧。而后就先选了用JavaScript写,就顺便想到了React。因此有了这个小破轮子,一个前端算是view层的框架吧,算是一个乞丐弱智版的React吧,只有两百多行。算法

而后又想着居然都造轮子了,那干脆JSX语法的转译也不用babel了,因此今天就摸了一个jsx的解析器,也只有两百多行json

算是一个学习的过程吧,虽然之后也不打算干前端,也都看看数组

反正也快期末考了,没大块时间了,就继续摸这个项目吧,可能会再加上state和dom diff之类的吧,再作点创新?babel

代码很水)不是前端)玩具而已)大佬轻喷)

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了,虽然写的时候知道这样很差,可是仍是想偷懒,因此如今就看看能不能改一改了

相关文章
相关标签/搜索