Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: reduce re-rendering #353

Merged
merged 2 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/bright-eggs-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@wangeditor-next/core": patch
"@wangeditor-next/editor": patch
---

perf: reduce re-rendering
87 changes: 70 additions & 17 deletions packages/core/src/text-area/update-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
* @author wangfupeng
*/

import { Descendant, Range } from 'slate'
import { h, VNode } from 'snabbdom'

import { IDomEditor } from '../editor/interface'
import TextArea from './TextArea'
import { genPatchFn, normalizeVnodeData } from '../utils/vdom'
import $, { Dom7Array, getDefaultView, getElementById } from '../utils/dom'
import { node2Vnode } from '../render/node2Vnode'
import $, { Dom7Array, getDefaultView, getElementById } from '../utils/dom'
import { genPatchFn, normalizeVnodeData } from '../utils/vdom'
import {
EDITOR_TO_ELEMENT,
EDITOR_TO_WINDOW,
ELEMENT_TO_NODE,
IS_FIRST_PATCH,
NODE_TO_ELEMENT,
TEXTAREA_TO_PATCH_FN,
TEXTAREA_TO_SELECTION,
TEXTAREA_TO_VNODE,
EDITOR_TO_ELEMENT,
NODE_TO_ELEMENT,
ELEMENT_TO_NODE,
EDITOR_TO_WINDOW,
} from '../utils/weak-maps'
import TextArea from './TextArea'

function genElemId(id: number) {
return `w-e-textarea-${id}`
Expand All @@ -31,7 +34,7 @@ function genElemId(id: number) {
function genRootVnode(elemId: string, readOnly = false): VNode {
return h(`div#${elemId}`, {
props: {
contentEditable: readOnly ? false : true,
contentEditable: !readOnly,
},
})
// 其他属性在 genRootElem 中定,这里不用重复写
Expand All @@ -42,7 +45,7 @@ function genRootVnode(elemId: string, readOnly = false): VNode {
* @param elemId elemId
* @param readOnly readOnly
*/
function genRootElem(elemId: string, readOnly = false): Dom7Array {
function genRootElem(elemId: string, _readOnly = false): Dom7Array {
const $elem = $(`<div
id="${elemId}"
data-slate-editor
Expand All @@ -59,6 +62,32 @@ function genRootElem(elemId: string, readOnly = false): Dom7Array {
return $elem
}

function diffBySelection(
prevVnode: VNode,
content: Descendant[],
editor: IDomEditor,
): VNode[] {
const selection = editor.selection

if (!selection) {
return prevVnode.children as VNode[]
}

const { anchor, focus } = selection

// 确定更新范围
const startIndex = Math.min(anchor.path[0], focus.path[0])

// 克隆数组并更新指定位置
const newChildren = [...(prevVnode.children || [])] as VNode[]
const newNode = node2Vnode(content[startIndex], startIndex, editor, editor)

normalizeVnodeData(newNode)
newChildren[startIndex] = newNode

return newChildren
}

/**
* 获取 editor.children 渲染 DOM
* @param textarea textarea
Expand All @@ -72,24 +101,39 @@ function updateView(textarea: TextArea, editor: IDomEditor) {
// 生成 newVnode
const newVnode = genRootVnode(elemId, readOnly)
const content = editor.children || []
newVnode.children = content.map((node, i) => {
let vnode = node2Vnode(node, i, editor, editor)
normalizeVnodeData(vnode) // 整理 vnode.data 以符合 snabbdom 的要求
return vnode
})
const prevVnode = TEXTAREA_TO_VNODE.get(textarea) // 获取上一次的 vnode
const cacheSelection = TEXTAREA_TO_SELECTION.get(textarea)

if (
prevVnode
&& cacheSelection
&& Range.isCollapsed(cacheSelection)
) {
newVnode.children = diffBySelection(prevVnode, content, editor)
} else {
newVnode.children = content.map((node, i) => {
const vnode = node2Vnode(node, i, editor, editor)

normalizeVnodeData(vnode) // 整理 vnode.data 以符合 snabbdom 的要求
return vnode
})
}

let textareaElem
let isFirstPatch = IS_FIRST_PATCH.get(textarea)
if (isFirstPatch == null) isFirstPatch = true // 尚未赋值,也是第一次

if (isFirstPatch == null) { isFirstPatch = true } // 尚未赋值,也是第一次
if (isFirstPatch) {
// 第一次 patch ,先生成 elem
const $textArea = genRootElem(elemId, readOnly)

$scroll.append($textArea)
textarea.$textArea = $textArea // 存储下编辑区域的 DOM 节点
textareaElem = $textArea[0]

// 再生成 patch 函数,并执行
const patchFn = genPatchFn()

patchFn(textareaElem, newVnode)

// 存储相关信息
Expand All @@ -99,7 +143,8 @@ function updateView(textarea: TextArea, editor: IDomEditor) {
// 不是第一次 patch
const curVnode = TEXTAREA_TO_VNODE.get(textarea)
const patchFn = TEXTAREA_TO_PATCH_FN.get(textarea)
if (curVnode == null || patchFn == null) return

if (curVnode == null || patchFn == null) { return }
textareaElem = curVnode.elm

patchFn(curVnode, newVnode)
Expand All @@ -109,11 +154,12 @@ function updateView(textarea: TextArea, editor: IDomEditor) {
textareaElem = getElementById(elemId)

// 通过 getElementById 获取的有可能是 null (销毁、重建时,可能会发生这种情况)
if (textareaElem == null) return
if (textareaElem == null) { return }
}

// focus
let isFocused

if (isFirstPatch) {
// 初次渲染
isFocused = autoFocus
Expand All @@ -130,9 +176,16 @@ function updateView(textarea: TextArea, editor: IDomEditor) {
// 存储相关信息
if (isFirstPatch) {
const window = getDefaultView(textareaElem)

// eslint-disable-next-line no-unused-expressions
window && EDITOR_TO_WINDOW.set(editor, window)
}

const selection = editor.selection

if (selection) {
TEXTAREA_TO_SELECTION.set(textarea, selection)
}
EDITOR_TO_ELEMENT.set(editor, textareaElem) // 存储 editor -> elem 对应关系
NODE_TO_ELEMENT.set(editor, textareaElem)
ELEMENT_TO_NODE.set(textareaElem, editor)
Expand Down
16 changes: 10 additions & 6 deletions packages/core/src/utils/weak-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
*/

import { Emitter } from 'event-emitter'
import {
Ancestor, Editor, Node, Path, Range,
} from 'slate'
import { VNode } from 'snabbdom'
import { Node, Ancestor, Editor, Path, Range } from 'slate'

import { IEditorConfig } from '../config/interface'
import { IDomEditor } from '../editor/interface'
import TextArea from '../text-area/TextArea'
import Toolbar from '../menus/bar/Toolbar'
import HoverBar from '../menus/bar/HoverBar'
import Toolbar from '../menus/bar/Toolbar'
import { IBarItem } from '../menus/bar-item/index'
import { Key } from './key'
import { PatchFn } from '../utils/vdom'
import { IEditorConfig } from '../config/interface'
import PanelAndModal from '../menus/panel-and-modal/BaseClass'
import TextArea from '../text-area/TextArea'
import { PatchFn } from '../utils/vdom'
import { Key } from './key'

// textarea - editor
export const EDITOR_TO_TEXTAREA = new WeakMap<IDomEditor, TextArea>()
Expand Down Expand Up @@ -74,6 +77,7 @@ export const CHANGING_NODE_PATH: WeakMap<Editor, Path> = new WeakMap()

// 保存 editor -> selection ,用于还原 editor 选区
export const EDITOR_TO_SELECTION: WeakMap<Editor, Range> = new WeakMap()
export const TEXTAREA_TO_SELECTION: WeakMap<TextArea, Range> = new WeakMap()

// editor -> eventEmitter 自定义事件
export const EDITOR_TO_EMITTER: WeakMap<Editor, Emitter> = new WeakMap()
Expand Down