slate 系列 - 在编辑器中输入 A 发生了什么
提示
本文以一次最简单的文本输入为例,细究 slate 内部的执行逻辑。
光标先选中某个位置,触发 selectionchange 事件。在事件中,通过原生
getSelection方法获取到当前光标坐标 selection,再通过 slate-react 的ReactEditor.toSlateRange方法转换成 slate range,最后用 Transforms.select 将该 range 设置到editor.selection上。这里会执行 apply 的 set_selection 操作(该方法的触发位置在这里)。输入 A 时,触发 slate-react 中的 beforeinput 事件。当判断
event.inputType为insertText时,调用Editor.insertText方法插入文本。Editor.insertText内部会执行 apply 的 insert_text 操作。上述的 apply 都是对 Operation 的操作。Operation 处理完之后,会主动触发 onChange 事件,用于驱动 React 重新渲染。
这里有一个细节:当调用某个复杂命令时,可能会在一个 task 中执行多次 apply,这意味着
onChange也会被调用多次。slate 用了一个巧妙的方式来合并:把onChange放进 Promise 的回调里,当前 task 执行完后只触发一次。// https://github.com/ianstormtaylor/slate/blob/d2fc25c3c31453597f59cd2ac6ba087a1beb1fe3/packages/slate/src/create-editor.ts#L90 apply: () => { ... ... if (!FLUSHING.get(editor)) { FLUSHING.set(editor, true) Promise.resolve().then(() => { FLUSHING.set(editor, false) editor.onChange() editor.operations = [] }) } }- 这里涉及了执行栈和队列的知识。我们知道 JS 是单线程的,异步任务会被先放置到队列中,等同步任务执行完后再执行。
- 异步任务又分宏任务(task)和微任务(microtask)。浏览器会在每个 task 之间执行 DOM 重新渲染,即 task → 渲染 → task。
- 而微任务则会在第一个 task 完成之后、渲染之前执行。
Promise.then是微任务。所以 slate 多次执行的 apply 会把其中的Promise.then一一放进队列;当同步任务执行完,DOM 渲染之前,统一触发onChange。
onChange最终会在 React 组件中更新 state Hook,并触发 Context.Provider 的值变化,从而更新组件。
