Để ngăn chặn Callback Hell mà không sử dụng Promises, async/await, hoặc generators, bạn có thể áp dụng một số kỹ thuật sau đây:

1. Sử dụng các hàm riêng biệt

Thay vì lồng ghép các callback vào nhau, bạn có thể tách chúng thành các hàm riêng biệt. Điều này giúp mã dễ đọc hơn và tránh được độ lồng ghép phức tạp.

function firstStep(callback) {
    // Thực hiện công việc đầu tiên
    console.log("Bước 1 hoàn thành");
    callback();
}

function secondStep(callback) {
    // Thực hiện công việc thứ hai
    console.log("Bước 2 hoàn thành");
    callback();
}

function thirdStep() {
    console.log("Bước 3 hoàn thành");
}

// Thực hiện các bước theo thứ tự
firstStep(() => {
    secondStep(() => {
        thirdStep();
    });
});

2. Sử dụng các hàm gọi lại có tên

Sử dụng hàm gọi lại có tên (named callbacks) thay vì hàm ẩn danh sẽ làm rõ hơn về cấu trúc mã và mục đích của từng callback.

function handleFirstStep() {
    console.log("Bước 1 hoàn thành");
    handleSecondStep();
}

function handleSecondStep() {
    console.log("Bước 2 hoàn thành");
    handleThirdStep();
}

function handleThirdStep() {
    console.log("Bước 3 hoàn thành");
}

// Bắt đầu quá trình
handleFirstStep();

3. Sử dụng Object để quản lý trạng thái

Bạn có thể sử dụng một object để lưu trữ trạng thái của các callback, giúp bạn quản lý luồng thực thi một cách rõ ràng hơn.

const steps = {
    step1: function(callback) {
        console.log("Bước 1 hoàn thành");
        callback();
    },
    step2: function(callback) {
        console.log("Bước 2 hoàn thành");
        callback();
    },
    step3: function() {
        console.log("Bước 3 hoàn thành");
    },
};

// Thực hiện các bước
steps.step1(() => {
    steps.step2(() => {
        steps.step3();
    });
});

4. Sử dụng Queue (Hàng đợi)

Tạo một hàng đợi cho các tác vụ mà bạn muốn thực hiện tuần tự. Điều này sẽ giúp bạn dễ dàng quản lý các callback mà không bị lồng ghép.

const queue = [];

function addToQueue(fn) {
    queue.push(fn);
    processQueue();
}

function processQueue() {
    if (queue.length > 0) {
        const fn = queue.shift();
        fn();
    }
}

// Định nghĩa các bước
function step1() {
    console.log("Bước 1 hoàn thành");
    addToQueue(step2);
}

function step2() {
    console.log("Bước 2 hoàn thành");
    addToQueue(step3);
}

function step3() {
    console.log("Bước 3 hoàn thành");
}

// Bắt đầu quy trình
addToQueue(step1);

5. Sử dụng EventEmitter

Sử dụng mô hình sự kiện để quản lý luồng thực thi thay vì callback. Sự kiện có thể giúp bạn dễ dàng quản lý nhiều tác vụ mà không cần phải lồng ghép.

const EventEmitter = require('events');
const eventEmitter = new EventEmitter();

eventEmitter.on('step1', () => {
    console.log("Bước 1 hoàn thành");
    eventEmitter.emit('step2');
});

eventEmitter.on('step2', () => {
    console.log("Bước 2 hoàn thành");
    eventEmitter.emit('step3');
});

eventEmitter.on('step3', () => {
    console.log("Bước 3 hoàn thành");
});

// Bắt đầu quy trình
eventEmitter.emit('step1');

Kết luận

Sử dụng các kỹ thuật trên, bạn có thể tổ chức mã JavaScript của mình một cách hiệu quả hơn mà không cần phụ thuộc vào Promises, async/await, hoặc generators. Mỗi phương pháp đều có ưu điểm riêng, và bạn có thể chọn phương pháp phù hợp với nhu cầu và phong cách lập trình của mình.