从Quill源码中学到的知识点

610 阅读4分钟

前言

最近一直在做Quill的项目,有时间也研究了一下Quill的源码,从中也学到一些知识,今天把这些知识总结下,算是对这些知识的巩固。

正文

事件

quill的api有提供3个事件,editor-changetext-changeselection-change

editor-change

editor-change总事件,就是触发text-changeselection-change事件之前要先触发editor-change,然后从里面再分发出去。

let args = [Emitter.events.SELECTION_CHANGE, clone(this.lastRange), clone(oldRange), source];
this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args); // 触发`editor-change`事件
if (source !== Emitter.sources.SILENT) {
this.emitter.emit(...args); // 再触发`selection-change`事件
}

可以看到触发SELECTION_CHANGE(也就是selection-change)事件之前,先触发 EDITOR_CHANGE事件,并把Emitter.events.SELECTION_CHANGE等参数传入。

selection-change

selection-change就是选择事件,只要你聚焦或者失焦,或者选择文本,就会触发该事件。

text-change

然后文本改变的时候会触发text-change事件。比如正常输入,工具栏设置格式,快捷键操作,只要来源于用户,就会触发。

那么是怎么监听这些操作的呢?

我去看了开发者工具的element的事件绑定,发现它们监听了以下事件

image.png

第一个是绑定的空函数,可以排除掉

this.domNode.addEventListener('DOMNodeInserted', function() {});

第二个和第三个这两个事件比较少用,查了一下资料,这里可以排除。

第四个keydown事件 是监听按键,设置格式,比如ctrl + B是加粗等等

第五个scroll事件,也可以排除掉。

那么可以得出结论,它不是通过监听事件触发的。

思索无果,只能打断点来调试一下了。

image.png

可以看到它的调用栈,它是有包装了一个modify方法,通过modify方法触发事件,然后继续看最前面的调用栈,终于发现了答案。

image.png

最前面的调用栈,可以看到是通过MutationObserver实现的。

下面总结下上面的知识点。

compositionstart/compositionend

这两个事件是文本合成事件,如果你输入中文的时候,一开始就会触发compositionstart事件,输入结束选择中文后,触发compositionend事件。

这两个事件在中文输入时非常实用,正常我们在input框是要监听input事件,但是你可以看看

image.png

触发input事件是实时输出,而不是选择了中文再输出。

如果我们使用了compositionstart/compositionend事件,就可以解决。

let composing = false
input.addEventListener('compositionstart',(e) =>{
    composing = true
})
input.addEventListener('compositionend',(e) =>{    
    composing = false
    console.log('compositionend',e.target.value)
})
input.addEventListener('input',(e) =>{  
    if(composing) return
    console.log('input', e.target.value)
})

compositionstart只会在刚输入中文时触发,这时可以设置个标志位,阻止input事件,然后在compositionend事件触发的时候,输出value。

因为compositionstart/compositionend事件只在输入中文时触发,如果输入字母,数字则不会触发,所以要配合input事件一起实现,可以拿到输入的value。

image.png

另外要注意⚠️:输入时先触发compositionstart事件,接着是input事件,再触发compositionend事件。

上面的图片input事件最后才从触发,是因为前面有return。

但是现在一般不需要这样写,因为现在都会用vue,它会在v-model的语法糖里做处理,拿到输入后的value再传给使用者。

  const vModelText = {
      created(el, { modifiers: { lazy, trim, number } }, vnode) {
          // ...省略部分代码
          if (!lazy) {
              addEventListener(el, 'compositionstart', onCompositionStart);
              addEventListener(el, 'compositionend', onCompositionEnd);
          }
      }
  };
  function onCompositionStart(e) {
      e.target.composing = true;
  }
  function onCompositionEnd(e) {
      const target = e.target;
      if (target.composing) {
          target.composing = false;
          target.dispatchEvent(new Event('input'));
      }
  }

MutationObserver

这个api是监听dom的变化的,比如属性变化,文本内容变化,子节点变化,都能监听到。

所以quill是使用了MutationObserver来触发事件。

而且,同时改变内容或者属性,不会触发多次,只会触发一次,然后以数组的形式返回变化的值。

语法:

const observer = new MutationObserver(callback)
observer.observe(dom, option) // 监听

dom为你监听的目标,option传对象,可以传:

  • childList 监听子节点变化,比如插入子节点,删除子节点
  • attributes 监听属性变化
  • characterData 监听文本内容变化, 监听元素节点不会触发,需要监听文本节点,可以配合textContent使用
  • subtree 所有后代节点的变化
  • attributeOldValue 是否监听attributes的旧值
  • characterDataOldValue 是否监听characterData的旧值
  • attributeFilter 监听特定的属性,数组形式,比如['id','class'],不填则监听所有属性

dom只要有变化,就会执行callback,参数有2个,一个是以数组形式的变化,一个是实例对象

举个例子,监听子节点变化

const observer =  new MutationObserver((mutationsList, observer) => {
    console.log(mutationsList, observer)
})
observer.observe($0, {
    childList: true
})

$0.innerHTML = '答案cp3' // 触发callback

监听dom文本变化

const observer =  new MutationObserver((mutationsList, observer) => {
    console.log(mutationsList, observer)
})
observer.observe($0.firstChild, {
    characterData: true
})

$0.firstChild.textContent = '答案cp3' // 触发callback

image.png

除了上面observe方法,还有disconnect方法,这个是断开监听方法,一旦断开,后面dom再改变就不会触发callback

observer.disconnect()

$0.innerHTML = 'cp3' // 不会触发callback

总结

看源码的过程虽然有时候很枯燥,但是也能学到很多东西,遇到不懂的地方,多去查查资料,记笔记,总结,很快你就会有进步。

继续加油⛽️