Angular18 Signals (对比Vue3)

350 阅读3分钟

Angular 前前后后接触了一个多月了,完成了好几次用户管理的demo,写一下使用感触。我从最开始的:

  • 在组件中使用原始变量+Default的变更检测策略实现用户管理的需求(贴个链接:gitee.com/zhang-rui-x… ),项目使用声明式编程写法。

学习了Angular基础的应该都会知道Default策略不是一个好的选择,因为在任何一个组件触发变更检测的时候,整个组件树都会执行变更检测以更新状态。这样显然会造成一定的性能问题。

到后来

  • 使用OnPush策略+RxJS两者配合实现更优雅的局部变更检测。

可以查看git仓库的master或者RxJS分支

  • 再到最后的使用Angular新特性:Signals

总结一下 Signals

自从Angular引入了Signals,对比一下Vue3,就像对比原神绝区零 (个人观点):

  1. signal:类似Vue3中的ref
  2. computed:跟Vue3计算属性类似,里面要写纯函数。
  3. effect:类似Vue3中的watch(),Angular中的 effect 中,一般要以声明式的写法来获取并使用依赖的signal。
  4. input:实现自定义组件单项绑定。Angular也有@Input,能实现类似的效果,但是input可以直接从父组件传signal到子组件,操作数据更加方便。
  5. model:实现自定义组件双向绑定。用法与input类似,只是数据是双向绑定的。Vue3实现双向绑定众所周知是使用defineProps+defineEmits,个人认为Angular的实现更简单一点。

个人认为既然引入了响应式,那么他们的概念都大差不差,确实Angular使用Singals之后,用起来跟Vue3很像。

一些代码示例 Singals

组件结构:

  • user-info(父组件)
    • user-list(子组件1)
    • user-detail(子组件2)

user-info(父组件)

Components

export class UserInfoComponent implements OnInit {
  realName = signal<string>('')
  pageNumber = signal<number>(1);
  users = signal<User[]>([])
  private pageSize = signal<number>(10);

  constructor(private userService: UserService,
              private injector: Injector) {
  }

  ngOnInit(): void {
    effect(() => {
      const pageNumber = this.pageNumber()
      const pageSize = this.pageSize()
      const realName = this.realName();
      this.userService.getUserList(
        {realName: realName},
        pageNumber,
        pageSize
      ).subscribe((res) => {
        if (this.pageNumber() == 1) {
          this.users.set(res.content)
        } else {
          this.users.update(users => [...users, ...res.content])
        }
      })
    }, { injector: this.injector });
  }

  userInfoChange(updatedUser: User) {
      this.users.update(users => {
        let newUsers = [] as User[]
        if (users.length > 0) {
          newUsers = [ ...users ]
          const index = users.findIndex(user => user.id == updatedUser.id)
          newUsers[index] = {...users[index], ...updatedUser}
        }
        return newUsers
      })
  }

  /**
   * 滚轮致页码变化
   */
  pageNumberChange() {
    this.pageNumber.set(this.pageNumber() + 1)
  }

  /**
   * 姓名搜索框发生变化
   */
  realNameChange(realName: string) {
    this.resetUsers()
    this.realName.set(realName)
    this.pageNumber.set(1)
    this.pageSize.set(10)
  }

  private resetUsers() {
    this.users.set([])
  }

}

html

<div class="wrapper">
  <div class="return-class">
    <nz-row>
      <nz-col nzFlex="1">
        <span class="title">Ant Design Of Angular</span>
      </nz-col>
      <nz-col>
        <button class="return-button-class" nz-button nzType="primary" routerLink="/home/user">返回</button>
      </nz-col>
    </nz-row>
  </div>

  <div class="content-wrapper">
    <div nz-row>
      <div class="list-wrapper" nz-col nzSpan="4">
        <user-list [users]="users()" (onPageNumberChange)="pageNumberChange()" (onRealNameChange)="realNameChange($event)"></user-list>
      </div>
      <div nz-col nzSpan="18" nzOffset="1">
        <user-detail (onRealNameChange)="userInfoChange($event)"></user-detail>
      </div>
    </div>
  </div>
</div>

user-list 子组件1

Components

export class UserListComponent implements OnInit {

  users = input<User[]>()
  @Output() onPageNumberChange: EventEmitter<undefined> = new EventEmitter<undefined>();
  @Output() onRealNameChange: EventEmitter<string> = new EventEmitter<string>();
  realName = signal<string>('')

  constructor(private router: Router,
              private injector: Injector) {
  }

  ngOnInit(): void {
    effect(() => {
      const realName = this.realName()
      this.onRealNameChange.emit(realName)
    }, {injector: this.injector});
  }

  /**
   * 搜索框输入姓名发生变化
   */
  realNameChange(name: string) {
    this.realName.set(name)
  }

  /**
   * 滚动触发
   */
  onScrollend() {
    // 触发滚轮说明需要进行分页查询了
    this.onPageNumberChange.emit()
  }

  /**
   * 点击list-item查看用户详情
   * @param user
   */
  checkUserDetail(user: User) {
    this.router.navigate([`/userInfo/${String(user.id)}`])
  }

}

html

<cdk-virtual-scroll-viewport itemSize="73" class="infinite-container" (scrollend)="onScrollend()">
  <nz-list [nzHeader]="header">
    <ng-template #header>
      <input (ngModelChange)="realNameChange($event)" [ngModel]="realName()" nz-input placeholder="输入姓名以查询">
    </ng-template>
    <nz-list-item class="list-item" *ngFor="let user of users()" (click)="checkUserDetail(user)">
      <span style="margin-left: 10px" nz-typography>{{ user.realName }} </span>
    </nz-list-item>
  </nz-list>
</cdk-virtual-scroll-viewport>

user-detail 子组件2

Components

export class UserDetailComponent implements OnInit {

  @Output() onRealNameChange: EventEmitter<User> = new EventEmitter<User>();

  realName = signal<string>('');
  username = signal<string>('');
  address = signal<string>('');
  age = signal<string>('');
  private userId = signal<string>('');

  constructor(private userService: UserService,
              private route: ActivatedRoute,
              private messageService: NzMessageService,
              private injector: Injector) {
  }

  ngOnInit(): void {
    this.route.params
      .subscribe({
        next: params => {
          const userId = params['userId'];
          this.userId.set(userId);
        },
        error: error => {
          console.error('获取路由id失败:' + error.message);
        }
      });

    effect(() => {
      const userId = this.userId();
      if (userId) {
        this.userService.getUserById(userId)
          .subscribe({
            next: res => {
              if (res.username) {
                this.username.set(res.username);
              }
              if (res.realName) {
                this.realName.set(res.realName);
              }
              if (res.age) {
                this.age.set(String(res.age));
              }
              if (res.address) {
                this.address.set(res.address);
              }
            },
            error: error => {
              this.messageService.error('获取用户详情失败:' + error.message);
            }
          });
      }
    }, {injector: this.injector});
  }

  realNameChange(realName: string): void {
    this.userService.editUser({
      id: this.userId(),
      realName: realName
    }).subscribe({
      next: () => {
        const newUser: User = {id: this.userId(), realName};
        this.onRealNameChange.emit(newUser);
        this.realName.set(realName);
        this.messageService.success('修改成功');
      },
      error: error => {
        this.messageService.error('修改失败:' + error.message);
      }
    });
  }

  ageChange(age: string): void {
    this.userService.editUser({
      id: this.userId(),
      age: Number(age)
    }).subscribe({
      next: () => {
        this.age.set(age);
        this.messageService.success('修改成功');
      },
      error: error => {
        this.messageService.error('修改失败:' + error.message);
      }
    });
  }

  addressChange(address: string): void {
    this.userService.editUser({
      id: this.userId(),
      address: address
    }).subscribe({
      next: () => {
        this.address.set(address);
        this.messageService.success('修改成功');
      },
      error: error => {
        this.messageService.error('修改失败:' + error.message);
      }
    });
  }
}

html

<nz-card>
  <nz-descriptions [nzTitle]="realName()">
    <nz-descriptions-item nzTitle="姓名">
      <p nz-typography nzEditable [nzContent]="realName()" (nzContentChange)="realNameChange($event)"></p>
    </nz-descriptions-item>
    <nz-descriptions-item nzTitle="年龄(岁)">
      <p nz-typography nzEditable [nzContent]="age()" (nzContentChange)="ageChange($event)"></p>
    </nz-descriptions-item>
    <nz-descriptions-item nzTitle="用户名">{{ username() }}</nz-descriptions-item>
    <nz-descriptions-item nzTitle="家庭住址">
      <p nz-typography nzEditable [nzContent]="address()" (nzContentChange)="addressChange($event)"></p>
    </nz-descriptions-item>
  </nz-descriptions>
</nz-card>