SolidJS:在控制台日志中"computations created outside a `createRoot` or `render` will never be disposed"消息



在处理SolidJS项目时,您可能会在JS控制台中看到以下警告消息:

computations created outside a `createRoot` or `render` will never be disposed

SolidJS的Github存储库问题中有一些相关信息。但在阅读了它们之后,我仍然不太确定这是怎么回事,也不确定我的代码是否真的做错了什么。

我设法找到了它的来源,并根据文档找到了修复方法。因此,我将为那些在谷歌上搜索这条警告信息的人提供解释和解决方案。

本质上,这是一个关于内存泄漏可能性的警告,因为创建的反应式计算没有适当的上下文,而上下文将在不再需要时处理它。

一个合适的上下文是通过几种不同的方式创建的。以下是我所知道的:

  • 通过使用render函数
  • 通过使用createRoot函数。发动机罩下render使用此功能
  • 通过使用createContext函数

第一种方法是迄今为止最常见的方法,因为每个应用程序至少有一个render函数调用来启动整个节目。

那么,是什么让代码";断章取义">

可能最常见的方式是通过异步调用。只有当代码的同步部分完成运行时,才会创建带有依赖树的上下文。这包括模块中的所有export default功能和主应用程序功能。

但是,由于setTimeoutasync函数而在以后运行的代码将不在此上下文中,并且创建的任何反应式计算都不会被跟踪,并且可能会一直存在而不会被垃圾收集。

一个例子

假设您有一个数据输入屏幕,上面有一个Save按钮,该按钮向服务器发出API调用以保存数据。无论操作是否成功,您都希望通过一条漂亮的HTML格式的消息向用户提供反馈。

[msg,setMsg] = createSignal(<></>)
async function saveForm(){
...
setMsg(<p>Saving your data.<i>Please stand by...</i></p>)
const result=await callApi('updateUser',formData)
if(result.ok){
setMsg(<p>Your changes were <b>successfully</b> saved!</p> )
} else {
setMsg(<p>There was a problem saving your data! <br>Error: </p><pre>{result.error}</pre> )
}
} 
...
<div>
...
<button onClick={saveForm} >Save</button>
{msg()}
</div>

当API调用返回错误时,这将产生上面提到的警告,但其他时候不会。为什么?

原因是SolidJS认为在JSX中插入的代码是被动的,即:需要监视和重新评估。因此,插入API调用的错误消息将创建一个反应式计算。

解决方案

我在SolidJS文档的最后找到了解决方案。这是一个特殊的JSX修改器:/*@once*/

它可以在花括号表达式的开头使用,它"告诉"SolidJS编译器明确不要使其成为反应式表达式。换句话说:当从JSX创建DOM节点时,它将被评估一次,并且只评估一次。

在上面的例子中,这里是如何使用它:

setMsg(<p>There was a problem saving your data! <br>Error: </p><pre>{/*@once*/result.error}</pre> )

在此之后,将不再有警告消息:)


在我的例子中,我有一个输入,当输入发生变化时,我重新创建了一个SVG图形。因为创建SVG是一项昂贵的操作,所以我在createEffect函数中添加了一个debounce,该函数在输入更改时运行。CCD_ 16是一种将处理推迟到输入停止改变至少X时间量的技术。它涉及在setTimeout函数内部运行SVG生成代码,因此不在主上下文中。在生成的JSX中插入表达式的任何位置使用/*@once*/修饰符都解决了这个问题。

错误"在根"之外创建的计算;在跟踪范围外执行计算时发出。

什么是计算?可以订阅信号的任何形式的效果,包括通过createComputationcreateEffectcreateRenderEffectcreateComponentcreateMemo功能创建的效果。实体组件也是效果。

什么是跟踪范围?跟踪作用域是一个可以访问所有者的JavaScript作用域。如果getOwner函数返回一个值,则表示您处于跟踪范围内。有几种方法可以创建跟踪范围,但最基本的方法是createRoot,其他方法如rendercreateContext在内部调用它。

为什么我们需要一个跟踪范围?用于内存管理。跟踪范围跟踪效果的依赖关系。想想一个组件,一个组件可以创建一个DOM元素,它有可以创建其他DOM元素的子组件。它不仅是组件,即使是常规效果也可以在其体内承载其他效果。

如果某个效果侦听信号,它将重新运行。当它们重新运行时,它们将重复它们所做的一切。如果它正在创建一个组件,它将创建新的组件。承载其他效果的效果承载其他效果,可能会消耗大量资源。如果他们的消费得不到管理,就会很快失去控制。

在跟踪范围下创建效果时,Solid会为其指定所有者,并构建一个显示谁拥有谁的图形。每当所有者超出范围时,该所有者拥有的任何计算都将被处理。

跟踪范围跟踪内部资源,由SolidJS自己创建的资源。对于像套接字连接这样的外部资源,您需要通过onCleanup钩子手动释放它们。

效果可能会访问某个信号,也可能与此无关。这种依赖性跟踪存在于信号之外。尝试运行任何没有信号访问的效果,你仍然会得到错误:

import { createEffect, createSignal } from 'solid-js';
createEffect(() => console.log('Hello World'));

如果在异步函数内部执行效果,即使异步函数位于跟踪范围内,也会收到此错误。为什么?因为Solid是同步运行的。它循环运行。效果在对信号值作出反应时订阅信号,并在调用后取消订阅。因此,在每个更新周期中,一切都是建立和拆除的。当异步函数运行时,上一个循环的所有者将在很久以前被丢弃。因此,异步函数内部的效果将从依赖关系图中分离出来,并变得无赖。但解决方案很简单:通过用runWithOwner函数包装效果来提供新的所有者:

runWithOwner(outerOwner, () => {
createEffect(() => {
console.log('Hello World');
});
})

对于其他没有根作用域的情况,最好使用rendercreateRoot函数。

现在是时候解释@oncepragma如何解决公认答案中的问题了:

首先,您要通过调用setMsg在回调函数中创建一个组件。

@once杂注将一个prop值标记为静态值。

取此组件:

<Comp count={count()} />

通常,count道具被编译为getter函数,该函数返回值:

_$insert(_el$3, _$createComponent(Comp, {
get count() {
return count();
}
}));

这是为了在将值从父级传递给子级时保持反应性。

当添加@once时,道具的值将被视为静态值:

_$insert(_el$3, _$createComponent(Comp, {
count: count()
}));

记住我们说过成分就是效果。使用@once时,Solid将子项视为静态值,而不是组件。换句话说,Solid在异步函数内部看不到任何效果,而是一个返回静态值的函数调用:

<pre>{/*@once*/ result.error}</pre>

顺便说一句,在接受的答案中使用的示例代码不是惯用的Solid组件。最好不要把UI和状态混合在一起。

相关内容

最新更新