Quill编辑器的文本查找与替换

100 阅读5分钟

文本查找

将编辑器内的文本提取出来,使用正则匹配出搜索内容的位置,然后标记上即可。

  const reg = new RegExp('搜索的内容', 'g')
  const text = quill.getText()
  let matches = [...text.matchAll(reg)]
  if (matches.length > 0) {
    const positions = matches.map(match => match.index)
    // 标记查找的内容
    // 通过位置索引,将该位置的内容背景颜色进行更改
  }
(1)正则表达式问题

const reg = new RegExp('搜索的内容', 'g'),这样子写有个问题,如果搜索的内容包含+等正则匹配规则相关的字符时,查询会出现问题。因为new RegExp第一个参数为字符串时,则会按照字符串模式进行构造,其中会将+ . *等符号作为正则匹配规则解析出来,而不会仅视为一个字符。

解决方法,将特殊符号进行转义

'搜索的内容'.replace(/[.*+?^${}()|[]\]/g, '\$&')
(2)getText问题

在quill编辑器中,getText只获取文本信息。但编辑器中可能会包含图片、视频等元素,在位置索引时,这些元素会额外占用一个索引位置。如下图,文字4的记录索引为3而实际索引为4。

image.png

因此需要进行额外的处理。

在quill编辑器中,采取了Delta的格式存储文本的内容和样式。

[ 
    { insert: 'Gandalf', attributes: { bold: true } }, 
    { insert: ' the ' }, 
    { insert: 'Grey', attributes: { color: '#cccccc' } } 
]

如果内容是图片、视频等非文字元素,其存储的Delta块中,insert中对应的不是字符串。因此,可以通过quill编辑器的getContents方法,获取编辑器中的所有Delta块,逐个Delta块判断是否为字符串,若不是,可以取巧地将其替换为\n字符串,重新将文本内容进行拼接,得到一个二次处理的全文文本内容。

image.png

如此,在记录查询内容位置的时候,便可以使用\n对图片、视频等元素进行占位,使得内容索引准确。

(3)标记查找的内容

看了一下quill的源码,发现它的getFormat方法,它只会获取第一个Blot元素的样式。

let formatsArr = [lines, leaves].map(function(blots) {
  if (blots.length === 0) return {};
  let formats = bubbleFormats(blots.shift());
  while (Object.keys(formats).length > 0) {
    let blot = blots.shift();
    if (blot == null) return formats;
    formats = combineFormats(bubbleFormats(blot), formats);
  }
  return formats;
});

这会导致一个问题,如果需要标记的内容为多个Blot元素,那么后续的Blot的样式将会无法获取。

为了解决这个问题,需要逐个Blot存储样式,恢复样式时,再逐个恢复回去。

文本替换

这个根据需要替换元素的索引,进行对应的文本删除以及被替换文本的插入即可。

(1)多个文本替换问题

多个查找内容进行替换的时候,需要注意,替换的时候,后续需要替换的内容的位置需要进行更新。

如文本:你好啊,你好!

你好替换为你们好,在替换第二个你好的时候,该你好的位置索引需要进行更新+1。

任务切片进行优化

文本查找与替换时,会进行元素的标记与取消标记,而当文本篇幅很大时或者查找到的元素很多时,频繁地调用getFormatformatText进行样式获取以及设置,会导致页面的严重卡顿。

检查一番,发现与浏览器的事件循环机制相关。

事件循环执行流程
  • 执行栈选择队列中的第一个宏任务,执行它的同步代码直至结束。
  • 检查是否有微任务,若有,则执行所有队列中的微任务。
  • 渲染页面。
  • 开始下一轮的事件循环,执行宏任务中的异步代码。
宏任务与微任务
  • 宏任务,一般由浏览器或Node.js发起,包括了setTimeout、setInterval、I/O操作等。
  • 微任务,一般由JavaScript发起,包括了Promise的回调函数、nextTick函数等。
任务切片

任务切片是将运行时间长的任务,拆分为多个运行时间短的任务。

function test() {
    for(let i=0;i<100000;i++){
        console.log(i)
    }
}
document.getElementsByTagName("button")[0].addEventListener("click",test)

image.png

在其中,会发现,该任务占用了较长的时间,且该时间内难以进行其它的交互,页面也很卡顿。因为事件循环一直在执行长任务。

这和前面说的卡顿是一致的,当我们调用的样式设置函数执行过多次时:

  • 事件循环因为宏任务还未执行完成,用户难以进行其它的交互操作,造成卡顿
  • 同样的,样式的更新,即浏览器新页面的渲染也会因为此而延迟,可能会导致等到所有函数调用完后,才一次性进行页面的渲染。

那么,我们可以考虑使用setTimeout实现任务的切片。

  // 使用setTimeout将其拆分为多个任务即可

image.png

如此,发现任务被拆分为多个小任务,同时页面的交互也不显得卡顿。

回到前面说的quill编辑器问题,尝试将setTimeout加入其中,发现渲染卡顿问题基本解决了。

requestAnimationFrame

除此之外,也可以使用requestAnimationFrame这个函数来替代setTimeout方法。

  • 调用 requestAnimationFrame,可以告诉浏览器希望执行一个动画,并要求浏览器在下次重绘之前调用指定的回调函数来更新动画。
  • 需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
  • 如果希望在浏览器下一次重绘之前继续更新下一帧动画,回调函数必须再次调用 requestAnimationFrame()。这个方法是一次性的,适用于准备更新屏幕动画的场景。

前面提到的重绘等浏览器渲染相关的内容,这篇文章有详细的说明:juejin.cn/post/684490…