Reactjs开发自制编程语言Monkey的编译器:高能技术干货之语法高亮2

上一节,我们利用词法解析器加上观察者模式,实现了代码语句的抽取关键字功能,对于给定代码:

<div><text>let five = 5; let six = 6; let seven = 7;</text></div>

MonkeyCompilerEditer把div节点里面的内容提交给MonkeyLexer,然后通过回调函数notifyTokenCreation获得了关键字对应的token对象,以及关键字字符串的起始和结束位置,并把相关信息存储到队列keyWordElementArray。例如上面的语句提交给MonkeyLexer后,编辑器对象的notifyTokenCreation会被调用若干次,同时三个关键字”let”对应的字符串起始和结束位置会被记录下来,这些位置将会用来对代码语句进行切分。

第一个关键字let的起始位置是0,于是我们把语句从开始到关键字起始位置之间的内容抽取出来,构造一个text节点,由于第一个关键字的起始位置就是语句的起始位置,所以我们先构造一个空的text节点:

<text></text>

然后我们把关键字let构造一个含有span标签的节点:

<span style="color:green">let</span>

第一个let关键字的结束位置是4,第二个关键字let的起始位置是15,因此我们把4到14之间的字符合在一起构造成一个text节点:

<text> five = 5; </text>

然后把第二个关键字单独构建成一个含有span标签的节点:

<span style="color:green">let</span>

第二个let关键字的结束位置是18,第三个关键字let的起始位置是28,所以我们把18到27之间的字符合在一起形成一个text节点:

<text> six = 6; </text>

然后把第三个关键字let单独构建成一个含有span标签的节点:

<span style="color:green">let</span>

第三个关键字let的结束位置为31,于是我们把32开始到字符串末尾之间的字符合成一个text节点:

<text> seven = 7;</text>

接着我们把上面新生成的节点调用DOM API insertBefore全部插入到div节点之下:

<div>
<text></text>
<span style="color:green">let</span>
<text> five = 5; </text>
<span style="color:green">let</span>
<text> six = 6; </text>
<span style="color:green">let</span>
<text> seven = 7;</text>
<text>let five = 5; let six = 6; let seven = 7;</text>
</div>

最后我们再把最后一个text节点给删除,得到下面的html代码就具备了关键字高亮效果:

<div>
<text></text>
<span style="color:green">let</span>
<text> five = 5; </text>
<span style="color:green">let</span>
<text> six = 6; </text>
<span style="color:green">let</span>
<text> seven = 7;</text>
</div>

我们看看上面算法的代码实现,在MonkeyCompilerEditer.js中,添加如下代码:

hightLightKeyWord(token,elementNode,begin,end) {
        var strBefore = elementNode.data.substr(this.lastBegin,begin - this.lastBegin)
        strBefore = this.changeSpaceToNBSP(strBefore)

        var textNode = document.createTextNode(strBefore)
        var parentNode = elementNode.parentNode
        parentNode.insertBefore(textNode,elementNode)


        var span = document.createElement('span')
        span.style.color = 'green'
        span.classList.add(this.keyWordClass)
        span.appendChild(document.createTextNode(token.getLiteral()))
        parentNode.insertBefore(span,elementNode)

        this.lastBegin = end - 1

        elementNode.keyWordCount--
        console.log(this.divInstance.innerHTML)
    }

changeSpaceToNBSP(str) {
        var s = ""
        for (var i = 0; i < str.length; i++) {
            if (str[i] === ' ') {
                s += '\u00a0'
            }
            else {
                s += str[i]
            }
        }

        return s;
    }
hightLightSyntax() {
        var i
        for (i = 0; i < this.keyWordElementArray.length; i++) {
            var e = this.keyWordElementArray[i]
            this.currentElement = e.node
            this.hightLightKeyWord(e.token,e.node,e.begin,e.end)

            if (this.currentElement.keyWordCount === 0) {
                var end = this.currentElement.data.length
                var lastText = this.currentElement.data.substr(this.lastBegin,end)
                lastText = this.changeSpaceToNBSP(lastText)
                var parent = this.currentElement.parentNode
                var lastNode = document.createTextNode(lastText)
                parent.insertBefore(lastNode,this.currentElement)
                parent.removeChild(this.currentElement)
            }
        }
        this.keyWordElementArray = []
    }

我们先看最后一个函数hightLightSyntax,它的if (this.currentElement.keyWordCount === 0)判断里面的代码做的操作就是我们前面算法的最后一步,把最后一个text节点从div中删除。在for循环中,它从keyWordArray中取出回调函数存入的关键字信息,然后调用hightLightKeyWord函数,这个函数的作用就是前面描述算法步骤中,根据关键字的起始和结束位置切割代码字符串,并生成不同节点的过程。

我们看看hightLightKeyWord函数的实现逻辑。传进来的参数begin代表关键字字符串的起始位置,end代表关键字字符串的结束位置。this.lastBegin一开始初始化为0,用来表示代码字符串的起始位置。

var strBefore = elementNode.data.substr(this.lastBegin,begin - this.lastBegin)
strBefore = this.changeSpaceToNBSP(strBefore)

var textNode = document.createTextNode(strBefore)
var parentNode = elementNode.parentNode
parentNode.insertBefore(textNode,elementNode)

上面代码作用是,把关键字起始位置之前的所有字符抽出来形成一个字符串strBefore,然后调用DOM API createTextNode构建一个text节点,然后再插入div节点作为它的子节点。这里有个函数需要强调就是changeSpaceToNBSP,当用字符串构建text节点时,如果字符串中有空格,那么构建处理的text节点,里面的字符串会自动把空格删掉,例如字符串:

five = 5;

如果构建text节点的话,中间两个空格会被删掉,变成:

<text>five=5;</text>

这样一来,字符再跟原有显示就跟原来不一样了,为了保持字符串的原有样貌,我们必须保留空格,处理这个问题的办法是,把空格转换成UNICODE空格编码’\u00a0’,这样当页面显示字符串时,当浏览器读取到编码’\u00a0’,它就知道这里是个空格,因此把字符串显示在页面上时,原有空格就会得以保留。

var span = document.createElement('span')
span.style.color = 'green'
span.classList.add(this.keyWordClass)
span.appendChild(document.createTextNode(token.getLiteral()))
parentNode.insertBefore(span,elementNode)

上面这部分代码的作用是为关键字字符串添加span标签,使得它在页面上展示时呈现出高亮的绿色。

this.lastBegin = end - 1
elementNode.keyWordCount--

上面代码作用是把lastBegin设置成当前字符串的结束位置减去1,那么处理下个关键字字符串时,就可以把当前字符串结尾直到下一个关键字开始位置之间的字符集合起来形成一个字符串,以便生成下一个text节点。

上面代码逻辑不好理解,请参看视频中的代码解读和调试过程来加深理解:
更详细的讲解和代码调试演示过程,请点击链接

由于语法高亮是即时显示的,对于关键字”let”,当用户敲下前两个字符”le”时,字符串还是黑色,一旦第三个字符’t’敲下之后,整个字符串就需要立马转换成绿色,为了即时性,我们必须在用户每次敲击键盘后,就立马解析当前代码,实现关键字高亮,所以我们需要在代码中监听键盘点击事件,于是需要继续添加如下代码,在MonkeyCompilerEditer.js中:

onDivContentChane(evt) {
        if (evt.key === 'Enter' || evt.key === " ") {
            return
        }

        var bookmark = undefined
        if (evt.key !== 'Enter') {
            bookmark = rangy.getSelection().getBookmark(this.divInstance)
        }

        var spans = document.getElementsByClassName(this.keyWordClass);
        while (spans.length) {
            var p = spans[0].parentNode;
            var t = document.createTextNode(spans[0].innerText)
            p.insertBefore(t,spans[0])
            p.removeChild(spans[0])
        }

        //把所有相邻的text node 合并成一个
        this.divInstance.normalize();
        this.changeNode(this.divInstance)
        this.hightLightSyntax()

        if (evt.key !== 'Enter') {
            rangy.getSelection().moveToBookmark(bookmark)
        }

    }

    render() {
        let textAreaStyle = {
            height: 480,border: "1px solid black"
        };

        return (
            <div style={textAreaStyle} 
            onKeyUp={this.onDivContentChane.bind(this)}
            ref = {(ref) => {this.divInstance = ref}}
            contentEditable>
            </div>
            );
    }

在render函数返回的jsx中,我们在div控件中添加了onKeyUp消息的响应,一旦用户点击键盘后,组件的onDivContentChane就会被调用。在onDivContentChane中,它先判断当前用户按下哪些按键,如果是回车或是空格,那么直接返回。在该函数中,使用到了一个外部控件叫rangy,这是google开发的一个组件,它的作用是记录当前光标所在位置。我们实现语法高亮,其实是通过改变页面的html代码结构实现的。但这会带来一个问题,假设用户在编辑框里敲下三个字符”let”,此时光标会在字符t的后面闪烁,当实现高亮时,我们会在html中,给字符串”let”的前后分别加上标签

<span style="color:green"></span>

一旦内部html代码发生改变后,附带的一个效果是,光标会返回到字符串的开头去,如果每次实现关键字高亮时,光标总是从当前输入位置返回到开头,那对用户来说是不堪忍受的,因此我们使用rangy组件来保证内部html代码改变后,光标能够回到原来所在的位置,所以代码:

var bookmark = undefined
        if (evt.key !== 'Enter') {
            bookmark = rangy.getSelection().getBookmark(this.divInstance)
        }

其作用是先记录当前光标所在的位置。后面对应代码:

if (evt.key !== 'Enter') { rangy.getSelection().moveToBookmark(bookmark) }

它的作用是,当实现语法高亮后,把光标返回到原来所在的位置。rangy组件的获取可以在当前项目路径下,通过控制台执行下面命令:

npm install rangy

接着看余下的代码:

var spans = document.getElementsByClassName(this.keyWordClass);
while (spans.length) {
        var p = spans[0].parentNode;
        var t = document.createTextNode(spans[0].innerText)
        p.insertBefore(t,spans[0])
        p.removeChild(spans[0])
}

this.keyWordClass 被初始化为字符串”keyword”,上面代码的作用是,找到所有class属性为”keyword”的节点。我们每次在关键字前添加span节点时,都会给这个节点赋予一个class属性叫”keyword”,例如:

<span class="keyword" sytle="color:green">if</span>

上面代码把所有带有”keyword”属性的span节点找出来,并把这些节点删除掉。这么做是因为,当用户敲下第二个关键字时,第一个关键字就已经是高亮状态了,假设第一个关键字是”if”,第二个关键字是else,那么当前html代码如下:

<span class="keyword" sytle="color:green">if</span><text>&nbsp;</text><text>else</text>

此时第二个关键字”else”还没有高亮,我们实现关键字高亮的策略是查找所有关键字字符串,并把他们包裹在”span”标签中,如果不事先把已经存在的span标签删除的话,那么就会出现一个关键字间套多个span标签的情况,于是上面的html代码在完成关键字高亮流程后会变成:

```
<span class="keyword" sytle="color:green"><span class="keyword" sytle="color:green">if</span></span><text>&nbsp;</text><span class="keyword" sytle="color:green">else</span>

于是第一个关键字if就包含在两个span标签中,这是不必要的。所以代码片段中的while把所有已经存在的span标签去除掉,把html转换成只包含text标签,于是例子中的html代码经过while这段代码的处理后变成如下情况:

<text>if</text><text>&nbsp;</text><text>else</text>

接着的语句this.divInstance.normalize() 把所有相连的text节点合成一个,于是上面的html代码就变成:

<text>if&nbsp;else</text>

接着调用this.changeNode(this.divInstance)就开始了使用词法解析器抽取关键字的流程,changeNode函数需要分析一下。

changeNode(n) {
      var f = n.childNodes; 
      for(var c in f) {
        this.changeNode(f[c]);
      }
      if (n.data) {
        console.log(n.parentNode.innerHTML)
        this.lastBegin = 0
        n.keyWordCount = 0;
        var lexer = new MonkeyLexer(n.data)
        lexer.setLexingOberver(this,n)
        lexer.lexing()
      } 
    }

它包含着递归调用的逻辑,n是父节点,通过n.childNodes找到所有子节点,然后分别对每个子节点调用changeNode函数,直到某个子节点的data属性不为空为止,先看下面这段html代码:

<div>
  <div>
    <div><text>let</text></div>
  </div>
</div>

上面html代码中,div有三层箭头,其中只有最里面的div是含有字符串的,也就是最里面的div它的data属性才不是空。changeNode会先找到最外层的div节点,然后通过childNodes找到第二层div节点,然后再次递归找到最里面第三层的div节点,这时候找到的div节点,它的data属性才包含了可供处理的有效字符串。

至此,整个即时性关键字语法高亮的算法逻辑和实现过程就解析完毕了,如果配合视频,理解起来会更容易一些。

更详细的讲解和代码调试演示过程,请点击链接

关键字即时高亮是一种技术难度不小的功能点,如果你用搜索引擎查找的话,你会发现有一个专门的插件叫Prim是专门用来实现这个功能的。原本我也想直接使用这个插件实现高亮功能,这样省事,但考虑到技术能力的真正提高,是需要足够的编码和思考设计才能得以实现,因此就自己从头到尾做一次。如果谁能够从头到尾跟着完成这个功能点,那么他的数据结构和算法能力,设计模式能力,DOM 树状模型的深入理解能力,都会得到相当程度的提升。

当前关键字高亮算法存在一个大问题是效率低,每当用户输入一个字符,所有的代码就都得全部进行词法解析,然后再把整个内部html改造一遍,如果编辑框中的代码很多的话,这么做是很浪费资源的,一个改进办法是,当用户输入时,我们把用户输入的所在行拿出来解析就好,没必要把编辑框里所有内容都拿出来解析。

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom