svelte的响应性
变量响应性
当一个变量的值发生改变时,与之关联的变量和在页面中的显示内容也随之改变
类似于vue中的ref()和react中的useState()
在svelte4版本中,赋值操作是触发Svelte响应性的核心,就是说只要出现等号(=)赋值语句,就会自动分析该语句中所有涉及的变量并对所有涉及的变量应用响应
let a = 1;
let b = a*2;
<p>a的值:{a}</p>
<p>b的值:{b}</p>
<button on:click={()=>a++}>a+1</button>
点击按钮后页面显示中只有a的值发生了改变,b的值是不变的
因为点击按钮后仅仅执行了a++,也就是a=a+1的赋值操作,而变量b并没有进行赋值操作
如果变量为一个对象,更新其中一个元素也会触发整个对象的完整的响应,而不是仅仅针对改变的元素响应
如果变量为一个数组,如果使用了如push、splice等改变原数组的方法也不会引起响应性
当时为了解决这个问题,必须在更新完数组后添加 arr=arr 语句
或者使用 arr=[...arr,new_vals] 语句进行数组更新
可以看出在svelte4中,响应性的实现相对来讲颗粒度较大,无法更加精细化的控制
在svelte5版本中,对响应性功能的实现做了巨大的重构,可以实现更加精细化的控制并且提高了性能
在svelte5版本中做了向前兼容,也就是说之前的响应性语法仍然可用
$state(val) 深度响应
就是在声明一个变量时使用一个特殊的语法糖$state(val)
这些变量被标注为“响应式”的,当变量的值改变时,所有引用的地方都会自动更新
并且在更新时没有使用API,就像一个普通的变量一样进行更新即可
如果为一个对象,任何一个元素发生了改变所有之关联的变量和在页面中的显示也随之改变,且该对象中其他无更改的元素不会进行响应
let num = $state(1);
<button onclick="{()=>num++}"></button>
需要注意的是,Set 和 Map 不会被代理,但 Svelte 为各种内置函数提供了响应式实现,可以从 svelte/reactivity 导入,再次就不做详细介绍了
$state.raw(val) 禁用响应
如果不希望对象或数组深度响应,可以使用 $state.raw(val)
此时无论使用什么方式修改其中元素的值,都不会触发响应性,但是该变量中元素的值实际上已经被修改了
当然我们可以使用 obj = {...obj}
语句来强制触发响应性,但是完全没必要,因为如果需要响应性我们完全可以使用更加简单的$state(val)
禁用响应可以提高那些不打算改变的大型数组和对象的性能,因为它避免了使它们响应的成本
$state.snapshot(val)
其实本质上svelte响应性是依靠Proxy实现的,每个响应性数据会转换为对应的Proxy对象,然后针对每个属性设置对应的拦截器
如果需要传递该对象的值给一些svelte之外的库,那么如果传递变量名实际上传递的是一个Proxy对象,这是就可以使用快照功能
$state.snapshot(val)返回一个val的实际数据类型的数据
脚本语句响应性
在svelte4版本中,$: 用于将任何位于顶级的语句标记为响应式,只要语句块中任何位置引用到的变量发生改变,就会重新运行整个语句块
let a = 1;
$: b = a*2;
$: if (a>10){
a = 1;
}
$: {
console.log('b的值为:'+b);
}
<button on:click={() => a++}>a+1</button>
当$: 语句后面接变量时,需要省略let关键字
$: 语句后面可以接一个简单的单行语句,也可以是用大括号包起来的语句块,也可用于if语句中
在svelte5版本中,对脚本语句响应性添加了$derived(exp)语法糖
$derived(exp) 单行语句响应性
其中exp表示一个单行的脚本语句,该脚本语句中任何引用的变量值发生了改变,都会重新运行该语句
重新运行的语句为整个包含该语法糖的语句块,也就是说如果有变量接收该语法糖的返回值,也会在重新运行脚本语句后重新执行赋值操作
该语法糖返回值为脚本语句的执行结果
let a = $state(1);
let b = $derived( a * 2 )
<p>{b}</p>
<button onclick="{()=>a++}">a+1</button>
$derived()中的脚本语句不能为变量赋值之类的语句,如$derived( a++ ),会触发无限递归
单行语句中如果涉及的变量没有使用$state()标注为响应式变量,则该值的改变不会触发语句块的重新运行
$derived.by(func) 多行语句响应性
$derived.by()可以接受一个函数来实现复杂的多行语句响应性
该语法糖返回值为函数运行后的返回值
实际上,$derived(exp) 等同于 $derived.by(() => exp)
组件响应式
$effect(func) 组件响应,DOM更新后
组件响应类似于多行语句响应,不同的是会在包含$effect(func)的组件挂载到DOM之后自动运行一次,随后就是与多行语句响应类似
接受一个函数并扫描函数体中的$state, $derived 和 $props相关变量并注册为依赖项,任何依赖项发生更改且DOM更新完整后,都将重新运行该函数
异步读取(如await之后或setTimeout中)的变量不会注册为依赖项
在扫描并注册依赖项时,如果存在短路操作,则不会将短路操作中的未执行语句中的变量注册为依赖项
$effect(()=>{
if ( a>5 || b>10 ){
console.log('running')
}
})
在扫描依赖项时如果变量 a 的值大于5,后面的b>10语句不会执行,也不会将变量 b 注册为依赖项
此后如果变量 b 的值发生更改也不会触发重新运行
如果传入的函数返回一个函数 refunc,该返回的函数将在下一次响应式运行之前 或 组件销毁即将销毁之前 运行
let num = $state(0);
let time = $state(1000);
$effect(() => {
const interval = setInterval(() => {
count += 1;
}, time);
// count在异步造作函数中,不会被注册为依赖项
// time会注册为依赖项
return () => {
clearInterval(interval);
};
// 返回一个函数,用于执行清理工作
})
1、整个函数会在整个组件挂载到DOM之后自动运行一次
2、当检测到依赖项发生改动时,首先运行上次返回的函数,然后运行整个函数
3、在该组件即将被销毁时运行返回的函数
$effect.pre(func) 组件响应,DOM更新前
与$effect()的功能及使用方式完全相同,唯一不同的是函数的运行时间实在DOM更新之前
$effect.tracking() 语句追踪
返回一个bool值,表示该函数是否在effect中运行
<script>
console.log($effect.tracking()); // false
$effect(() => {
console.log($effect.tracking()); // true
});
</script>
<p>{$effect.tracking()}</p> // false
调试响应式
在开发阶段有时候需要时刻关注某些值的改动情况,可以使用$inspect(...val)语法糖
会在初始化时和传入的若干个参数中任意一个发生改变时运行指定函数
默认运行console.log(type,...val),其中type有两个值"init"和"update",分别表示初始化和变量更改两种状态
<script>
let a = $state(1);
let b = $derived( a * 2 );
$inspect(a,b)
</script>
<button onclick="{()=>a++}">++</button>
// init 1 2 初始化时显示
// update 2 4 点击一次按钮后显示
with回调,$inspect()返回对象有一个with方法,用于指定运行的函数
$inspect(val1,val2,...).with(
(type,val1,val2,...)=>{
// some code
}
)
type有两个值,init和update
回调中传入的val与$inspect()中的val一一对应,表示这些变量的最新值
通过with回调可以修改默认的运行console.log()
也就是说,默认的回调函数为console.log
$inspect(val1,val2,...).with(console.log)
其实这个变量值的监控完全可以通过$derived()来实现,使用$inspect()的一个好处是比较方便,另外最重要的就是$inspect()仅在开发期间有效,一旦进行了build将失效