在 Angular 19 中,實驗性生命週期鉤子 afterRenderEffect 允許開發人員更新 DOM。
與 afterNextRender 和 afterRender 一樣,afterRenderEffect 也有四個階段:earlyRead、write、mixedReadWrite 和 read。
在此示範中,我將使用 afterRenderEffect 附加新的圖表數據和變更圖表顏色。 afterRenderEffect 註冊 effect 並在渲染所有元件後運行它。
write 階段之前從 DOM 讀取的階段。如果讀取可以等到寫入階段之後,則優先選擇read階段。。在此階段切勿讀取 DOM。在這篇文章中,我描述如何使用 afterNextRender 在畫布上新增圖表,以及如何使用 afterRenderEffect 重繪圖表。
在此示範中,我想使用第三方圖表庫 Chart.js 在畫布元素上渲染長條圖。因此,我實作了 afterNextRender 鉤子來將圖表插入到畫布中。然後,我使用 RxJS timer運算子和 toSignal 產生新的圖表數據,並每秒將其附加到圖表數組中。
為了使示範具有互動性,顏色下拉清單會變更條形的顏色。我將實作兩個 afterRenderEffect 鉤子;第一個鉤子將新的圖表資料附加到圖表資料數組中,第二個鉤子修改圖表顏色。
@Component({
 selector: 'app-root',
 imports: [FormsModule],
 template: `
   <h1>Hello from AfterRenderEffect!</h1>
   <div style="width: 400px;">
     <div>
       <label>
         Bar Color:
         <select [(ngModel)]="barColor">
           @for (c of barColors(); track c.id) {
             <option [value]="c.id">{{ c.color }}</option>
           }
         </select>
       </label>
     </div>
     <canvas #canvas></canvas>
   </div>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App implements OnDestroy {
 barColors = signal([
   { id: 'red', color: 'Red' },
   { id: 'pink', color: 'Pink' },
 ]);
 data = signal([
   { year: 2022, count: 30 },
   { year: 2023, count: 4 },
 ]);
 barColor = signal('red');
 numBars = signal(0);
 chart: Chart | null = null;
  
 constructor() {
   // Implement afterNextRender and afterRenderEffect here
   afterNextRender({});
   
   afterRenderEffect({});
   afterRenderEffect({});
 }
 
 ngOnDestroy(): void {
   this.chart?.destroy();
 }
}
npm i --save-exact chart.js
afterNextRender 生命週期鉤子在下次變更偵測後執行一次。因此,write 階段是將新圖表插入 DOM 的理想入口點。
canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas')
nativeElement = computed(() => this.canvas().nativeElement);
首先,我使用 viewChild 函數來取得畫布的 ElementRef。 nativeElement 是一個存取 HTMLCanvasElement 實例的計算訊號。 在建構函式中, afterNextRender 生命週期鉤子用這些訊號初始化圖表。
type ChartDataType = {
 year: number;
 count: number;
};
function initializeChart(canvas: HTMLCanvasElement, data: ChartDataType[], backgroundColor: string) {
 return new Chart(canvas,
   {
     type: 'bar',
     data: {
       labels: data.map(row => row.year),
       datasets: [
         {
           label: 'Acquisitions by year',
           data: data.map(row => row.count),
           backgroundColor,
         }
       ]
     }
   }
 );
}
上面的輔助函數接受畫布、初始圖表資料和圖表顏色來建立並傳回圖表。
constructor() {
   afterNextRender({
     write: () => {
       console.log('afterNextRender write is called');
       this.chart = initializeChart(this.nativeElement(), this.data(), this.barColor());
     }
   });
}
在建構函式中,我實作了 afterNextRender 生命週期鉤,並在 write 階段將圖表附加到畫布。
我已成功將 JavaScript 圖表插入 DOM 中。 DOM 讀取和寫入只執行一次,因此畫布不會錯誤地顯示多個圖表。
我想在鉤子的 earlyRead 和 write 階段附加圖表資料。然後,我將重構鉤子以避免 earlyRead 階段並僅在 write 階段執行更新。
chartData = toSignal(timer(100, 1000)
    .pipe(take(15)), { initialValue: -1 });
這個 timer 運算子發出 15 個整數,我將在 earlyRead 階段每秒匯出圖表資料。產生資料後,write 階段接收圖表資料物件並在圖表中顯示新的長條圖。 toSignal 從 Observable 創建一個唯讀訊號,以便 afterRenderEffect 鉤子可以在 earlyRead 階段讀取它。
constructor() {
  afterRenderEffect({
     earlyRead: () => {
       const index = this.chartData();
       return index < 0 ? undefined :
         { year: 2024 + index, count: Math.floor(Math.random() * 20) + 2 } as ChartDataType;
     },
     write: (randomData) => {
       const chartData = randomData();
       if (chartData) {
         const { year, count } = chartData;
         const chart = this.chart?.data; 
         chart?.labels?.push(year);
         chart?.datasets.forEach(({ data }) => data.push(count));
         this.chart?.update();
       }
       return chartData;
     }
   });
earlyRead 階段讀取圖表資料訊號,產生並傳回隨機資料。在 write 階段,回呼函數的第一個參數是隨機數據,它被附加到圖表的資料數組中。
如果讀取操作可以在寫入操作之後發生,則 Angular 文件偏好使用讀取階段。我將重構 Observable 以產生圖表數據,以消除 earlyRead 階段。
chartData = toSignal(timer(100, 1000)
   .pipe(
     take(15),
     map((i) => ({ year: 2024 + i, count: Math.floor(Math.random() * 20) + 2 } as ChartDataType))
   ), { initialValue: undefined });
Observable 使用 map 運算子來產生圖表資料。
afterRenderEffect({
     write: () => {
       const chartData = this.chartData();
       if (chartData) {
         const { year, count } = chartData;
         const chart = this.chart?.data;
         chart?.labels?.push(year);
         chart?.datasets.forEach((dataset) => dataset.data.push(count));
         this.chart?.update();
       }
       return chartData;
    }
});
重構 chartData 訊號後,write 階段存取訊號的值並執行相同的邏輯以將 chartData 附加到圖表的data array 。
另一個案例是當使用者從下拉清單中選擇顏色名稱時更新圖表顏色。這個簡單的下拉清單應用 two-way data binding 將 ngModel 綁定到 barColor 訊號。
<div>
    <label>
        Bar Color:
        <select [(ngModel)]="barColor">
          @for (c of barColors(); track c.id) {
             <option [value]="c.id">{{ c.color }}</option>
          }
        </select>
    </label>
</div>
barColor = signal('red');
當使用者進行選擇時,應用程式會更新 this.barColor 並觸發 afterRenderEffect 生命週期鉤子。因此,我將邏輯放在 afterRenderEffect 生命週期鉤子中來更新圖表顏色。
afterRenderEffect({
     write: () => {
       this.chart?.data.datasets.forEach((dataset) => dataset.backgroundColor = this.barColor());
       this.chart?.update();
       return this.barColor();
     }
});
在 afterRenderEffect 鉤子中, dataset.backgroundColor = this.barColor(); 更新顏色和 this.chart.update(); 再次更新圖表。