[Angular 基础] - Observable

[Angular 基础] - Observable

之前的笔记:


我以前对 Observable 的理解是 Promise 的一个超集,重新了解了一下,感觉这个说法不太对。更准确一些的说法应该是 stream of events,只不过大多数情况下,这个 events 是一个 Promise 而已。比如说 routes 的变化不属于 Promise,但是它是一个 event,也就自然可以被放到 Observable 里面去进行监听。

大多数 rxjs 相关的内容都在 rxjs 的 observable 里面说过了,这里主要记一些 angular 相关的知识,之后有需求的话继续拓展

先简单的说明一下项目结构:

bash 复制代码
❯ tree src/app/
src/app/
├── app-routing.module.ts
├── app.component.css
├── app.component.html
├── app.component.ts
├── app.module.ts
├── home
│   ├── home.component.css
│   ├── home.component.html
│   └── home.component.ts
└── user
    ├── user.component.css
    ├── user.component.html
    └── user.component.ts

3 directories, 11 files

路由的配置如下:

typescript 复制代码
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'user/:id', component: UserComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

简单来说,项目总共有两个路径,默认路径渲染 Home 组件,user/:id 路径渲染 User 组件

使用 observable

这里的变化会在 HomeComponent 中实现:

typescript 复制代码
@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
})
export class HomeComponent implements OnInit {
  constructor() {}

  ngOnInit() {
    interval(1000).subscribe((count) => {
      console.log(count);
    });
  }
}

其中,interval 是 rxjs 提供的一个 observable:

typescript 复制代码
export declare function interval(
  period?: number,
  scheduler?: SchedulerLike
): Observable<number>;

这个函数会在每个周期输出一个数字 (counter),效果如下:

需要注意这里有一个问题------当页面离开 Home 时,log 并没有停止。而是会持续输出。这个原因是因为这是一个 custom observable,它的创建和终结并不由 Angular 进行控制。因此如果不在 NgOnDestroy 中销毁,它就会一直 log 下去

最糟糕的情况是,如果当前组件被重复创建/毁灭多次,那么同一个组件就会有多个 observable 一起运行:

解决方法如下:

typescript 复制代码
export class HomeComponent implements OnInit, OnDestroy {
  private intervalSub: Subscription;

  constructor() {}

  ngOnInit() {
    this.intervalSub = interval(1000).subscribe((count) => {
      console.log(count);
    });
  }

  ngOnDestroy(): void {
    this.intervalSub.unsubscribe();
  }
}

效果如下:

可以看到,当切换到其他页面的时候,log 的输出停止了,而从其他页面回来的时候,subscription 中的输出被清零了

👀:subscribe 中的就是 observer,它主要的目的就是用来提供 Observable 中,对下一步操作的处理,包括 next, errorcomplete

创建 observable

这里会创建一个新的 observable:

typescript 复制代码
export class HomeComponent implements OnInit, OnDestroy {
  private intervalSub: Subscription;

  constructor() {}

  ngOnInit() {
    const customIntervalObservable = new Observable((observer) => {
      let count = 0;
      setInterval(() => {
        observer.next(++count);
        console.log(customIntervalObservable);
      }, 1000);
    });

    this.intervalSub = customIntervalObservable.subscribe((data) => {
      console.log(data);
    });
  }

  ngOnDestroy(): void {
    this.intervalSub.unsubscribe();
  }
}

实现效果如下:

可以看到,实现方面基本上没什么变化

error

报错也是一个常见的情况,observable 这里对于报错的处理也很简单,如下:

typescript 复制代码
const customIntervalObservable = new Observable((observer) => {
  let count = 0;
  setInterval(() => {
    observer.next(++count);

    if (count > 3) {
      observer.error('count is greater than 3');
    }
  }, 1000);
});

效果如下:

可以看到,一旦报错后,observable 就会被取消。在 subscription 处理报错方法如下:

typescript 复制代码
this.intervalSub = customIntervalObservable.subscribe(
  (data) => {
    console.log(data);
  },
  (error) => {
    console.error(error);
  }
);

效果如下:

complete

因为 observable 是一个 stream of events,所以它也有提供对于 events 终止的处理,也即是 complete:

typescript 复制代码
const customIntervalObservable = new Observable((observer) => {
  let count = 0;
  setInterval(() => {
    observer.next(++count);

    if (count === 2) {
      observer.complete();
    }

    if (count > 3) {
      observer.error('count is greater than 3');
    }
  }, 1000);
});

this.intervalSub = customIntervalObservable.subscribe(
  (data) => {
    console.log(data);
  },
  (error) => {
    console.error(error);
  },
  () => {
    console.log('%c' + 'Observable completes.', 'color: #198754; ');
  }
);

效果如下:

⚠️:如果在 complete 之前 subscription 就遇到报错,那么 complete 状态不会被触发

operators

先前的笔记有提到过,rxjs 说自己是 observable 版本的 lodash,其中一个原因就是 rxjs 提供了相当丰富的 operators,这样操作 observables 变得更加简单------虽然所有功能都是可以写在 observer 中进行实现的,不过 rxjs 提供的了很多开箱即用的 operators,可以极大地简化操作

pipe 的用处在于可以接受多个 operators,并合并成一个 operators。这样 operator 就可以承接上一个 observable 的返回值,对其进行修改,并 emit 到一个 新的 observable 中。这代表通过 pipe 对数据进行操作,并不会修改上一个 observable 中的值。

map

这里使用 map 做一个案例,修改的代码如下:

typescript 复制代码
const customIntervalObservable = new Observable((observer) => {
  let count = 0;
  setInterval(() => {
    observer.next(++count);
  }, 1000);
});

const pipedObservable = customIntervalObservable.pipe(
  map((data: number) => {
    console.log('%c' + 'Round: ' + (data + 1), 'color: #0d6efd; ');
    return data + 1;
  })
);

customIntervalObservable.subscribe((data) => {
  console.log('%c' + data, 'color: #198754; ');
});

pipedObservable.subscribe((data) => {
  console.log('%c' + data, 'color: #bada55; background-color: #222;');
});

输出结果为:

可以看到:

  • 原本的 observable ------即绿色字体的 observable------其 conter 从 1 开始输出
  • 通过 pipe,并使用 map 进行操作的 observable------即黄色字体的 observable------其 conter 从 2 开始输出

这也说明了通过 pipe 的 operator 对数据操作,并不会 修改上一个 observable 的值

filter

修改如下:

typescript 复制代码
const customIntervalObservable = new Observable((observer) => {
  let count = 0;
  setInterval(() => {
    observer.next(++count);
  }, 1000);
});

const pipedObservable = customIntervalObservable.pipe(
  filter((data: number) => {
    return data % 2 === 0;
  }),
  map((data: number) => {
    return 'Round: ' + (data + 1);
  })
);

pipedObservable.subscribe((data) => {
  console.log(
    '%c' + data,
    'color: #bada55; background-color: #222; padding: 5px;'
  );
});

其中:

  • filter 过滤掉所有的偶数值
  • map 只是单纯返回会被 log 的字符串

最终运行结果为:

⚠️:这里运行的 observable 只有一个,就是被 subscribed 的 pipedObservable

subjects

当需要触发一些数据的变动,并将其传递到不同的组件时,之前的做法都是使用 EventEmitter 去实现,如下面这个 service 案例:

typescript 复制代码
import { EventEmitter, Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  activatedEmitter = new EventEmitter<boolean>();
}

实际上在应用的时候,也可以用 subject 去取代 EventEmitter,用法如下:

typescript 复制代码
export class UserService {
  activatedEmitter = new Subject<boolean>();
}

替换了 Subject 后,就不能使用 emit,而是需要使用 next 去触发事件:

typescript 复制代码
export class UserComponent implements OnInit {
  id: number;

  constructor(
    private route: ActivatedRoute,
    private userService: UserService
  ) {}

  ngOnInit() {
    this.route.params.subscribe((params: Params) => {
      this.id = +params.id;
    });
  }
}

其他 subscribe 的使用方法一致。

二者使用上的区别主要是是否与 @Output 搭配使用,本质上来说 EventEmitter 的 interface 是这样的:

typescript 复制代码
/**
 * Use in components with the `@Output` directive to emit custom events
 * synchronously or asynchronously, and register handlers for those events
 * by subscribing to an instance.
 */
export declare interface EventEmitter<T> extends Subject<T> {}

它的初衷就是为了搭配 @Output 使用的,因此它做了一些限制,比如说只有父组件可以 subscribe @Output

项目部分更新

这里是对 第一个 Angular 项目 部分的更新,主要就是清理了一些代码,将 EventEmitter 更换成了 Subject,其他方面没什么大的变动

除了清理部分的代码修改如下:

相关推荐
Мартин.2 分钟前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。1 小时前
案例-表白墙简单实现
前端·javascript·css
数云界1 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd2 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常2 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer2 小时前
Vite:为什么选 Vite
前端
小御姐@stella2 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing2 小时前
【React】增量传输与渲染
前端·javascript·面试
GISer_Jing2 小时前
WebGL在低配置电脑的应用
javascript
eHackyd2 小时前
前端知识汇总(持续更新)
前端