Quản lý state trong Tini App - Cái nhìn tổng quan và cách tiếp cận (P2)
Xin chào các bạn, trong phần 1 của Quản lý state trong Tini App - Cái nhìn tổng quan và cách tiếp cận
chúng ta đã cùng nhau tìm hiểu về các vấn đề rất cơ bản nhưng rất quan trọng của một ứng dụng front-end nói chung và của một ứng dụng Tini App nói riêng. Tuy nhiên còn một vài vấn đề đang bị bỏ ngỏ.
Để gỡ những nút thắt đó, hôm nay chúng sẽ cùng nhau đi qua các nội dung:
- I. Nhắc lại vấn đề
- II. Event emitter pattern - Giải pháp hoàn hảo
- III. Disposable pattern - Bạn chỉ việc sử dụng, dọn dẹp cứ để tôi
- IV. Kết luận
I. Nhắc lại vấn đề
Ở phần trước, chúng ta đã có một “global store” chính là app - nơi mà có thể được truy cập ở bất kì đâu và bất kì khi nào thông qua phương thức getApp()
.
Tuy nhiên vẫn còn 2 vấn đề khiến app thay đổi nhưng UI vẫn không được cập nhật:
- Thứ nhất: Khi các properties ở app thay đổi, chúng không trigger event nào để báo cho các page/component sử dụng getApp() nhận biết.
- Thứ hai: Cho dù ở app có trigger event thông báo sự thay đổi đi chăng nữa thì UI cũng sẽ không được cập nhật vì hiện tại ở các page/component ấy không có phương thức hay life cycle nào lắng nghe sự thay đổi để cập nhật lại state.
Thật may mắn cho chúng ta, có 1 pattern sinh ra để giải quyết 2 vấn đề này: Event emitter.
II. Event emitter pattern - Giải pháp hoàn hảo
Nếu các bạn đã làm quen với Nodejs
thì có thể các bạn sẽ quen với pattern này. Còn nếu bạn là một front-end developer thì sao, đừng lo hãy đọc những dòng code sau:
function handleOnClick() {
// Do something
}
const button = document.getElementById("#button");
button.addEventListener("click", handleOnClick)
- Chúng ta đăng ký sự kiện
click
của mộtbutton
với callbackhandleOnClick
. - Khi bấm vào
button
, sự kiệnclick
sẽ được gọi và nó sẽ trigger callbackhandleOnClick
. - Và khi không sử dụng nữa thì ta hủy lắng nghe:
button.removeEventListener("click", handleOnClick)
Có phải nó rất quen thuộc không ?
Đây chính là một ví dụ về event emitter, để hiểu sâu hơn, hãy cùng mình cài đặt một simple event emitter
utils/event-emitter.js
class EventEmitter {
constructor() {
this.events = {};
}
}
Class EventEmitter gồm 1 property events
là một object lưu giữ tất cả các sự kiện, nó sẽ có dạng như sau:
this.events = {
'eventName': [function callback() {}],
};
Với key eventName
là tên của sự kiện và value là 1 mảng các callback
sẽ được thực thi khi sự kiện được gọi.
Vậy còn các method thì sao ? Như ví dụ trên chúng ta cần cài đặt thêm các methods sau:
- on (eventName, callback): Đăng ký lắng nghe
eventName
và sẽ thực thicallback
khieventName
được emit. - removeListener (eventName, callback): Ngược lại với
on
, method này sẽ loại bỏcallback
tương ứng vớieventName
- emit(eventName, data): Thực thi tất cả
callback
ứng vớieventName
với input làdata
Chúng ta sẽ có class EventEmitter
hoàn chỉnh như thế này:
utils/event-emitter.js
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!Array.isArray(this.events[eventName])) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName, data) {
if (Array.isArray(this.events[eventName])) {
this.events[eventName].forEach((callback) => callback(data));
}
}
removeListener(eventName, callback) {
this.events[eventName] = this.events[eventName].filter(
(listener) => listener !== callback
);
}
}
export default EventEmitter;
Còn chờ gì nữa mà không ráp vào ứng dụng của chúng ta nào:
app.js
import EventEmitter from './utils/event-emitter';
App({
cart: [],
// Thêm cartEvent
cartEvent: new EventEmitter(),
onAddToCart(product) {
this.cart.push(product);
// Emit event `cart::update` sau khi đã cập nhật giỏ hàng
this.cartEvent.emit('cart::update', this.cart);
},
});
pages/product-list/index.js
Page({
data: {
cart: [],
products: [
// ...
],
},
onReady() {
this.setData({
cart: getApp().cart,
});
},
onLoad() {
this.cartUpdate = (cart) => {
this.setData({
cart,
});
};
// Đăng ký event `cart:update` với callback `cartUpdate`
getApp().cartEvent.on('cart::update', this.cartUpdate);
},
// Và trước khi page unmount thì ta hủy lắng nghe
onUnload() {
getApp().cartEvent.removeListener('cart::update', this.cartUpdate);
},
});
Bạn có thể tham khảo code ở đây.
Và ta thu được kết quả:
Xin chúc mừng vì chúng ta đã giải quyết được 2 vấn đề còn tồn đọng cũng như đã tiếp cận được cách quản lý state cơ bản bằng app và event emitter.
Bạn có thể dừng tại đây, tuy nhiên nếu bạn muốn code của mình chuyên nghiệp hơn cũng như là để dễ bảo trì sau này thì hãy đi cùng mình đến phần tiếp theo: Disposable pattern.
III. Disposable pattern - Bạn chỉ việc sử dụng, dọn dẹp cứ để tôi
Hiện tại mọi thứ vẫn ổn, nhưng hãy tưởng tượng bạn phải lắng nghe hàng chục events thì ở life-cycle method onLoad()
bạn phải xóa bỏ từng event một.
Và nếu ứng dụng của bạn có đăng ký các timer như setInterval thì onLoad()
của bạn sẽ trông như thế này:
onUnload() {
getApp().event.removeListener('event_1', callback_1);
getApp().event.removeListener('event_2', callback_2);
// ...
getApp().event.removeListener('event_n', callback_n);
clearInterval(timer_1);
clearInterval(timer_2);
// ...
clearInterval(timer_n);
},
Rất cồng kềnh, khó bảo trì và nếu như bạn quên clear một vài event hoặc timer, nó sẽ rò rỉ bộ nhớ và ảnh hưởng tới hiệu suất ứng dụng của bạn.
Nhưng không sao, mình ở đây để giới thiệu với các bạn cách giải quyết vấn đề này: Disposable pattern.
Hiện tại chúng ta đang cài đặt các event theo các bước sau:
Đăng ký event–> Sử dụng event → Xóa bỏ event.
Nhưng bây giờ bạn hãy tưởng tượng bạn có thêm một chiếc máy (tạm gọi là cleaner) chuyên dọn dẹp các event và timer, việc của bạn là hướng dẫn nó cách dọn dẹp và nó sẽ tự động xóa bỏ chúng khi hết vòng đời của page/component.
Khi đó các bước cài đặt một event của chúng ta sẽ chuyển thành:
Đăng ký event + Hướng dẫn cho cleaner cách dọn dẹp event đó → Sử dụng event → Khi cần dọn dẹp thì cleaner sẽ làm chuyện đó giúp bạn.
Nói suông có vẻ khó hiểu, hãy bắt tay vào cài đặt nào:
pages/product-list/index.js
Page({
// Khai báo chiếc máy dọn dẹp
cleaner: [],
// ...
onLoad() {
this.cartUpdate = (cart) => {
this.setData({
cart,
});
};
getApp().cartEvent.on('cart::update', this.cartUpdate);
// Ngay khi đăng ký event, hãy hướng dẫn cleaner cách dọn dẹp nó
this.cleaner.push(
getApp().cartEvent.removeListener('cart::update', this.cartUpdate)
);
},
onUnload() {
// Và hãy để cleaner dọn dẹp thay bạn
this.cleaner.forEach((dispose) => dispose());
},
});
Tuy nhiên, chúng ta có thể cài đặt gọn hơn nữa bằng cách return về removeListener()
ngay trong method on
của EventEmitter
utils/event-emitter.js
class EventEmitter {
// ...
on(eventName, callback) {
if (!Array.isArray(this.events[eventName])) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
// Ở method on chúng ta sẽ return về luôn cách dọn dẹp event
return () => this.removeListener(eventName, callback);
}
// ...
}
Khi đó, product-list
của chúng ta sẽ gọn hơn:
pages/product-list/index.js
Page({
cleaner: [],
// ...
onLoad() {
this.cleaner.push(
getApp().cartEvent.on('cart::update', (cart) => {
this.setData({
cart,
});
})
);
},
onUnload() {
this.cleaner.forEach((dispose) => dispose());
},
});
Còn nếu sử dụng thêm các timer thì sao ?
pages/product-list/index.js
Page({
cleaner: [],
// ...
onLoad() {
const timer = setInterval(() => {
// Do something
});
this.cleaner.push(
getApp().cartEvent.on('cart::update', (cart) => {
this.setData({
cart,
});
}),
// Thêm clearInterval vào cleaner
() => clearInterval(timer)
);
},
onUnload() {
this.cleaner.forEach((dispose) => dispose());
},
});
Rất đơn giản phải không nào.
Bạn có thể tham khảo source code tại đây.
IV. Kết luận
Ở phần này chúng ta đã tìm được giải pháp Event emitter
kết hợp với Disposable
để giải quyết vấn đề re-render khi sử dụng app
và getApp()
làm global store.
Trên đây chỉ là source code đơn giản, bạn có thể tìm đọc thêm các bài viết về Event emitter
và Disposable
để bổ sung vào code của mình.
Chúng ta đã cùng nhau đi hết 2 phần của Quản lý state trong Tini App - Cái nhìn tổng quan và cách tiếp cận
.
Có thể còn rất nhiều cách để quản lý store đơn cử là dùng các thư viện bên thứ ba như Redux
, tuy nhiên mình vọng đây là bài viết đơn giản giúp các bạn làm quen và mang lại cảm hứng cho bạn cho hành trình chinh phục Tini App sắp tới.
Hẹn gặp các bạn ở các bài viết sau.