V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Mirachael
V2EX  ›  Angular

请教下 angular computed 相关的问题

  •  
  •   Mirachael · 224 天前 · 2105 次点击
    这是一个创建于 224 天前的主题,其中的信息可能已经有所发展或是发生改变。

    使用 computed 获取数组长度,但是视图不会更新

      addrLength = computed(() => {
        const user = this.dataService.user();
    
        return user.addresses.length;
      });
    

    使用 computed 重新构建新的数组,视图也不会更新

      addresses = computed(() => {
        const user = this.dataService.user();
        // transform data 后页面不会更新
        // return user.addresses.map(addr => ({ address: addr, title: `Address-${addr.title}` }));
    
        // 直接返回 user.addresses ,页面会更新
        return user.addresses;
      });
    

    这里是在线 demo

    第 1 条附言  ·  224 天前

    其实是想试下angular的新特性signal computed文档

    看文档,我理解为,signal更新后,任何依赖此signal的computed也会更新,文档里有原话。

    const count: WritableSignal<number> = signal(0);
    const doubleCount: Signal<number> = computed(() => count() * 2);
    
    

    The doubleCount signal depends on count. Whenever count updates, Angular knows that anything which depends on either count or doubleCount needs to update as well.

    第 2 条附言  ·  223 天前
    结论: 使 signal 的 equal 方法始终返回 false ,这样就能保证触发更新
    27 条回复    2024-04-19 13:36:02 +08:00
    echoless
        1
    echoless  
       224 天前
    感觉是 computed object/array signal tracking 算法的问题. 我查了半天文档也没有查到原因.

    代码在这, 暂时不太能看懂
    https://github.com/angular/angular/tree/16.2.12/packages/core/src/signals
    Chad0000
        2
    Chad0000  
       224 天前   ❤️ 2
    你这种还不如这样:{{user.address.length}}。user 来自 userService ,这个对象一直不变,登录修改里面的值。这样肯定可行。你那个主要问题是不确定什么时间会改变,会导致 Angular 不断调用才能判断。
    Chad0000
        3
    Chad0000  
       224 天前
    我用 Angular 这么多年,主要领悟就是能共享对象就不要传什么方法。然后在其他地方替换对象或更新它的值。比计算要高性能,也不要用 get/set 属性,这个会比一般的变量浪费性能因为需要计算才能拿到值,直接给字段,省去重复调用的过程。
    wunonglin
        4
    wunonglin  
       224 天前
    原因是你的 user 是同一个,你虽然设置了 addresses ,但是实际的 user 是没有变化的。

    你要做的是把 user 里的 addresses 也设置 signal 。
    tedding
        5
    tedding  
       224 天前 via iPhone
    Create a computed Signal which derives a reactive value from an expression.
    angular 说了计算的是 signal
    参考 https://angular.io/guide/signals
    chnwillliu
        6
    chnwillliu  
       224 天前 via Android
    你在 update user address 时,user 这个 signal 并没有变 dirty ,自然 computed 不会重新计算。
    Mirachael
        7
    Mirachael  
    OP
       224 天前
    @chnwillliu 调用了 signal 的 update 方法,怎么变 dirty
    chnwillliu
        8
    chnwillliu  
       224 天前 via Android
    signal 相当于默认有 rxjs 的 distinctUntilChanged ,你第二次 emit 同一个 object reference 会被 skip 掉的。
    chnwillliu
        9
    chnwillliu  
       224 天前 via Android
    自定义 signal 判等方式,或者 update address 的时候把整个 user update 成另一个 object reference ,好比 redux / ngrx 处理的方式。
    Mirachael
        10
    Mirachael  
    OP
       224 天前
    @Chad0000 #3 这是 angular 的新特性 signal ,如果在模板里获取 signal 的当前值,需要像方法一样调用。比如 dataService.user().addresses 。这个 user 是 signal ,不是方法。另外,你说模板不要传方法,其实是更新策略选择的问题。
    Mirachael
        11
    Mirachael  
    OP
       224 天前
    @chnwillliu #9 我觉的跟 user 的 object reference 变没变没关系,因为这种写法是可以更新视图的

    ```
    addresses = computed(() => {
    const user = this.dataService.user();

    // 直接返回 user.addresses ,页面会更新
    return user.addresses;
    });
    ```
    Chad0000
        12
    Chad0000  
       224 天前
    @Mirachael #11
    你这个返回的是 object 了,后面改变的也是 object ,那么不管它来自哪里,都能触发更新。

    我倒是没注意这个新的 Feature ,但我觉得它可能会性能差。怎么说呢,我在做一个白板应用,用 Angular 管理成千的节点渲染出来,目前没有使用 Push 模式,有一次我大量将 field 改成了 getter/setter ,结果性能直线下降产生了明显卡顿感。所以我后面的实践都是能直接给 Field 就直接给,哪怕 getter 这种不计算的也是需要调用后才知道是否改变,会导致性能下降。

    你的这个 Feature 就算没导致性能下降,但会导致写法过于混乱,跟直接绑定对象或属性比,不够简洁直观。
    Mirachael
        13
    Mirachael  
    OP
       224 天前
    @chnwillliu #9 你说的对,浅拷贝还不行,必须使用深拷贝,因为在 computed 里使用的都是 addresses ,我使用 lodash 的 cloneDeep 方法就可以

    ```
    addAddress() {
    this.user.update((u) => {
    const addr = new Address('test', '20000');
    u.addAddress(addr);

    return cloneDeep(u);
    });
    }
    ```
    Mirachael
        14
    Mirachael  
    OP
       224 天前
    @Chad0000 #12 你应该就是没有使用 push 模式,因为默认的更新策略就是会把组件树的所有节点都检查一遍,跟使用 getter/setter 没有关系,你可以看下这篇文章 https://juejin.cn/post/6844904017836032007
    Mirachael
        15
    Mirachael  
    OP
       224 天前
    @Mirachael #13 但是又无法解释,不使用深拷贝,只返回 user.addresses 时,视图能更新这种情况。。。
    Chad0000
        16
    Chad0000  
       224 天前
    @Mirachael #14
    我知道启用 Push 会更好,但我的问题是我在未遇到性能问题前我只想保持简洁高效,能用 Push 最好但比较繁琐,带来不必要的心智负担。我的 Default 模式下,将 Field 改成了 getter/setter 之后,直接导致性能严重下降:这已经明确证明了是与之前 Angular 能直接拿到对象或值相比,getter/setter 需要 Angular 每次都调用才能拿到值对比,而不是值就已经在它的处理层,只需要看值是否改变而已。

    我目前基于 Default 模式做了很多框架简化开发,并没有引起性能问题,我又不是做专业组件所以从来没弄过 Push 模式。
    Chad0000
        17
    Chad0000  
       224 天前
    @Mirachael #14

    我做了很长时间的 WPF 和 Xamarin 应用,它们使用的就是 MVVM 设计,而且只有 Push 模式,我倒是羡慕 Angular 有主动检查而且不太影响性能的这种做法,这样能使很多事情大大简化。我做的白板需要同时渲染上千个节点在 SVG 面板中,实现各种效果,目前一点儿也不卡顿,同时还保留了架构和数据的简洁。在做这个项目过程中,最简单地检测是否卡顿的方式就是选中若干个对象,能拖多快就拖多快,看拖动是否平滑(因为会带着箭头什么的一起变动)。目前说真的,Default 模式就足够高性能。
    wunonglin
        18
    wunonglin  
       223 天前
    @Mirachael #11

    默认比较方法就是 Object.Is 呀,怎么没关系啊?

    https://github.com/angular/angular/blob/a5b5b7d5ef84b9852d2115dd7a764f4ab3299379/packages/core/primitives/signals/src/equality.ts#L17


    解决办法上面都说啦
    1 、返回一个新的 user
    2 、将 addresses 也设置为 signal
    wunonglin
        19
    wunonglin  
       223 天前
    你试试吧 user 的 equal 值设置为() => true ,保你每次都会更新
    wunonglin
        20
    wunonglin  
       223 天前
    在以前的 v16 ,signal 是有三个方法,set ,update ,mutate 。

    set 、update 这两个设置之后,是会运行 equal 方法比较值的引用是否变化,才决定更新。

    mutate 相当于使用后一定更新,但是这个 mutate 已经下线了

    https://github.com/angular/angular/issues/52735#issuecomment-1804195570
    https://github.com/angular/angular/pull/52348
    chnwillliu
        21
    chnwillliu  
       223 天前 via Android   ❤️ 1
    @Mirachael 一个 signal 只有脏了才会 push dirty 的 notification 给下游的 computed ,effect 或者 view ,signal update 后脏不脏由 signal equality function 决定,默认用的 Object.is 检测前后两个值。所以并不需要 deep clone ,shallow clone 一样可以。

    直接返回 address 能 work 只是因为你在 app component 里没用 onPush strategy ,默认在 UI event 后更新 component 的 view ,而 view 中使用了 address ,address push 了新值自然能在 view 中体现出来,和 signal 没关系,你就是写个普通 get 函数一样 work 。
    chnwillliu
        22
    chnwillliu  
       223 天前 via Android
    @Chad0000 这个 feature 就是前端大热的 signal ,就是为了做到细粒度更新 view 而引入的,将来可是要干掉 zonejs ,颠覆 Angular 自上而下 change detection 的。
    Chad0000
        23
    Chad0000  
       223 天前 via iPhone
    @chnwillliu
    那它这么折腾侵入性太强了,反而不如 wpf 的实现优雅了。后者实现接口通知属性变更就行,我在实际使用过程中是偷懒反射然后动态触发通知,也算简洁和入侵不强。
    Mirachael
        24
    Mirachael  
    OP
       223 天前
    @wunonglin #19

    试过了,不是设置 true ,应该时设置为 false ,确实可以更新视图

    ```

    user = signal<User>({} as User, {
    equal: () => false,
    });
    ```
    Mirachael
        25
    Mirachael  
    OP
       223 天前
    @chnwillliu #21 shallow clone 确实可以,但是实际情况时 object 通常是带有方法的类实例,浅拷贝会导致 user signal 丢失方法。所以这种情况应该 deepClone
    wunonglin
        26
    wunonglin  
       223 天前
    @Mirachael #24 对,这里我写反了我勘误。但是你直接把 addresses 设置为 signal 不就好了吗
    chnwillliu
        27
    chnwillliu  
       223 天前 via Android
    @Mirachael 可以了解下 ngrx 的 singal store 和 deep signal 的概念哦
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5063 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 39ms · UTC 05:41 · PVG 13:41 · LAX 21:41 · JFK 00:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.