前提
编辑器不触发组件内容更新的解决方案是手动调用editor.refresh()。
<CodeMirror
editorDidMount={(editor) => {
// https://codemirror.net/doc/manual.html#api
// 遇到编辑器不触发的情况,手动刷新编辑器
setTimeout(() => {
editor.refresh()
}, 100)
}}
/>
这样每当CodeMirror组件被挂载后就会进行一次刷新,更新的内容就可以填充进来了。
问题
当为CodeMirror组件设置了display: none属性时,取消该属性使CodeMirror组件重新渲染在界面上时,为CodeMirror编辑设置的内容必须在点击一下组件时才能正确加载出来,不点击前编辑器内容始终为空白。
这个现象具体的原因是:当有一个组件的style为display: none时,这个组件实际上已经被挂载在DOM树中了。但当CodeMirror的盒子被resize了或者未能给盒子提供有效的宽高时,CodeMirror编辑器是没办法refresh textarea的内容的。那么,这个时候,当组件unhide时如何让CodeMirror编辑器能够更新内容呢?
这里牵扯到浏览器的渲染过程:
第一步,解析HTML结构,渲染DOM树;
第二步,处理CSS,构建CSSOM树;
第三步,组合DOM Tree和CSSOM Tree,构建渲染树(render tree);
第四步,计算节点结构并布局;
第五步,绘制;
react中,挂载(mount)这个概念,实际上指的是DOM树的挂载,也就是说,CodeMirror的editorDidMount生命周期在渲染的第一步就已经执行了。如果给CodeMirror加一个display:none,就是CSSOM树上给这个节点增加了一个属性display:none,这个属性会使得该节点DOM树和CSSOM树结合时不会出现在渲染树上。
可以参考一些详解display: none的文章:juejin.cn/post/690501…
也就是说,CodeMirror在editorDidMount这个生命周期触发之后,这个节点消失在渲染树中了,它的尺寸自然无法被计算,refresh()并没有生效。
refresh()必须在节点有有效宽高时才有意义,所以需要在editorDidMount内进行setTimeout并设置延时时长,给编辑器留一些进行渲染的时间。但是当你的display: none属性在setTimeout的延时之后才取消,refresh()是没办法在组件拥有有效宽高后才触发的。
如果我写了一个<Step />组件,当点击“下一步”更新step时,下一个step显示的组件的display: none才会被取消,而CodeMirror编辑器在第二个step才显示,也就是说,这个组件的display: none是被用户主动触发时,就无法通过setTimeout的方式来有效刷新了。
一个旁门左道的解决方案
我一开始没想到这个问题是由于CodeMirror和display: none样式在update时有冲突。
我以为是对CodeMirror组件传入的value的获取时间和editorDidMount的生命周期不匹配,所以我做了如下方案:
import { useInterval } from 'react-use'
// 组件内
useInterval(() => {
if (value) {
myEditor.refresh()
}
}, value ? 100 : null)
只要对包裹CodeMirror的组件传递进来了value,就在组件中增加一个计时器,保证refresh能够正常触发。
但是不合理的地方在于,其实只要CodeMirror所在的组件被渲染之后refesh一次就可以了,没必要一直refresh增加组件渲染负担。
一个正经的解决方案
找到你对display: none进行控制的变量,传递进包裹CodeMirror组件的组件中,对其进行refresh的手动刷新即可。比如:
useEffect(() => {
if(show) {
myEditor.refresh()
}
}, [show])
更优的解决方案
上面这个传递show的方案还是太复杂了,我们可以从根本上避免display: none。
将你需要display: none的dom结构直接改为position: absolute; left: -10000px;让组件挂载在浏览器中,但不显示在用户视角中,避免了组件消失在渲染树的问题,且用户体感没有差别。
其实top、right、bottom设为很大的负值都可以,对于一般情况建议设置left,原因是组件的长度会随着你组件内容的增加而变长,但是宽度往往不会超过显示屏宽度,相对来说left更保险一点,且未来不用随着组件内容更改调整大小。当然,具体情况具体分析。
总结
解决方案其实很简单,核心是要找到bug产生的原因,越了解bug原因解决方案往往会更加简洁明了合理。比如这个useInterval的方式,在解决bug的时候更容易想到,反而是动脑子比较少的解决方案,但是它不好,虽然能解决问题,但没从根本上解释原因,也有可能会引入其他问题。
以前总觉得面试八股文没什么用,都说面试造火箭工作拧螺丝。但是真的工作过程中总会发现,之前背过的东西经常会拿来解决一些边边角角的问题。如果我从不知道生命周期、DOM树可能就跟很难理解这个bug。
八股还是有用的。