Phần I, chúng ta đã đi qua về cách cài đặt và sử dụng redux-saga trong ứng dụng reactJS. Bài này mình sẽ đi qua về các khái niệm cơ bản khi sử dụng redux-saga mình phải nắm được để làm việc được tốt hơn.
- Hiệu ứng (Effects) trong redux-saga
- Phân biệt watcher, worker trong redux-saga
- Sử dụng 1 số effects hay dùng (takeEvery, takeLatest, take, delay…)
Hiệu ứng (Effect) trong redux-saga
Về cơ bản thì redux-saga được xây dựng dựa trên nền là Generator function. Nên việc thực thi các hàm trong redux-saga, sẽ là chạy nhiều lần, chạy -> dừng -> chạy tiếp. Các hiệu ứng trong redux-saga, bạn cứ hình dung nó là các hàm hổ trợ được redux-saga xây dựng sẵn để giúp mình thao tác khi làm việc với Generator function thuận tiện hơn, tất cả hiệu ứng nằm trong đường dẫn ‘redux-saga/effects‘. Các bạn có thể sử dụng bằng cách import vào
1 |
import { take, takeLatest, takeEvery, call, put, select, delay } from 'redux-saga/effects' |
Các effects trong redux-saga, ngắn gọn lại theo cách hiểu đơn giản nhất là nó là những hàm hổ trợ được gói trong package redux-saga/effects. Dựa vào effect đã cung cấp mà mình có thể làm được những công việc sau trong ứng dụng
- take: chờ đợi 1 action được gởi tới, nếu đúng hợp lệ thì sẽ qua dòng lệnh kế tiếp
- takeLatest: nó cũng giống take là chờ đợi action gởi tới, kiểm tra có khớp với mẫu đã khai báo không. Nhưng nó không như take là chỉ chờ và chạy tiếp dòng tiếp theo, mà khi có action gởi tới khớp được, nó sẽ gởi yêu cầu này cho worker để xử lý (latest ở đây có nghĩa là gì, nếu nhiều action được gởi tới đồng thời, thì nó chỉ nhận 1 action cuối cùng và gởi action này cho worker xử lý, hủy tất cả các nhiệm vụ đã giao trước đó)
- takeLeading: cũng tương tự takeLatest, khác với công việc là nó chỉ xử lý 1 action được gởi tới đầu tiên, các action gởi tới sau sẽ không được nhận nếu nhiệm vụ này chưa hoàn thành, nên có gởi 10 action tới 1 lúc thì cũng chỉ nhận 1 cái đầu tiên để xử lý cho xong thì mới quay lại nhận việc tiếp
- call: gọi 1 hàm để xử lý, hàm này có thể là 1 Promise, 1 function bình thường hoặc 1 Generator function
- select: effect này là bộ chọn cung cấp tiện ích để chúng ta thao tác với dữ liệu được chọn từ store
- delay: cho phép delay ứng dụng lại 1 khoảng thời gian trước khi chạy đến đoạn mã tiếp theo
Cách sử dụng là thêm từ khóa yield ở phía trước mổi effect nha các bạn. Ví dụ: yield take(‘WAITING’);
Và khi xử dụng effect trong saga, mình sẽ có thêm khái niệm về blocking, và non-blocking effect (nghĩa là một hiệu ứng có làm block luồng đang chạy lại không hay là chạy đồng thời với nhiều effect khác) các bạn tham khảo thêm mô tả hiệu ứng nào blocking, non blocking nhé
Phân biệt watcher, worker trong redux-saga
Khi sử dụng redux-saga, mình sẽ đụng tới khái niệm watcher, worker. Tên của nó cũng đã mang theo cái nghĩa của nó luôn rồi. Các bạn chỉ cần hiểu đơn giản, để chạy được saga thì trong file định nghĩa các saga, mình sẽ tạo ra 1 ông là watcher (người xem hay ông chủ) và 1 ông là worker (công nhân), cả 2 ông này đều là Generator function nhé các bạn. Một watcher sẽ quản lý 1 hoặc nhiều worker nhé các bạn
Ông watcher sẽ đợi công việc được gởi tới, tương ứng với mổi việc nhận được là gì, thì sẽ giao việc lại cho ông worker mà mình quản lý làm thì giao việc cho worker (công nhân) làm việc :). Cách khai báo như thế nào, mình xem ví dụ bên dưới:
1 2 3 4 5 6 7 8 9 10 |
function* loginWorker() { console.log('Tôi nhận việc nhé'); // xử lý login console.log('Tôi đã xử lý xong') } export function* loginWatcher() { // effect được sử dụng ở đây là takeLatest yield takeLatest('XU_LY_LOGIN_GIUM_MINH_VOI' /* action type */, loginWorker /* công nhân nhận việc login */ ); } |
Ở ví dụ trên mình có khai báo 1 watcher là loginWatcher và 1 worker là loginWorker
Nhiệm vụ của watcher ở đây là quan sát có công việc (action) nào được gởi tới với lời nhắn (type) là ‘XU_LY_LOGIN_GIUM_MINH_VOI‘, khi có action thỏa điều kiện này thì ông watcher này sẽ đưa nhiệm vụ cho ông worker để xử lý công việc login. Việc còn lại là ông worker sẽ xử lý login và làm gì đó tiếp theo 🙂
Tùy theo cách quản lý source code của mổi người, mà chúng ta có thể khai báo saga tập trung theo chức năng hoặc khai báo riêng lẻ. Ví dụ như tất cả các xử lý liên qua tới auth mình khai báo luôn cho 1 watcher xử lý, và giao nhiệm vụ xử lý cho nhiều ông worker được quản lý. Xét ví dụ sau:
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 |
function* loginWorker() { console.log('Tôi nhận việc nhé'); // xử lý login console.log('Tôi đã xử lý xong') } function* registerWorker() { console.log('Tôi nhận việc nhé'); // xử lý register console.log('Tôi đã xử lý xong') } function* logoutWorker() { console.log('Tôi nhận việc nhé'); // xử lý logout console.log('Tôi đã xử lý xong') } export function authWatcher*() { // các effect được sử dụng ở đây là takeLatest yield takeLatest('XU_LY_LOGIN_GIUM_MINH_VOI' /* action type */, loginWorker /* công nhân nhận việc login */ ); yield takeLatest('XU_LY_REGISTER_GIUM_MINH_VOI' /* action type */, registerWorker /* công nhân nhận việc register */ ); yield takeLatest('XU_LY_LOGOUT_GIUM_MINH_VOI' /* action type */, logoutWorker /* công nhân nhận việc logout */ ); } |
Ở ví dụ trên thì 1 ông chủ là authWatcher quản lý tới 3 công nhân là loginWorker, registerWorker, logoutWorker
Tương tự, mình cũng sẽ có thêm các watcher khác như productWatcher, cartWatcher… chẳng hạn
Sử dụng 1 số effects hay dùng
Thường thì trong redux-saga các bạn sẽ hay dùng nhất là takeLatest, takeEvery, take, put, call, select. Một số khác thì có chức năng nâng cao hơn là race, cancel, fork, spawn…, mình chỉ mô tả 1 số thường gặp còn 1 số khác các bạn có thể hỏi thêm hoặc coi thêm trên official document nhé
Căn bản thì takeLatest, takeLeading nó cũng là một hàm hổ trợ wrap các hàm khác mà ra như fork, take, cancel, call. Xem mã bên dưới để hiểu được takeLatest và takeLeading là việc như thế nào
1 2 3 4 5 6 7 8 9 10 |
const takeLatest = (patternOrChannel, saga, ...args) => fork(function*() { let lastTask while (true) { const action = yield take(patternOrChannel) if (lastTask) { yield cancel(lastTask) // cancel is no-op if the task has already terminated } lastTask = yield fork(saga, ...args.concat(action)) } }) |
takeLatest thì được xây dựng từ fork, take, cancel
Mình giải thích cơ chế của đoạn mã, ở đây, ta có 1 hàm là takeLatest. Khi sử dụng cú pháp khai báo là
1 |
yield takeLatest('ACTION_TYPE', worker1); |
Thì việc này tạo ra cho ta 1 nhiệm vụ chạy nền (background task) ứng với đoạn mã là
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fork(function*() { let lastTask while (true) { // quan sát action gởi tới có khớp không const action = yield take(patternOrChannel) // kiếm tra nhiệm vụ trước đó đã hoàn thành chưa if (lastTask) { // nếu chưa hoàn thành thì hủy nhiệm vụ đã gọi trước yield cancel(lastTask) // cancel is no-op if the task has already terminated } // tạo nhiệm vụ nền để xử lý và lưu lại để cho lần kiếm tra tiếp theo lastTask = yield fork(saga, ...args.concat(action)) } }) |
Nhiệm vụ nền này là 1 vòng lặp liên tục theo cơ chế: lắng nghe có action gởi tới phù hợp -> kiểm tra có nhiệm vụ nền nào được tạo ra trước đó mà chưa hoàn thành ( nếu tốn tại thì hủy nhiệm vụ này) -> tạo nhiệm vụ nền xử lý tiếp
1 2 3 4 5 6 |
const takeLeading = (patternOrChannel, saga, ...args) => fork(function*() { while (true) { const action = yield take(patternOrChannel); yield call(saga, ...args.concat(action)); } }) |
takeLeading thì từ fork, take, call
Dựa vào mô tả chi tiết của 2 đoạn mã trên về hiệu ứng takeLatest và takeLeading, chúng ta có thể hiểu rõ hơn về bản chất khi làm việc với redux-saga. Về cơ bản thì takeLatest và takeLeading mới khai báo thì nó sẽ tạo ta task nền con tương ứng với hàm fork(function*() {}), và trong task nền con này nó sẽ xử lý việc lắng nghe action nào khớp và tiếp tục tạo ra nhiều nhiệm vụ con nhỏ hơn (takeLatest->fork) hoặc 1 nhiệm vụ (takeLeading – > call). Vì quá trình xử lý được loop liên tục bởi vòng lặp while(true), nên cứ mổi khí có hành động nào được gởi tới khớp thì lại bắt đầu tiến trình xử lý lại, cứ tiếp diễn liên tục trong vòng đời của ứng dụng