这是以前在掘金发的两条沸点,懒得写了,直接复制过来做为前言了。而后这个项目可能以后还会继续写,增长一些路由或者模板引擎的指令什么的,可是再过没多久寒假就有大块时间了就可能不摸这个鱼去开其它坑了,随缘吧。因此先写JSX的解析器吧,这个部分也比较独立html
掘金沸点里有一些代码截图,就不发在markdown里前端
算是利用期末考这段碎片时间摸一个水项目吧git
项目地址:github
lusterjson
最近心情比较低落,摸鱼也摸到恐慌,而后昨天就想着随便写点东西吧。而后就先选了用JavaScript写,就顺便想到了React。因此有了这个小破轮子,一个前端算是view层的框架吧,算是一个乞丐弱智版的React吧,只有两百多行。数组
而后又想着居然都造轮子了,那干脆JSX语法的转译也不用babel了,因此今天就摸了一个jsx的解析器,也只有两百多行babel
算是一个学习的过程吧,虽然之后也不打算干前端,也都看看markdown
反正也快期末考了,没大块时间了,就继续摸这个项目吧,可能会再加上state和dom diff之类的吧,再作点创新?数据结构
代码很水)不是前端)玩具而已)大佬轻喷)
继上一条,这个乞丐版React昨天又增长了setState和dom-diff算法。成功的实现了功能,而后把代码写成了一坨💩,估计还有我还没发现的bug。因此下面可能会稍微重构一下代码,而后写一下路由和模板引擎的指令?
这两天可能去找找有没有更好玩的能够写,不过这两天最大的收获就是清楚的了解了工整的代码变成💩堆的过程
其实这个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对象的结构,其实就和上面的json对象是一致的
class Jsx { constructor(type, props) { this.type = type this.props = props } } 复制代码
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 } 复制代码
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了,虽然写的时候知道这样很差,可是仍是想偷懒,因此如今就看看能不能改一改了