游牧周记第34期
阳台种植
冰菜
没想到冰菜是这么容易发芽的。 但是因为种子实在太小(和粉尘差不多,传说中的芥子是否也如此?),播散时无法间隔,现在想间苗移栽难度很大。
其他苗的移栽也有点操作困难,我是否需要学一下,或者买点工具?
其他
似乎是罗勒和生菜,还有个薄荷我不记得种在哪里了。
开发
关于zustand的调用方法
我的问题是这样:
expo项目中,zustand定义了一组全局状态如下:
type TheStates = {
step: Steps
auto: boolean
result?: ChangingYaoNumber[]
coinsToPay: number
activeSubscriberPayCoins?: boolean
updateStates: (setting: Partial<TheStates>) => void
}
export const useDivinationStates = create<TheStates>()(
set => ({
step: Steps.ASK,
auto: true,
result: undefined,
coinsToPay: C.MIN_DIVINATION_COINS,
activeSubscriberPayCoins: false,
updateStates: statesData => set((state) => ({ ...state, ...statesData }))
})
)
...
UI页面在调用其中多个状态的时候,一般采用以下2种方式:
1. 一句话:
const { auto, step } = useTheStates()
这种方式似乎 会造成当其他状态变化,也会重复渲染?
2. 分开写
const auto = useTheState(state=>state.auto)
const step = useTheState(state=>state.auto)
这种方式可以精准管理每个状态,避免重复渲染,但太麻烦。
请简单分析以上方法优缺点,直接输出结果。
另外为了结合两者优点可否这样写:
const { auto, step } = useDivinationStates(state => ({
auto: state.auto,
step: state.step,
}))
这个写法是否会造成组件无限重复渲染?简单分析并直接给结论。
实际上最后那个方式真的造成了重复渲染,我一般用2,但这个问题只是测试一下AI,同时想搞清此问题原理。
各个AI说法不一。
kimi k2
- 两种写法的优缺点
-
写法1:
优点:最简洁。
缺点:只要 store 中任何字段变化(如 coinsToPay、result…),组件都会重新渲染,哪怕只用到了 auto、step。 -
写法2: 优点:每个字段独立订阅,仅当该字段变化时才渲染。
缺点:代码啰嗦,字段多时需要写很多行。
- 你给出的写法
不会导致无限渲染。
Zustand 会对 selector 返回的对象做浅比较。只要 auto、step 的值没变,返回的新对象会被判定为“相等”,不会触发额外渲染。因此它同时保留了写法1的简洁和写法2的按需订阅效果,推荐使用。
豆包
第一个问题都差不多结论,关于第二个问题如下(后面也是这样):
不会导致无限重渲染。这种写法通过自定义选择器只提取所需状态,只有当 auto
或 step
变化时才会触发更新。但需注意确保选择器返回值的引用稳定性(例如避免内联函数导致的浅比较失效)。
最后提到了一个稳定性问题。
Qwen 3
代码模式
分析:
- 这种方法通过选择器函数来提取所需的状态,类似于方法2中的精确控制,但又保持了一定程度上的简洁性。
- 它只会对指定的状态进行浅比较,只有这些状态发生变化时才会触发组件的重新渲染。
结论:
这种写法不会造成组件无限重复渲染。只要选择器返回的对象中包含的状态发生变化,组件才会重新渲染,否则不会。因此,这种方法能够有效地结合两种方法的优点,既保证了代码的简洁性,又避免了不必要的重新渲染。
Grok 3
分析:
- 这种写法使用 Zustand 的选择器(selector)功能,通过 useDivinationStates 的回调函数返回一个对象 auto, step ,仅订阅 auto 和 step 两个状态。
- Zustand 的选择器会比较返回对象的字段(auto 和 step),只有当这些字段的值发生变化时,组件才会重新渲染。
- 不会导致无限重复渲染,因为 Zustand 的选择器是基于值的浅比较(shallow comparison),只要 auto 和 step 的值不变化,组件就不会重新渲染。
- 这种方式结合了方式1的简洁性和方式2的精准订阅优势,避免了无关状态变化导致的重复渲染。
结论:
- 新写法 不会造成无限重复渲染。
- 它是最佳实践,兼具简洁性和性能优化,仅当 auto 或 step 变化时触发渲染,适合大多数场景。推荐使用。
完全是肯定的,对了最早推荐这样写的也是它。
Gemini 2.5 Pro
先Thinking了一阵子
这个写法的意图很好,希望结合前两者的优点。但它存在一个严重的问题。
-
分析:
- 组件每次渲染(或因父组件渲染而重新渲染)时,选择器函数
state => ({ auto: state.auto, step: state.step })
都会被重新执行。 - 这个函数每次执行都会在内存中创建一个全新的对象
{ auto: ..., step: ... }
。 - Zustand 在接收到 store 的更新后,会比较选择器上一次返回的值和这一次返回的值是否相等,以决定是否要触发组件渲染。
- 默认的比较方式是严格相等 (
Object.is
,类似于===
)。由于每次返回的都是一个新对象,它们的内存地址不同,所以oldObject === newObject
的结果永远是false
。 - 因此 ,只要 store 中任何一个状态发生变化(即使不是
auto
或step
),这个 hook 就会认为它所订阅的值发生了变化(因为对象引用变了),从而触发组件重新渲染。 - 组件的重新渲染又会再次执行选择器,创建一个新对象... 如果这个组件内部有触发 store 更新的逻辑,就会陷入无限重复渲染的死循环。
- 组件每次渲染(或因父组件渲染而重新渲染)时,选择器函数
-
结论:
是的,这种写法会造成不必要的重复渲染,并且在特定情况下可能导致无限循环。 它不仅没有解决方法一的性能问题,反而因为每次都创建新对象而可能表现更差。绝对应该避免这种写法。
第一个提到无限循环的,而且要求绝对避免,态度坚决!!!
ChatGPT 4.1
- 结论:不会造成组件无限重复渲染。
- 原因:Zustand 会对比 selector 返回的对象(此处是
{auto, step}
),只有当对象内容变化(即auto
或step
变化)时才会触发渲染。不会因为无关状态变化而导致渲染,也不会死循环。 ... - 推荐用第三种写法:
useDivinationStates(state => ({ auto: state.auto, step: state.step }))
,既简洁又精准。 - 不会导致无限渲染,可以放心使用。
GPT居然没分析出来,令我有点意外。
Claude-Sonnet-4
结论:会造成无限重复渲染
原因:
每次组件渲染时,state => ({ auto: state.auto, step: state.step })
都会创建一个新的对象。由于对象引用不同,zustand认为状态发生了变化,触发重渲染,形成无限循环。
正确的解决方案:
使用 shallow
比较或自定义比较函数:
javascript
复制
import { shallow } from 'zustand/shallow'
const { auto, step } = useDivinationStates(
state => ({ auto: state.auto, step: state.step }),
shallow
)
或者使用 useShallow
hook(zustand v4.4+):
javascript
复制
import { useShallow } from 'zustand/react/shallow'
const { auto, step } = useDivinationStates(
useShallow(state => ({ auto: state.auto, step: state.step }))
)
这样既保持了代码简洁,又避免了不必要的重渲染。
第二个发现问题的,并且还给出了建议的写法,要试试!!!
最后我补充测试了Grok 4
这个写法使用了zustand的selector机制,返回一个自定义对象,只订阅auto和step的状态。zustand会使用浅比 较(shallow equality)检查selector返回的对象是否变化:
- 如果auto或step变化,对象内容变化,组件会重新渲染(预期行为)。
- 如果其他状态(如result)变化,但auto和step不变,selector返回的对象相同,不会触发渲染。
- 它不会修改store状态,只读取,因此不会导致渲染循环。无限渲染通常源于渲染过程中直接修改状态(如在useEffect中无条件setState),而这里仅是读取。
结论:
不会造成组件无限重复渲染。这个写法有效结合了两者的优点:简洁(一行解构多个状态)且高效(只在选定状态变化时渲染),是推荐的优化方式。
马斯克的大力出奇迹,还是没能正确分析!!!
果然代码领域Claude还是老大,Gemini 2.5 Pro也非常令人惊喜!ChatGPT有点拉了我没想到。 国产还需努力。
新版react-native-picker的奇葩问题
android不说了,本来就不用它。 ios上,不管传什么值,永远报错:
Error: Exception in HostFunction: TypeError: expected dynamic type 'string', but had type 'double'
用尽无数方法(AI也用了无数)没法解决。 最后降级,目前版本是"@react-native-picker/picker": "2.11.1",参考expo官方资料降到"2.7.5"后ios正常,但android build出错;只好重新安装:
npx expo install @react-native-picker/picker
结果这次两边都ok了,版本还是:"@react-native-picker/picker": "2.11.1"。 不知这次能稳定多久? 结果第二天编译又出错了,貌似还是缓存问题。 突发奇想,我再升级到最新版呢,于是再装(2.11.1),一切正常了,仿佛没事发生。
reanimated旋转动画哪家AI强
我是指AI编程领域。 对我是要做一个硬币旋转的效果。 其实几个月前我就在设计了,所有AI都问遍了,多多少少都有问题,且无法解决。 截至上次Claude和GPT的还算能用,android完全ok,IOS模拟器上不行,显示效果Bug无法解决。
今天采用Gemini 2.5pro完美解决,虽然过程中也遇到一两次同样错误,但很快就修复搞定了。 其实知道现在我也不知怎么搞定的。😅
Gemini还给出了很优雅的代码分离方式。
JS随机数小问题
actionFn: () => Math.floor(Math.random() * 6) + 1, // 生成1到6的随机数!Math.random() 的取值区间是 左闭右开:
Math.random() 的取值区间是 左闭右开:
[0, 1),即 0 ≤ r < 1。
因此
Math.random() * 6 的区间是
[0, 6),即 0 ≤ r·6 < 6。
Math.floor 只会向下取整,最大只能取到 5(因为 5.999… 向下取整也是 5)。
所以 Math.floor(Math.random() * 6)
的结果范围是 0–5,绝不会出现 6。
组件设计的styles小tip
在React Native中,style
属性通常用于定义组件的样式。关于style?: StyleProp<ViewStyle>;
与style?: ViewStyle;
的区别和前者的好处,主要体现在类型安全和灵活性上。
-
类型安全:
StyleProp<ViewStyle>
实际上是一个可以接受多种类型的联合类型,不仅仅包括直接的样式对象(即ViewStyle
),还包括数组形式的样式(用于组合多个样式对象)以及null
或undefined
。这意味着使用StyleProp<ViewStyle>
可以更准确地描述style
属性能够接受的所有合法输入类型,提供更好的类型检查和错误提示。 -
灵活性:通过使用
StyleProp<ViewStyle>
,你不仅可以传递一个样式对象,还可以传递一个样式的数组。这种方式允许你 以一种更加灵活的方式组合不同的样式规则,例如覆盖默认样式或合并多个样式源。这对于动态调整UI或根据状态应用不同样式非常有用。 -
支持其他样式类型:虽然这里的讨论集中在
ViewStyle
上,但StyleProp
也可以处理其他类型的样式,如TextStyle
或ImageStyle
,增加了代码的复用性和可读性。
综上所述,使用style?: StyleProp<ViewStyle>;
相比于style?: ViewStyle;
提供了更高的灵活性和更强的类型安全性,使得代码更加健壮和易于维护。