Các phương pháp giao tiếp giữa các component trong Vue.js

Props và $emit

Đây là phương pháp cơ bản và phổ biến nhất để giao tiếp giữa các component cha và con. Cơ chế props tuân theo nguyên tắc dòng dữ liệu một chiều (one-way data flow): dữ liệu chỉ có thể chảy từ component cha xuống component con. Component con không được phép trực tiếp thay đổi giá trị của props nhận được. Việc thay đổi trực tiếp sẽ phá vỡ luồng dữ liệu, khiến ứng dụng khó bảo trì. Nếu truyền kiểu dữ liệu nguyên thủy, Vue sẽ cảnh báo khi có sự cố gắng thay đổi; với kiểu dữ liệu tham chiếu (object), dù không có cảnh báo nhưng đây vẫn là anti-pattern.

Để xử lý tình huống component con cần cập nhật dữ liệu của cha, Vue cung cấp $emit. Component con sẽ phát ra một sự kiện tùy chỉnh kèm theo dữ liệu mới, và component cha sẽ lắng nghe sự kiện đó để thực thi logic cập nhật trạng thái.

Truyền dữ liệu từ Cha xuống Con

Cha truyền dữ liệu thông qua thuộc tính, con nhận thông qua định nghĩa props.

<!-- ChildComponent.vue -->
<template>
  <div>Nhận từ cha: {{ title }}</div>
</template>

<script>
export default {
  props: ["title"]
}
</script>
<!-- ParentComponent.vue -->
<template>
  <ChildComponent :title="parentTitle" />
</template>

<script>
import ChildComponent from "./ChildComponent.vue";
export default {
  components: { ChildComponent },
  data() {
    return { parentTitle: "Tiêu đề từ cha" }
  }
}
</script>

Truyền dữ liệu từ Con lên Cha

Con sử dụng $emit để gửi sự kiện, cha lắng nghe sự kiện và thay đổi dữ liệu.

<!-- ChildComponent.vue -->
<template>
  <div>
    <button @click="$emit('updateTitle', 'Tiêu đề mới từ con')">Cập nhật Cha</button>
  </div>
</template>

<script>
export default {
  props: ["title"]
}
</script>
<!-- ParentComponent.vue -->
<template>
  <ChildComponent :title="parentTitle" @updateTitle="handleUpdate" />
</template>

<script>
import ChildComponent from "./ChildComponent.vue";
export default {
  components: { ChildComponent },
  data() {
    return { parentTitle: "Tiêu đề từ cha" }
  },
  methods: {
    handleUpdate(newVal) {
      this.parentTitle = newVal;
    }
  }
}
</script>

v-model

v-model là cú pháp rút gọn (syntactic sugar) cho ràng buộc dữ liệu hai chiều, chủ yếu dùng trên các phần tử form. Bản chất nó là sự kết hợp giữa việc truyền props (mặc định là value) và lắng nghe sự kiện input để cập nhật giá trị.

<template>
  <div>
    <p>{{ searchText }}</p>
    <input :value="searchText" @input="searchText = $event.target.value" />
  </div>
</template>

<script>
export default {
  data() {
    return { searchText: "" }
  }
}
</script>

Modifier .sync

Từ Vue 2.3, .sync là một cú pháp rút gọn khác tương tự v-model, dùng để ràng buộc hai chiều trên một prop tùy chỉnh. Nó tự động chuyển đổi thành việc lắng nghe sự kiện update:propName.

<!-- ChildComponent.vue -->
<template>
  <button @click="$emit('update:isVisible', false)">Đóng</button>
</template>

<script>
export default {
  props: ["isVisible"]
}
</script>
<!-- ParentComponent.vue -->
<template>
  <!-- Cách viết đầy đủ -->
  <ChildComponent :isVisible="dialogStatus" @update:isVisible="val => dialogStatus = val" />
  
  <!-- Cách viết tắt với .sync -->
  <ChildComponent :isVisible.sync="dialogStatus" />
</template>

<script>
import ChildComponent from "./ChildComponent.vue";
export default {
  components: { ChildComponent },
  data() {
    return { dialogStatus: true }
  }
}
</script>

provide và inject

Cặp API này cho phép một component tổ tiên cung cấp dữ liệu cho tất cả các component con cháu của nó bất kể cấp bậc lồng nhau sâu bao nhiêu, tương tự Context API của React. Cha dùng provide để khai báo dữ liệu, con dùng inject để tiêu thụ. Phương pháp này thường được khuyên dùng khi phát triển thư viện hoặc plugin, không nên lạm dụng trong code ứng dụng thông thường vì làm giảm tính minh bạch của dữ liệu.

<!-- DescendantComponent.vue -->
<template>
  <div>{{ injectedConfig }}</div>
</template>

<script>
export default {
  inject: ["injectedConfig"]
}
</script>
<!-- AncestorComponent.vue -->
<template>
  <DescendantComponent />
</template>

<script>
import DescendantComponent from "./DescendantComponent.vue";
export default {
  components: { DescendantComponent },
  provide: {
    injectedConfig: { theme: "dark" }
  }
}
</script>

$attrs và $listeners

Được giới thiệu từ Vue 2.4, cặp này rất hữu ích cho việc truyền dữ liệu xuyên cấp (ví dụ A -> B -> C) mà không cần phải khai báo props trung gian ở component B. $attrs chứa các thuộc tính không được khai báo là props của component hiện tại, $listeners chứa các sự kiện v-on được gắn vào. Bằng cách v-bind="$attrs"v-on="$listeners", component B có thể chuyển tiếp toàn bộ cho component C.

<!-- GrandChildComponent.vue (C) -->
<script>
export default {
  created() {
    console.log(this.$attrs); // { dataId: 1, status: 'active' }
    console.log(this.$listeners); // { triggerAction: ƒ }
  }
}
</script>
<!-- MiddleComponent.vue (B) -->
<template>
  <GrandChildComponent v-bind="$attrs" v-on="$listeners" />
</template>

<script>
import GrandChildComponent from "./GrandChildComponent.vue";
export default {
  components: { GrandChildComponent },
  props: ["coreValue"], // Chỉ khai báo props cần dùng ở B, phần còn lại sẽ nằm trong $attrs
  inheritAttrs: false
}
</script>
<!-- TopComponent.vue (A) -->
<template>
  <MiddleComponent :coreValue="val" :dataId="1" :status="'active'" @triggerAction="handleAction" />
</template>

<script>
import MiddleComponent from "./MiddleComponent.vue";
export default {
  components: { MiddleComponent },
  data() { return { val: "Core" } },
  methods: {
    handleAction() { /* logic */ }
  }
}
</script>

$parent và $children

Hai thuộc tính này cho phép truy cập trực tiếp vào instance của component cha hoặc mảng các component con. Đây là cách giao tiếp trực quan nhưng được khuyến cáo hạn chế sử dụng vì nó tạo ra sự phụ thuộc chặt chẽ giữa các component, làm giảm tính tái sử dụng. Nhiều UI Framework cũ sử dụng cơ chế này để mô phỏng lại $dispatch$broadcast thời kỳ Vue 1.x.

<!-- ChildComponent.vue -->
<script>
export default {
  mounted() {
    console.log(this.$parent); // Truy cập instance của cha
  }
}
</script>
<!-- ParentComponent.vue -->
<script>
export default {
  mounted() {
    console.log(this.$children); // Mảng các instance con trực tiếp
  }
}
</script>

$refs

ref dùng để đăng ký tham chiếu cho phần tử DOM hoặc component con. Sau khi mounted, ta có thể truy cập trực tiếp instance hoặc node DOM thông qua this.$refs.refName. Cần lưu ý rằng $refs không phải là reactive, không nên sử dụng trong computed hay template để ràng buộc dữ liệu.

<!-- InputField.vue -->
<script>
export default {
  methods: {
    validate() { return true; }
  }
}
</script>
<!-- FormComponent.vue -->
<template>
  <InputField ref="userInput" />
</template>

<script>
import InputField from "./InputField.vue";
export default {
  components: { InputField },
  mounted() {
    this.$refs.userInput.validate(); // Gọi phương thức của con
  }
}
</script>

EventBus

Đối với các dự án quy mô nhỏ, việc sử dụng một trung tâm sự kiện (EventBus) là giải pháp linh hoạt nhất, có thể giải quyết giao tiếp giữa mọi loại component (bao gồm cả anh em). Về bản chất, đây là triển khai của mô hình Pub/Sub (Publish/Subscribe). Cần đặc biệt lưu ý phải hủy đăng ký sự kiện (off) trong hook beforeDestroy để tránh rò rỉ bộ nhớ (memory leak).

// event-bus.js
class EventBus {
  constructor() {
    this.events = {};
  }

  subscribe(event, callback) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(callback);
  }

  unsubscribe(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(cb => cb !== callback);
  }

  dispatch(event, payload) {
    if (!this.events[event]) return;
    this.events[event].forEach(cb => cb(payload));
  }
}

export default new EventBus();
<!-- ComponentA.vue -->
<script>
import bus from "./event-bus";
export default {
  data() {
    return { message: "Chưa cập nhật" }
  },
  created() {
    bus.subscribe("DATA_UPDATED", this.updateMessage);
  },
  beforeDestroy() {
    bus.unsubscribe("DATA_UPDATED", this.updateMessage);
  },
  methods: {
    updateMessage(payload) {
      this.message = payload;
    }
  }
}
</script>
<!-- ComponentB.vue -->
<script>
import bus from "./event-bus";
export default {
  methods: {
    sendData() {
      bus.dispatch("DATA_UPDATED", "Dữ liệu từ B");
    }
  }
}
</script>

Vuex

Vuex là hệ thống quản lý trạng thái tập trung dành cho các ứng dụng Vue.js quy mô lớn. Nó lưu trữ toàn bộ trạng thái của ứng dụng trong một kho (store) duy nhất, đảm bảo tính phản ứng (reactive) và có thể dự đoán được sự thay đổi thông qua các mutation. Khác với biến toàn cục thông thường, Vuex có hai đặc điểm: trạng thái là reactive, và chỉ có thể thay đổi thông qua commit mutation.

Ưu điểm của Vuex bao gồm khả năng debug bằng thời gian (time-travel), dễ dàng theo dõi luồng thay đổi dữ liệu, hỗ trợ thu thập log, và không gây ô nhiễm namespace toàn cục. Đối với các ứng dụng đơn giản, dùng Vuex có thể là phức tạp và không cần thiết, một store pattern đơn giản hoặc EventBus là đủ.

// store.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    score: 0
  },
  mutations: {
    ADD_SCORE(state, points) {
      state.score += points;
    },
    DEDUCT_SCORE(state, points) {
      state.score -= points;
    }
  }
});
<!-- ScoreBoard.vue -->
<template>
  <div>
    <p>Điểm: {{ currentScore }}</p>
    <button @click="addPoint">Cộng điểm</button>
    <button @click="deductPoint">Trừ điểm</button>
  </div>
</template>

<script>
export default {
  computed: {
    currentScore() {
      return this.$store.state.score;
    }
  },
  methods: {
    addPoint() {
      this.$store.commit("ADD_SCORE", 10);
    },
    deductPoint() {
      this.$store.commit("DEDUCT_SCORE", 5);
    }
  }
}
</script>

Thẻ: Vue.js Component Communication Vuex props eventbus

Đăng vào ngày 2 tháng 8 lúc 03:01