0

Binding 2 chiều và 1 chiều. Mình đã nhầm lẫn thế nào?

Bài viết này sẽ mang tính chất chia sẻ. Tôi sẽ kể lại kinh nghiệm xuất phát từ chính sai lầm của bản thân cho mọi người cùng đọc, vì tôi biết có thể nhiều anh em cũng đang nhầm lẫn giống tôi. Bài viết này sẽ nằm trong phạm vi của Angular nhé anh em.

Cách đây một hai hôm, tôi có ngồi nói chuyện với 1 người anh đồng nghiệp, câu chuyện cũng chẳng có gì đặc biệt cho đến khi nói về Data Binding. Ông anh tôi có hỏi là:

"Anh em có biết sự khác nhau giữa Binding 2 chiều và 1 chiều không?"

Tôi vẫn hồn nhiên đáp ☺️:

"Dễ mà anh, 1-way binding ([property]) là truyền một chiều từ Cha xuống Con. Còn 2-way binding ([(property)]) là đồng bộ hai chiều, con đổi thì cha đổi và ngược lại."

Ông anh cười mỉm, hỏi bồi thêm một câu:

Nếu ví dụ anh truyền vào component con một object thì sao

1. Sai lầm thường gặp (Pha sập hầm đầu đời)

Thật sự với anh em là ông anh tôi hỏi câu này phát là tôi ngớ người ngay. Mà tính tôi thấy ngượng là phải thử ngay, phải tìm hiểu liền.
Bắt đầu với project mới, Tôi có 2 component là classstudent:

class.html

<div style="border: 1px solid green; padding: 10px; margin: 10px;">
  <h3>Parent Component (Class)</h3>
  <p>Student Info in Parent: {{ studentObj.name }} - {{ studentObj.age }}</p>
  <student [studentInfo]="studentObj"></student>
</div>

class.ts

@Component({
  selector: 'class',
  imports: [Student],
  templateUrl: './class.html',
  styleUrl: './class.css',
})
export class Class {
  studentObj = {
    name: 'Nguyen Van A',
    age: 20
  };
}

student.html

<div style="border: 1px solid blue; padding: 10px; margin-top: 10px;">
  <h4>Child Component (Student)</h4>
  <p>Student Info in Child: {{ studentInfo.name }} - {{ studentInfo.age }}</p>
  <button (click)="changeData()">Change Property in Child</button>
</div>

student.ts

@Component({
  selector: 'student',
  templateUrl: './student.html',
  styleUrl: './student.css',
})
export class Student {
  @Input() studentInfo: any;
  changeData() {
    if (this.studentInfo) {
      this.studentInfo.name = 'Manh Le';
      this.studentInfo.age = 29;
    }
  }
}

image.png

Sau khi click Change Property in Child thì : BÙM💥💥💥 image.png

Cả thông tin Object của Component Cha cũng được cập nhật luôn! Rõ ràng tôi đang sử dụng Binding 1 chiều cơ mà? Tôi không can tâm nên quyết thử một cách khác. Vì tôi đang nghĩ đến Reference Type của Js. Ở cách trên thì tôi đang gán trực tiếp giá trị vào thuộc tính, giờ tôi thử tạo hẳn một object mới tinh bên trong Component Con xem thế nào:

student.ts (sửa lại)

export class Student {
  @Input() studentInfo: any;
  changeData() {
    if (this.studentInfo) {
      //sửa đoạn này
      this.studentInfo = {...this.studentInfo, name: 'Manh Le', age: 29}
    }
  }
}

Và kết quả chuẩn luôn:
Dữ liệu bên Con đổi, nhưng bên Cha vẫn đứng im! image.png

2. Khoảnh khắc "Aha!": Kẻ thao túng tâm lý mang tên "JavaScript"

Khi truyền một Object từ Component Cha xuống Component Con, thực chất JS chỉ phát cho cả 2 ông chung 1 chiếc chìa khoá để mở vào chung một ô nhớ.

  • Lúc tôi gán thẳng cho property (this.studentInfo.name = ...): Bản chất là component Con lấy chìa khóa, chui vào ô nhớ đó sửa đổi. Component Cha đứng ở ngoài, nhìn vào cái ô nhớ dùng chung đó dĩ nhiên cũng thấy sự thay đổi. Mình đã bị "ảo giác" đó là 2-way binding!
  • Lúc tôi tạo object mới (this.studentInfo = { ... }): Tôi đã tạo ra một ô nhớ hoàn toàn mới và component Con ngắt luôn liên kết với ô nhớ cũ. Nhưng component Cha thì không hề hay biết, nó vẫn cầm chìa khóa cũ đứng ngó vào ngôi nhà cũ.

Thế rốt cuộc 2-way Binding để làm gì ???

Chính là để giải quyết bài toán số 2 ở trên! Nó đảm bảo dù Component Con có đập đi xây "nhà mới" (tạo Object mới), thì nó vẫn sẽ báo cho Component Cha biết để lấy chìa khóa nhà mới.
Giờ mình thử sửa lại theo cơ chế của 2 way binding:

(Quy tắc sống còn ở đây là: Tên Output BẮT BUỘC phải là [Tên Input] + "Change". Sai một chữ là nghỉ chạy!)

class.html

<div style="border: 1px solid green; padding: 10px; margin: 10px;">
  <h3>Parent Component (Class)</h3>
  <p>Student Info in Parent: {{ studentObj.name }} - {{ studentObj.age }}</p>
  <student [(studentInfo)]="studentObj"></student> //Sửa dòng này
</div>

student.ts

export class Student {
  @Input() studentInfo: any;
  //Thêm dòng này
  @Output() studentInfoChange = new EventEmitter<any>();
  
  changeData() {
    if (this.studentInfo) {
      this.studentInfo = {...this.studentInfo, name: 'Manh Le', age: 29}
      //Thêm dòng này
      this.studentInfoChange.emit(this.studentInfo);
    }
  }
}

Và kết quả chuẩn luôn => Khi thay đổi ở component Component con thì Component Cha cũng cập nhật theo : image.png

3. Bóc trần cú pháp "Quả chuối trong hộp" (Banana in box) [()]

Theo những kiến thức tôi tìm hiểu trên mạng thì cú pháp:

<student [(studentInfo)]="studentObj"></student>

Thực chất tương đương với cách viết tường minh sau:

<student [studentInfo]="studentObj" (studentInfoChange)="studentObj = $event"></student>

=> Và quả nhiên khi để viết rõ ra như này, kết quả chạy cũng y hệt nhau.
Vậy là KhÔNG CÓ PHÁP THUẬT NÀO Ở ĐÂY CẢ. Cách viết [()] ở trên chỉ là "sugar syntax" giúp code gọn hơn thôi.
Hóa ra, [studentInfo]="studentObj" là để truyền data xuống (Input). Còn (studentInfoChange) là một cái Event bắn data mới lên Cha (Output). Rất rành mạch: Data đi xuống, Event đi lên. Hoàn toàn không dính líu gì đến việc chung vùng nhớ của JavaScript.

Một ví dụ khác để anh em thấy rõ hơn cơ chế 2 way binding của Angular
Trong class.html tôi khai báo thêm biến someText và trong class.html tôi thêm đoạn này:

class.html

<div style="border: 1px solid green; padding: 10px; margin: 10px;">
  <h3>Parent Component (Class)</h3>
  
  <div>
    <h4>Ví dụ với thẻ Input (Dùng ngModel có sẵn)</h4>
    <label>Nhập text: </label>
    <input [(ngModel)]="someText">
    <p>Giá trị hiện tại của biến someText: <strong>{{ someText }}</strong></p>
  </div>

  <hr>

  <h4>2. Ví dụ với Custom Component (Phải tự viết Output)</h4>
  <p>Student Info in Parent: {{ studentObj.name }} - {{ studentObj.age }}</p>
  <student [(studentInfo)]="studentObj"></student>
</div>

Khi anh em thay đổi text ở input, biến someText lập tức cập nhật theo (và ngược lại) vì bản thân ngModel (do đội ngũ Angular viết sẵn) đã tự xử lý cái @Output() ngModelChange ngầm ở bên dưới cho chúng ta rồi.
Còn ở student.ts là do chính anh em tự tạo ra, Angular không hề biết bên trong component student của bạn có dữ liệu gì, hoạt động như thế nào, và khi nào thì bạn muốn báo dữ liệu thay đổi.

4. Kết luận

  1. Binding 2 chiều không có gì quá vi diệu, nó cũng chỉ là sự tuân thủ những quy tắc về Data và Event. Nguyên lý muôn thuở: Data đi xuống thì Event đi lên.
  2. Rất cảm ơn anh em đã đọc bài viết của tôi. Đây là những kiến thức mà tôi qua quá trình trải nghiệm và viết nên
  3. Mong rằng bài viết này sẽ giúp anh em hiểu hơn về Data Binding trong Angular (vì tôi dev angular mà 😁). Từ đó kiểm soát luồng dữ liệu tốt hơn và cải thiện chất lượng code.
  4. Tôi vốn là một dev BE tiến thân lên Fullstack nên bài viết về FE chắc chắn còn nhiều thiếu sót. Rất mong được anh em góp ý nhiệt tình để các bài viết sau được chất lượng cao hơn.

Chúc anh em code không bug! 🚀


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí