跳到主要内容

游牧周记第34期

· 阅读需 10 分钟
Suhe
This site owner

阳台种植

冰菜

没想到冰菜是这么容易发芽的。 但是因为种子实在太小(和粉尘差不多,传说中的芥子是否也如此?),播散时无法间隔,现在想间苗移栽难度很大。

其他苗的移栽也有点操作困难,我是否需要学一下,或者买点工具?

其他

似乎是罗勒和生菜,还有个薄荷我不记得种在哪里了。

开发

关于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. 两种写法的优缺点
  • 写法1:
    优点:最简洁。
    缺点:只要 store 中任何字段变化(如 coinsToPay、result…),组件都会重新渲染,哪怕只用到了 auto、step。

  • 写法2: 优点:每个字段独立订阅,仅当该字段变化时才渲染。
    缺点:代码啰嗦,字段多时需要写很多行。

  1. 你给出的写法

不会导致无限渲染。
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了一阵子

这个写法的意图很好,希望结合前两者的优点。但它存在一个严重的问题。

  • 分析:

    1. 组件每次渲染(或因父组件渲染而重新渲染)时,选择器函数 state => ({ auto: state.auto, step: state.step }) 都会被重新执行。
    2. 这个函数每次执行都会在内存中创建一个全新的对象 { auto: ..., step: ... }
    3. Zustand 在接收到 store 的更新后,会比较选择器上一次返回的值和这一次返回的值是否相等,以决定是否要触发组件渲染。
    4. 默认的比较方式是严格相等 (Object.is,类似于 ===)。由于每次返回的都是一个新对象,它们的内存地址不同,所以 oldObject === newObject 的结果永远是 false
    5. 因此,只要 store 中任何一个状态发生变化(即使不是 autostep),这个 hook 就会认为它所订阅的值发生了变化(因为对象引用变了),从而触发组件重新渲染。
    6. 组件的重新渲染又会再次执行选择器,创建一个新对象... 如果这个组件内部有触发 store 更新的逻辑,就会陷入无限重复渲染的死循环。
  • 结论:
    是的,这种写法会造成不必要的重复渲染,并且在特定情况下可能导致无限循环。 它不仅没有解决方法一的性能问题,反而因为每次都创建新对象而可能表现更差。绝对应该避免这种写法。

第一个提到无限循环的,而且要求绝对避免,态度坚决!!!

ChatGPT 4.1

  • 结论不会造成组件无限重复渲染
  • 原因:Zustand 会对比 selector 返回的对象(此处是 {auto, step}),只有当对象内容变化(即 autostep 变化)时才会触发渲染。不会因为无关状态变化而导致渲染,也不会死循环。 ...
  • 推荐用第三种写法: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;的区别和前者的好处,主要体现在类型安全和灵活性上。

  1. 类型安全StyleProp<ViewStyle>实际上是一个可以接受多种类型的联合类型,不仅仅包括直接的样式对象(即ViewStyle),还包括数组形式的样式(用于组合多个样式对象)以及nullundefined。这意味着使用StyleProp<ViewStyle>可以更准确地描述style属性能够接受的所有合法输入类型,提供更好的类型检查和错误提示。

  2. 灵活性:通过使用StyleProp<ViewStyle>,你不仅可以传递一个样式对象,还可以传递一个样式的数组。这种方式允许你以一种更加灵活的方式组合不同的样式规则,例如覆盖默认样式或合并多个样式源。这对于动态调整UI或根据状态应用不同样式非常有用。

  3. 支持其他样式类型:虽然这里的讨论集中在ViewStyle上,但StyleProp也可以处理其他类型的样式,如TextStyleImageStyle,增加了代码的复用性和可读性。

综上所述,使用style?: StyleProp<ViewStyle>;相比于style?: ViewStyle;提供了更高的灵活性和更强的类型安全性,使得代码更加健壮和易于维护。