Procházet zdrojové kódy

fix: editor.destroy() 同步调用 + apply 包 try-catch 兜住 stale Slate 代理

根因:不调 destroy 导致 selectionchange 监听器 + closure 持有的旧 Slate state
代理持续驻留 document,setHtml 触发 op 后旧代理被 revoke,下次 selectionchange
触达即抛 'proxy has been revoked'。

修复:
1. _destroyEditor 同步调 editor.destroy(),destroyed 事件触发 removeEventListener
   把 selectionchange 摘掉,try-catch 兜住 destroy 链内部抛错
2. _createEditor 后把 editor.apply 包一层 try-catch,捕获 stale 代理导致的 Uncaught,
   仅吞这一类错误,其它异常正常抛出
Developer před 6 dny
rodič
revize
3faa00861e
2 změnil soubory, kde provedl 30 přidání a 13 odebrání
  1. binární
      dist.zip
  2. 30 13
      src/components/editor/index.vue

binární
dist.zip


+ 30 - 13
src/components/editor/index.vue

@@ -53,23 +53,20 @@ export default {
             }
         },
         _destroyEditor() {
-            // 关键决策:wangEditor 5 在销毁 path 上有 SLIGHT-TIMING 的 listener 残留问题,
-            // editor.destroy() 之前 TextArea 已经在 document 上 addEventListener 了
-            // selectionchange;一旦销毁链中途抛 "Cannot perform 'get' on a proxy
-            // that has been revoked",emit("destroyed") 不会触发,removeEventListener
-            // 也就不会跑。下次用户点页面 selectionchange 触发同一个 revoke 错误循环。
-            //
-            // 解法:不调用 editor.destroy()/toolbar.destroy(),只断引用。
-            // Vue 卸载组件时已经会把 $toolbarContainer / $editorContainer DOM 节点摘掉,
-            // 元素级监听器随之释放。document 上的 selectionchange listener 持有的是
-            // TextArea 实例;TextArea 内部 WeakMap 指向 editor 实例;editor 实例
-            // 不再被任何地方引用时,GC 一并回收,listener 因 handler 函数被回收而失效。
-            // 期间不调用 destroy,Slate proxy 不会被显式 revoke,所有 handler 路径上的
-            // 属性访问都还是合法的,不会再抛异常。
+            // 同步调用 destroy():coreCreateEditor 内部注册了 document 上的
+            // selectionchange 监听器,只有 emit("destroyed") 才会走
+            // removeEventListener。如果不调 destroy,旧监听器会永久留在
+            // document 上,且其 closure 持有的 Slate state 代理在后续操作
+            // (setHtml / 输入触发 change) 中会被 Slate 自动 revoke,下次用户
+            // 一点页面就抛 "Cannot perform 'get' on a proxy that has been revoked"。
+            // 同步调用 + try-catch 兜住 destroy 链内部任何抛错,保证 emit("destroyed")
+            // 至少能走到,监听器能被摘掉。
             if (this.editor) {
+                try { this.editor.destroy() } catch (e) { /* swallow */ }
                 this.editor = null
             }
             if (this.toolbar) {
+                try { this.toolbar.destroy() } catch (e) { /* swallow */ }
                 this.toolbar = null
             }
         },
@@ -116,6 +113,26 @@ export default {
                 mode: 'default'
             })
 
+            // 兜底:把 editor.apply 包一层 try-catch。Slate.js 在每次 op 后会
+            // revoke 旧 state 代理;如果 selectionchange 触发的 editor.select →
+            // apply 路径撞上一个 stale 代理(常见于 setHtml 跑了一串 op 后,
+            // 监听器 closure 还指向早期 state),会直接 Uncaught。捕获后该次
+            // 选择同步丢弃,不影响后续输入;后续 Slate 自己的 op 流程仍走原 apply,
+            // 这里只是把"已是 normal"的封死,不是替换实现。
+            if (this.editor && typeof this.editor.apply === 'function') {
+                const origApply = this.editor.apply
+                this.editor.apply = function(op) {
+                    try {
+                        return origApply.call(this, op)
+                    } catch (err) {
+                        if (err && err.message && err.message.indexOf('proxy has been revoked') !== -1) {
+                            return null
+                        }
+                        throw err
+                    }
+                }
+            }
+
             // 工具栏:列出 wangEditor 5 已注册的全部有效菜单 key
             // 注:wangEditor 把删除线菜单命名为 "through","mergeCells" 不是注册菜单
             const toolbarConfig = {