angular2 学习笔记 ( 状态管理 state management )

时间:2022-03-23 07:57:41

更新: 2020-05-13

有些冷门的事件, 是没有在 ngZone 的范围的. 比如 ResizeObserver

https://dev.to/christiankohler/how-to-use-resizeobserver-with-angular-9l5
所以要在触发后跑的话,就可以写 this.ngZone.run(() => {  this.cdr.markForCheck();  });
 

更新: 2019-12-18

ngZone runOutsideAngular & runTask

有时候在调用第三方插件时,内部可能会使用 setInterval

比如 oidc client, 而 ngZone 默认情况下会拦截这种情况然后触发 app.tick

虽然在 onpush 的情况下影响很小,但是如果依然不希望它发生。

可以使用 runOutsideAngular

this.ngZone.runOutsideAngular(() => {
setInterval(() => {
this.ngZone.runTask(() => {
this.cdr.markForCheck();
});
}, 1000);
});

在 outside angular 里面调用任何东西都不会被 zone 拦截. 那么如果我们在里面某些情况下希望它触发 app.tick 的话,可以在内部调用 runTask 然后就可以像平时那样 mark for check 了.

更新 : 2019-12-14 

event and detech change 次数

之前不知道在哪里讲过, 一个 element 每一个不同的事件触发,ng 都会 digest 一轮, 比如 mousedown -> click

比如 element a bind 了 mousedown 和 click 那么一个点击就触发了 2 个 digest

但是如果同一个 element bind 了多个 click 事件, 比如放了 3 个指令,每一个指令都 bind click event

最后只会 digest 一次.

又但是, 如果 parent component 也 bind 了一个 click 事件, 那么就会变成 2 次 digest.

更新 : 2019-01-10 

对 redux 的思考

假设有一个场景

ParentComponent

ChildAComponent

ChildBComponent

parent.person 是一个对象

<child-a [person]="person" ></child-a>
<child-b [person]="person" ></child-b>

我们把对象传入 ChildA 和 ChildB

现在如果我们在 ChildA 里修改 person.name = 'dada';

ChildB 是无法同步更新的, 这是因为使用了 onpush, person ref 没有改变.

那我们用 immutable 试试, 在 ChildA 里调用 person = { ...person, ...{ name : 'dada' } }

还是不行丫, ChildA 直接覆盖 input 属性 person, 那就断了链接了丫.

所以只能交由 Parent 调用了 person = { ...person, ...{ name : 'dada' } }

成了.

上面就是我对 redux 的理解.

更新 : 2019-01-07

不错的文章

refer :

https://blog.angularindepth.com/if-you-think-ngdocheck-means-your-component-is-being-checked-read-this-article-36ce63a3f3e5

更新 : 2018-12-19 

async pipe 的好与坏

在使用 onpush 的情况下,一但遇到异步情形,比如 ajax 获取数据, 我们就需要调用 markforcheck

常见的情形是这样的.

有个 ajax 方法

class Person {
name: string;
} function getPersons(): Promise<Person[]> {
return new Promise((resolve) => {
setTimeout(() => {
const person = new Person();
person.name = 'keatkeat';
resolve([person]);
}, 3000);
});
}

有个 controller

export class TestAsyncPipeComponent implements OnInit {
constructor(
private cdr : ChangeDetectorRef
) { } persons: Person[] | undefined; async ngOnInit() {
this.persons = await getPersons();
this.cdr.markForCheck();
}
}

view

<div *ngIf="persons !== undefined; else loading" >
{{ persons[0].name }}
</div>
<ng-template #loading >loading...</ng-template>

这样的写法对比 async 好处是直观.

controller 没有 stream 的概念, 通过一个 await 获取到最终的值.

缺点是需要调用 mark for check, 同时要顾虑到 person 在 ajax 还没有返回的时候是 undefiend 的.

如果换成 stream + async pipe 的写法是

export class TestAsyncPipeComponent implements OnInit {

  constructor(
) { } personsPromise: Promise<Person[]>; ngOnInit() {
this.personsPromise = getPersons();
}
}

view :

<div *ngIf="personsPromise | async as persons; else loading" >
{{ person[0].name }}
</div>
<ng-template #loading >loading...</ng-template>

看得出来, controller 干净了许多, view 只是多了一个 | async.

由于 controller binding 的是 promise 所以在 init 时就有了,没有了 undefined 的概念, 然后 async pipe 负责了 mark for check 所以 controller 也无需注入 cdr 了.  (注 : 在 promise 还没有 resolve 的时候 async pipe 返回值是 null 而不是 undefined 哦)

在这种简单情况下, async pipe 是不错的. 但如果遇到复杂一些些的情况就不一定了.

比如现在 getPerson 只返回 10 条数据, 有一个 show all 按钮, 用户按下后需要返回所有数据 (这是很常见的情况)

如果我们使用的是第一个方案, 那么我们只要写一个 click 事件, 然后写一个 show all loading, ajax 回来后把 loading 关掉, 然后赋值给 persons 就可以了.

方案二就不同了. 由于 controller view binding 的是 promise, 而我们不可能替换掉 promise 如果替换 promise loading 就会出现了. 这不是我们要的.

那如果依照 stream 的概念去修改的话, persons 这个 stream 是会发布 2 次值得. 我们需要用一个 subject 来表示或者通过 merge(firstload, showAll)

2 个 action 表示, 这有点像做 ngrx 了. 一个 state 2个 action 可改变它. 然后又要处理 2 个 loading (first big loading and show all small loading..)

整个感觉就差很多了.

这里顺便说说对 ngrx 的想法 (虽然我只是看了看教程)

store 保管了所有的 state, 它只提供 2 个方式让外部使用,一个是 get () 返回 observable, 另一个 publish action (payload) 一个事件名字 + parameter

重点是 1. 外部是不可以直接改变 state. 2. 所有 state 都是 observalbe

controller 要 state 就跟 store 拿 ( 比如上面的 persons, this.persons$ = store.get('person'))

init 时, controller 要 publish 一个 init action, 可能叫 firstLoadPerson

这时 reducer 出现了, reducer 负责监听所有 action 然后去修改 state

这个阶段修改的 state 是 loading = true;

同时呢, 另一个叫 effect 的出现了, 它也监听 action

它的工作不是修改 state, 而是做一些事情,比如发 http 请求去 ajax persons

effect 监听一个 action 然后... 返回另一个 action, 返回 action 意思是 publish this action with payload

比如 personLoadSuccess(personsData) 成功了.

reducer 又会监听到这个 action 然后修改 state

比如 loading = false; persons = personsData.

整个过程中 controller 就监听 state 和 publish action 就好了, 既然 state 是 observable 通常就用 async pipe 来处理了咯.

那么 show all 也是一样的做法, publish 一个 show all action

show all loading obserable 被 reducer 改变

effect 去 ajax 然后发布 done action reducer 又改变.

大概就是这样一个流程.

我个人的感觉是 redux 这种方式职责分的很清楚, 所以经常的感觉就是, 处理大事分工很好,处理小事分工很废...

最后说说感想, 虽然 ng 给了我们很多很优秀的方案实现需求,但是我们依然需要搞清楚不同情形下适合用什么方案. 用错了的话,可能本末倒置.

更新 : 2017-12-29 

ng5 移除 zone.js

https://zhuanlan.zhihu.com/p/29577461

zone 的用途就是拦截游览器事件, 比如 click, ajax, timeout 等

ng 就是用它来实现 dirty check 的, 或者叫 change detech

这个很方便, 但是每一次事件触发都来个 change detech 有时候会很浪费性能.

所以有了 onPush + markforcheck

如果你想更极端一点,干脆就连 zone 都不要了. 自己调用 changeDetechRef 来管理更新.

platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' })
.catch(err => console.log(err));

运行上面代码后,你会发现, ng 不会更新 view 了.... , click, ajax , async pipe 统统不灵了.

这时我们可以通过一些方法来让 ng 更新.

1. ApplicationRef.tick() 这个就是 root detech change. 然后会一直往下流, 之前有讲过 detech change 的流程.

2. changeDetectorRef.detectChanges() or markForCheck()

这 2 个 用途和区别之前有讲过了,  markforcheck 只是 mark 而已, 之所以你调用 markforcheck 会 update view 是因为

你调用它的时候刚巧在 ngZone scope 内. 而我们把 zone 移除后, 单单调用 markforcheck 就没用了,这也是为什么 pipe async 也不 work 了. 因为它只是调用了 markforcheck

那我们调用 markforcheck 后我们必须在调用一个 app.tick() or detechchange()  让一切正常工作.

这就是移除 zone 的代价.

再一次提醒 :

app.tick 就是 root component detechchange

detech change 是当前的 component 一直往下到子孙 compoent docheck + detech change ...

markforcheck 是当前的 component 一直往上到 root 把每一个 component mark to check (等待下一次 detech change 触发就 update view).

更新 : 2017-08-25 

markforcheck 常用在 onPush 情况下,但是能调用的地方依然是基于 ngZone 内的. 如果 ngZone 外的情况, markForCheck 是不灵的, 用 detechChange 倒可以..

this.ngZone.run((0 => { this.cdr.markForCheck() }) 这样就 OK.

markforcheck 和 click event 类似, self to ancestor (祖先root) 都会detechChange, 而 detechChange 则是 self to descendants (子孙).

往上的 detechChange 是肯定一条龙触发到 root 的, 往下的 detechChange 则遇到 onPush component 只做 docheck, 不一定会 detechChange, 如果有detechChange 则继续往下走,不然就停止了.

更新 : 2017-08-06

doCheck != detectChange

只要 parent component 发生了 detechChange, 那么其所有的 children 都会 doCheck.(我们拦截与否那是另外一件事, )

parent 为什么会发生 detechChange, 很多原因啦, 可以是 (click), 可以是 markForcheck 等等等. 参考我之前写的.

如果触发 doCheck 的 component 是 ChangeDetectionStrategy.OnPush

那么 component 不一定会 detechChange. detechChange( @input changed 就会自动 detech change ) , 如果没有 detech change 那么就中断了 . 子孙就不会触发任何东西了。这也是 angular 优化的原理.

相反如果没有设置 ChangeDetectionStrategy.OnPush , angular 就会设置此 component detechChange. 而由于 detechChange 了, 其所有的 children 又会触发 doCheck 然后一直循环到子子孙孙.

所以记住 parent detechChange -> all children will doCheck -> if children not OnPush then continue detechChange 一直下去...

if children is OnPush then no detechChange(是否 detechchange depend on input and docheck ) then 结束 ( but we still have chance 拦截 doCheck and manual call markForCheck to let it detechChange )

 

更新 : 2017-03-27

change detech 在没有使用 onpush 的情况下是很聪明的

ajax, timeout, document.addeven 都可以监听到.

-detech
-reattach
-markascheck

component detech 之后 ng 在检测时就不会理它了。
需要 component 自己维护
可以调用 detechChange 来使它 check
也可以调用 reattach 让它加回去, 调用这个也会马上 detechChange
detech 后使用 markascheck 是无效的. 这也是 markascheck 和 detechchange 最大的区别.
markascheck 是用于 onpush 场景下的. 而 detech,detechChange,reattach 跟 onpush 则没有关系.

onpush 之后
timeout, ajax, document.addevent 统统不会跑了 (用 ng bind 的 event 会跑, e.g. (click)="这个会跑" )
使用 markascheck 来手动让它 check

@input 传进来的值 change 了会触发 check, 不过必须是值类型,如果是引用类型比如 object array, 那么你就累了,因为你需要自己 watch 这个 @input

ng 没有 $watch 了, 所以一般的做法就是 "强转" 这个值取 immutable 或则是 observable

service 更惨, 一定要用 observable, 你用 immutable 都没有用.

redux 走的是 immutable 的路, mobx 走的是 obserable 的路. 个人比较需要 mobx 大家有兴趣可以多留意.

更新 : 2017-02-22

在实际开发中对状态有了多一些的体会.

在没有使用 state management tools or framework (like redux) 的情况下, angular 怎么走..

先说说 angular 对状态变化和渲染.

ng认为能改变状态的只有 event (click,timeout,ajax等)

所以只要检测到 event 触发 (通过zone.js) 就让所有的模板重新去抓取值 (模板上的表达式)

然后进行对比和渲染.

这个好理解效果也不错. 但是它只能帮你搞定模板渲染.

如果你想写依赖的话, angular2 没有 $watch 了, 所有的依赖值应该使用 getter 去写.

或者把依赖对象封装起来,类似 activateRoute 那样. 让大家可以去 subsribe 它.

有人可能会说没使用 onPush 性能会不好, 主要还是看项目, 如果大部分的 component 都不伤性能的话, 能不写 onPush 就别写了.

只在需要性能优化的 component 手动调用 ChangeDetectorRef.detach, detectChanges 就可以了。

以上前提是在不使用 state management tools 的情况下哦.

redux 真的好吗?

refer :

https://medium.com/@machnicki/why-redux-is-not-so-easy-some-alternatives-24816d5ad22d#.1a8o2ehv9

http://blog.zhangrgk.ninja/program/2016/12/06/redux-is-not-good-enough.html

http://blog.angular-university.io/angular-2-redux-ngrx-rxjs/

refer :

http://cn.redux.js.org/  (redux 中文版)

https://egghead.io/courses/getting-started-with-redux  ( 入门 videos, 偏向 react )

https://egghead.io/courses/building-react-applications-with-idiomatic-redux ( 高级 videos, 偏向 react )

http://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html ( ng2 change detection 机制)

http://blog.angular-university.io/how-does-angular-2-change-detection-really-work/ ( ng2 change detection 机制 )

https://egghead.io/courses/building-a-time-machine-with-angular-2-and-rxjs (ngrx 收费 videos)

https://egghead.io/lessons/angular-2-ngrx-store-in-10-minutes ( ngrx 初次见面 )

因为前端越来越复杂, 所以需要定义更多的规则来管理,

state management 指的是对前端所有状态的管理方案.

ng 通过 zone 拦截 event, timeout , ajax callback 等
然后 component detector 从上到下检验一次, 然后更新 dom
by default 任何值都不会放过, obj is deep scan
如果你要优化可以使用2 种方式
1 .immutables
就是说如果对象的指针没有换就表示内部都没有改变 (不管是@input 还是use a service get a share obj)
如果发现 parent component no change then his child all will skip check, even child no say want to use immutables strategy,
所以它需要一整套的规范才可以做的好哦!要小心.
using angular2 and immutable.js

 

2. observer
监听依赖 stream 然后 manual check

 

可以把 ng2 的检测机制改成 onPush
它只有在5种情况下才会检测
1. component got fire event ( timeout, ajaxcallback 不算,dom event 而已 )
2. child component got check
3. manual check
4. input changed (obj no deep scan哦)

5. async pipe receive event

http://gold.xitu.io/entry/576cb79a2e958a0078d08b67
http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/
https://egghead.io/lessons/angular-2-ngrx-store-in-10-minutes
https://egghead.io/lessons/javascript-redux-describing-state-changes-with-actions

change detect, 单向数据流, 函数式 , redux, flux, ngrx
http://gold.xitu.io/entry/576cb79a2e958a0078d08b67 (flux redux 前世今生)
http://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html (angular change detection)
https://www.youtube.com/watch?v=CUxD91DWkGM ( youtube angular change detection )

redux :
keywords : dumb and smart component, redux, flux, ngrx
https://egghead.io/courses/getting-started-with-redux ( tube 教程入门 )
https://egghead.io/courses/building-react-applications-with-idiomatic-redux ( tube 教程高级 )
https://medium.com/javascript-scene/10-tips-for-better-redux-architecture-69250425af44#.yr1kwwlf4 ( 解释 redux 基本概念 )
http://cn.redux.js.org/docs/advanced/AsyncActions.html ( 中文官方文档, 在读着 )

react 提出了 flux 单向数据流的概念来简化 model 和 view 之前双向更新带来的复杂和难以跟踪.

flux 只是一个抽象的概念, 经过一阵子大家的研究, 最终诞生了 redux

redux 是一种 flux 的实现 + 变种.

redux 有几个好处.

1. 集中 state

2. undo/redo and time travel

3. 性能

4. easy test and track state

好处都围绕在维护和性能上.

我们不一定需要 redux, 就好像我们不一定需要设计模式一样, 它是一个平衡针对不同项目的管理方案.

redux 通过规范来达到可维护.

我们来看看它是怎么做的

规范 : redux 把前端所有的状态都放入一个对象里, 我们叫 "store", 这好比一个数据库.

好处 : 这样我们就可以一次看完整个 appication 的状态了.

规范 : redux 的 store 可以直接 json 化.

好处 : 这样可以很容易的导出, 导入和收藏,

规范 : 要修改 state, 就必须发布一个 action 给 store. store 会运行 reducer 创建出新的 state.

time travel 就是有了上面这 3 个规范才变的容易实现的.

规范 : reducer is immutable

好处 : 性能. immutable 在对比对象数组引用时可以不需要 deepEqual

规范 : reducer is pure function

好处 : pure function 的好处咯, 比如容易测试.

目前看上去不太好的地方 :

1.reducer 在维护共享 state 的地方有点弱, 因为 immutable 后我们失去了对象引用的好处. ( 目前替代引用的做法是把对象 Id 化, 引用的对象都用 Id 来替代, 然后再 convert 回去对象. )

参考 :

http://cn.redux.js.org/docs/advanced/AsyncActions.html

https://github.com/paularmstrong/normalizr

ng2 目前有 2 个 redux 的 tools

一个是 ngrx

一个是 ng2-redux

资源教程都挺少的, 所以我并没有打算现在就使用 redux.

以后使用了再分享心得吧.

更新 : 2017-02-10

提醒自己

1.onPush 的情况下,

如果我们想通过 service share value 那么 service 一定要提供 value change observer 让 component transaction subscribe

如果我们想通过 component input share value to child 的话,当 parent 要修改 value 时, 一定要使用 override referrence 的方式 ( immutable concept )

2.切记无论什么情况下, child component shouldn't change @input value, child 应该通过 @output 去让 parent change value, 单向数据流 concept.

3.parent 通过 #child or viewChild 修改 child 的 property 值, child 也是不会更新的哦. 可以在child inject ChangeDetectorRef 然后 parent 调用 changeDetectorRef.markForCheck();

4.流程 : 所有 event 运行结束后, event 触发地(dom event only, timeout 不算) 和 markForCheck 的 component 往上到 root 都要 check, 然后一直往下一层 check @input change 直到 not change 就停.