[從 0 開始的 Angular 生活]No.30 解釋單向資料流與不可變的物件

前言

單向資料流與不可變的物件,究竟這是什麼意思呢?

目前架構

我們目前的專案結構大致如下:

  • ArticleList 元件 - 擁有所有的資料,並透過屬性繫結把資料傳給子元件
    • ArticleHeader - 擁有屬性 item ,承接來自父元件的物件資料
    • ArticleBody - 擁有屬性 item ,承接來自父元件的物件資料

功能需求

假設今天有一個需求是要更改文章標題,最簡單的做法就是直接從 ArticleHeader 動手,修改 item 這個物件的屬性值,這個屬性值只要一改變,父元件 data 下的這些物件值也會跟著改變。

而雖然這個改法很簡單,不過建議最好不要這麼做。

盡量不要在子元件內直接對資料進行任何修改,否則元件間的相依性會太重,當有 Bug 產生時也不容易除錯。

最好是透過單向資料流的方式來達成。

單向資料流

單向資料流的意思就是,所有資料的變更永遠是從上層元件傳給下層元件。

換句話說,比較好的做法是:

  • ArticleHeader 內提供一個編輯的介面,讓使用者設定新的標題
  • 當確定送出時才把要變更的內容透過事件繫結傳遞給父元件,也就是 ArticleList 元件
  • ArticleList 元件做實際物件的操作 (變更資料等等)

不可變的物件

JavaScript 物件本身是可變的,所以當物件發生改變時如果希望另一個元件也能知道這個物件發生改變,這在 JavaScript 內非常不容易做到。

舉例來說,在 ArticleHeader 內修改了 item 物件,那麼 ArticleBody 如何知道 ArticleHeader 內修改了 item 物件的哪個屬性呢?

要怎麼樣 Angular 這個物件確實發生了改變、讓 ArticleBody 能反映出相對應的資料呢?

Angular 確實做得到,但是這樣效能會很差。

像是在 ArticleHeader 新增了一個點擊事件,並且觸發一個 test 方法,執行 item 物件內 summary 屬性值的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<header class="post-header" (click)="test()">
<h2 class="post-title" >
<a [href]="item.href">{{ item.subject?.title|lowercase|slice:-20 }}</a>
<small>{{item.subject?.subtite}}</small>
</h2>
<div class="post-info clearfix">
<span class="post-date"><i class="glyphicon glyphicon-calendar"></i>{{item.date|date:'yyyy-MM-dd HH:mm'}}</span>
<span class="post-author"><i class="glyphicon glyphicon-user"></i><a
href="http://blog.miniasp.com/author/will.aspx">{{item.author}}</a></span>
<span class="post-category"><i class="glyphicon glyphicon-folder-close"></i><a
[href]="item['category-link']">{{item.category}}</a></span>
</div>
<span><button (click)="deleteArticle()">刪除</button></span>
</header>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component, OnInit, Input, Output, EventEmitter} from '@angular/core';

@Component({
selector: 'app-article-header',
templateUrl: './article-header.component.html',
styleUrls: ['./article-header.component.scss']
})
export class ArticleHeaderComponent implements OnInit {
@Input()
item;

@Output()
delete = new EventEmitter<any>();
constructor() { }

ngOnInit() {
}
deleteArticle() {
this.delete.emit(this.item);
}
test() {
console.log(this.item);
this.item.summary = 'aaa';
}
}

點擊後的確影響到 ArticleBody 內的資料了,但是非常不推薦這麼做。

好的作法

  • 當在 ArticleHeader 修改了 item 物件的內容,應該把修改過的內容往上傳給父元件
  • 在父元件內找出是哪一筆資料更動,假設是第一筆
    • 接著重新建立一個全新的物件、連同陣列的部分也產生新的

而這樣就是所謂的不可變的物件特性

只要物件不管哪個屬性發生變更,通通都重新產生物件的話,在 Angular 內很容易就可以辨識出那些物件發生了變化。

舉例來說

  • 只要陣列變成新的,那麼 ngFor 就會重新渲染
  • 陣列的其中一個元素的物件是新的,Angular 就會知道這個物件在做資料繫結時要把新的資料渲染到畫面上。
0%