+3

Tự code lại JavaScript Promise từ đầu

Hôm nay mình đã thử tự code lại Promise trong JavaScript từ đầu theo chuẩn Promises/A+ để kiểm tra khả năng của mình. Sẵn tiện mình viết bài này chia sẻ cách làm cho những bạn quan tâm. Bắt đầu luôn nha.

Hãy sẽ gọi Promise của chúng ta là NotNow. Những cái tên bắt tai như will, future, later, v.v. đã bị dùng bởi mấy package khác rồi.

Khởi đầu đơn giản thôi.

const PENDING = "pending";
const FULFILLED = "fulfilled";

class NotNow {
  #state = PENDING;
  #onFulfilleds = [];
  #value;

  constructor(fn) {
    fn(this.#fulfill.bind(this));
  }

  #fulfill(value) {
    this.#state = FULFILLED;
    this.#value = value;
    this.#onFulfilleds.forEach((fn) => fn(value));
  }

  then(onFulfilled) {
    this.#addOnFulfilled(onFulfilled);
  }

  #addOnFulfilled(onFulfilled) {
    if (this.#state === PENDING) {
      this.#onFulfilleds.push(onFulfilled);
    } else if (this.#state === FULFILLED) {
      onFulfilled(this.#value);
    }
  }
}

constructor sẽ thực thi function bạn truyền vào, với phương thức #fulfill là callback để bạn trả về kết quả.

Phương thức #fulfill lưu lại giá trị trả về và kích hoạt các callback với giá trị đó.

Phương thức then thêm callback để thực thi khi promise nhận được giá trị trả về. Bên trong phương thức này gọi phương thức #addOnFulfilled.

Phương thức #addOnFulfilled, nếu promise chưa nhận được giá trị sẽ thêm callback vào để thực thi khi promise nhận được giá trị, còn nếu promise đã có giá trị thì thực thi callback luôn.

Test thử nào.

const notNow = new NotNow((fulfill) => {
  setTimeout(() => fulfill(2), 2000);
});
notNow.then((value) => console.log(`callback 1, fulfilled with ${value}`));
notNow.then((value) => console.log(`callback 2, fulfilled with ${value}`));

Sau 2 giây, màn hình sẽ in ra:

callback 1, fulfilled with 2
callback 2, fulfilled with 2

Ok. Bước đầu tạm ổn.

Thực tế thì các callback không được thực thi một cách đồng bộ ngay khi có kết quả trả về, mà sẽ thực thi bất đồng bộ. Điều này để đảm bảo tính nhất quán: cho dù promise có nhận được giá trị một cách đồng bộ, thì các callback vẫn được thực thi một cách bất đồng bộ. Bạn có thể dùng setTimeout hay queueMicrotask đều được. Mình sẽ dùng queueMicrotask để các callback được thực thi sớm hơn.

Sửa lại phương thức #fulfill:

// this.#onFulfilleds.forEach((fn) => fn(value));
queueMicrotask(() => this.#onFulfilleds.forEach((fn) => fn(value)));

Sửa lại phương thức #addOnFulfilled:

// onFulfilled(this.#result);
queueMicrotask(() => onFulfilled(this.#value));

Mỗi khi thêm callback cho promise, thực ra chúng ta đang tạo ra một promise mới. Promise mới này chính là kết quả khi thực thi callback. Chúng ta sau đó có thể truyền promise mới này đến những nơi khác trong chương trình để có thể thêm các callback tiếp tục xử lý kết quả của callback hiện tại.

Vậy thì trong phương thức then, chúng ta sẽ tạo ra một promise mới. Khi promise hiện tại nhận được giá trị, chúng ta thực thi callback, rồi trả về cho promise mới kết quả của callback.

class NotNow {
  // ...
  then(onFulfilled) {
    return new Promise((nextFulfill) => {
      this.#addOnFulfilled((value) => {
        const nextValue = onFulfilled(value);
        nextFulfill(nextValue);
      });
    });
  }
  // ...
}

Test thử luôn.

const a = new NotNow((fulfill) => {
  setTimeout(() => fulfill(2), 2000);
});
const b = a.then((value) => value * 2);
b.then((value) => console.log(value));

a sẽ nhận được giá trị 2, rồi b sẽ nhận được giá trị 2 * 2 là 4. Vậy 4 sẽ được in ra màn hình.

Chúng ta có thể gọi nối tiếp then:

new NotNow((fulfill) => {
  setTimeout(() => fulfill(2), 2000);
})
  .then((value) => value * 2)
  .then((value) => console.log(value));

Nối thêm nhiều lần cũng được luôn:

new NotNow((fulfill) => {
  setTimeout(() => fulfill(2), 2000);
})
  .then((value) => value * 2)
  .then((value) => value * 2)
  .then((value) => value * 2);
  .then((value) => console.log(value));

Ahh, method chaining nhìn cứ thấy dễ chịu nhỉ.

Một đặc tính của promise là khi giá trị trả về cho promise cũng là một promise (là cũng có phương thức then), thì promise hiện tại sẽ cố nhận lấy giá trị trả về của promise đó luôn. Ví dụ:

const a = new Promise((fulfill) => fulfill(2));
const b = new Promise((fulfill) => fulfill(a));
b.then((value) => console.log(value));

Kết quả in ra sẽ là:

2

Mặc dù chúng ta đã trả cho b giá trị là a, nó vẫn cố nhận lấy luôn giá trị trả về của a là 2, bởi vì a là một promise.

Kết quả cũng vẫn vậy dù có có lồng qua nhiều lớp promise:

const a = new Promise((fulfill) => fulfill(2));
const b = new Promise((fulfill) => fulfill(a));
const c = new Promise((fulfill) => fulfill(b));
const d = new Promise((fulfill) => fulfill(c));
d.then((value) => console.log(value));

Kết quả:

2

Để làm được điều này, khi nhận được giá trị trong phương thức #fulfill, kiểm tra nếu giá trị đó là promise thì chúng ta không thực thi các callback ngay, mà đợi cho đến khi promise đó cũng nhận được giá trị luôn, bằng cách thêm #fulfill vào promise đó như một callback.

class NotNow {
  //...
  #fulfill(value) {
    if (typeof value?.then === "function") {
      return value.then(this.#fulfill);
    }
    this.#state = FULFILLED;
    this.#value = value;
    queueMicrotask(() => this.#onFulfilleds.forEach((fn) => fn(value)));
  }
  //...
}

Lần tiếp theo khi #fulfill được gọi, nó cũng thực thi tương tự, nên nó hoạt động một cách đệ quy tới khi giá trị nhận được không phải là promise.

Kiểm tra luôn:

const a = new Promise((fulfill) => fulfill(2));
const b = new Promise((fulfill) => fulfill(a));
const c = new Promise((fulfill) => fulfill(b));
const d = new Promise((fulfill) => fulfill(c));
d.then((value) => console.log(value));

Kết quả:

2

Chính xác!

Có thể dùng async/await với NotNow không? Tất nhiên rồi! Giá trị được awaitchỉ cần hoạt động như một promise, class thực sự của nó không phải thành vấn đề.

const a = await new NotNow((fulfill) => {
  setTimeout(() => fulfill(5), 2000);
});
console.log(a);

Đại khái là vậy. Trong bài viết này mình đã tạm bỏ qua xử lý lỗi cho đơn giản, để dễ tập trung vào các vấn đề cốt lõi. Hãy tham khảo phiên bản đầy đủ trong source code. Code đã pass hoàn toàn bộ test của Promises/A+. Ngoài ra trong đó còn có các phương thức bổ trợ như all, allSettled, any, race.

Mong là bài viết hữu ích. Hãy thoải mái chia sẻ suy nghĩ, góp ý, hoặc cả cách làm của bạn, ở phần bình luận.


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í