1. Một số điểm lưu ý

Mình sẽ đưa ra một số điểm lưu ý khi sử dụng generator function và giải thích ở phía dưới.

  • Hàm generator trả về một iterator có số lần lặp bằng với số từ khoá yield trong hàm.
  • Khi gọi hàm next thì code trong hàm generator sẽ được thực thi cho đến khi gặp từ khoá yield và dừng lại chờ cho lời gọi next tiếp theo.
  • Khi gọi hàm next thì giá trị trả về của nó là 1 object có dạng {value: any, done: boolean} .
  • Nhìn từ trong hàm generator thì hàm next luôn dừng lại ở yield (không thực thi phần phía sau yield), nhưng nhìn từ ngoài hàm (nơi gọi hàm next) thì giá trị trả về của hàm next là một object có value chính là giá trị thực thi của phần phía sau yield đang được dừng lại đó.
  • Hàm next có param truyền vào nó (next(param)) thì param chính là giá trị thay thế của yield đang bị dừng lại trong hàm generator. Do đó, hàm next đầu tiên dù có truyền param vào cũng bị bỏ qua bởi vì đối với hàm next đầu tiên, vẫn chưa có yield nào đang bị dừng lại. Nghĩa là đối với lời gọi next đầu tiên thì next() = next(undefined) = next(null) = next(true) = next(100) = next("any string") = next([]) = next({key: "value"}) .
  • Khi sử dụng phép gán yield cho một biến hay hằng số nào đó trong hàm generator thì phải truyền param vào hàm next (không áp dụng cho next đầu tiên), nếu không biến đó sẽ có giá trị là undefined.

 

2. Ví dụ và giải thích

  • Hàm generator trả về một iterator có số lần lặp bằng với số từ khoá yield trong hàm.
function* testGen() {
  yield 100;
  yield 101;
  yield 102;
}

const gen: Generator = testGen();
for (let i of gen) {
   console.log(i);
}

// === Result === //
// 100
// 101
// 102

 

  • Khi gọi hàm next thì code trong hàm generator sẽ được thực thi cho đến khi gặp từ khoá yield và dừng lại chờ cho lời gọi next tiếp theo

VD:

function* testGen(i: number) {
  console.log("inside i", i);
  yield 100;
  console.log("inside end");
}

Gọi next một lần:

const gen: Generator = testGen(10);
gen.next();

// === Result === //
// inside i 10

Gọi next 2 lần:

const gen: Generator = testGen(10);
gen.next();
gen.next();

// === Result === //
// inside i 10
// inside end

 

  • Khi gọi hàm next thì giá trị trả về của nó là 1 object có dạng {value: any, done: boolean}  
function* testGen() {
  yield 100;
}

const gen: Generator = testGen();
console.log("outside next 1st", gen.next());
console.log("outside next 2nd", gen.next());

// === Result === //
// outside next 1st { value: 100, done: false }
// outside next 2nd { value: undefined, done: true }

 

  • Nhìn từ trong hàm generator thì hàm next luôn dừng lại ở yield (không thực thi phần phía sau yield), nhưng nhìn từ ngoài hàm (nơi gọi hàm next) thì giá trị trả về của hàm next là một object có value chính là giá trị thực thi của phần phía sau yield đang được dừng lại đó.
let i = 0, j = 0;

function* testGen() {
  console.log("inside j", j);
  i = ++j + (yield 100);
  console.log("inside end", i);
}

const gen: Generator = testGen();

console.log("outside next 1st", gen.next());
console.log("outside i", i);
console.log("outside j", j);

// === Result === //
// inside j 0
// outside next 1st { value: 100, done: false }
// outside i 0
// outside j 1

Nhìn từ trong hàm generator testGen thì next đang được dừng lại ngay trước yield 100, nghĩa là câu lệnh ++j đã được thực thi, ở đây hằng số i vẫn chưa được gán và dòng inside end vẫn chưa được thực thi. Vì thế ta mới có outside i 0 và outside j 1 .

Nhìn từ ngoài hàm testGen (nơi gọi hàm next) thì giá trị trả về của hàm next là một object có value chính là giá trị thực thi của phần phía sau yield đang được dừng lại đó, nghĩa là {value: 100, done: false} .

 

  • Hàm next có param truyền vào nó (next(param)) thì param chính là giá trị thay thế của yield đang bị dừng lại trong hàm generator. Do đó, hàm next đầu tiên dù có truyền param vào cũng bị bỏ qua bởi vì đối với hàm next đầu tiên, vẫn chưa có yield nào đang bị dừng lại. Nghĩa là đối với lời gọi next đầu tiên thì next() = next(undefined) = next(null) = next(true) = next(100) = next("any string") = next([]) = next({key: "value"}) .

Hàm next đầu tiên luôn trả về cùng giá trị dù có truyền param vào hay không:

function* testGen() {
  const i = 5 * (yield 100);
  console.log("insdie i", i);
  const j = yield (2 * i) / 4;
  console.log("inside end");
}

const gen: Generator = testGen();
console.log("outside first next without param", gen.next());

const other: Generator = testGen();
console.log("outside first next with any param", other.next(500));

// === Result === //
// outside first next without param { value: 100, done: false }
// outside first next with any param { value: 100, done: false }

 

Hàm next có param truyền vào nó (next(param)) thì param chính là giá trị thay thế của yield đang bị dừng lại trong hàm generator.

function* testGen() {
  const i = 5 * (yield 100);
  console.log("inside i", i);
  const j = yield (2 * i) / 4;
  console.log("inside j", j);
  const k = i + (yield j);
  console.log("inside k", k);
}

const gen: Generator = testGen();
console.log("outside next 1st", gen.next());
console.log("outside next 2nd", gen.next(20));
console.log("outside next 3rd", gen.next(10));

// === Result === //
// outside next 1st { value: 100, done: false }
// inside i 100
// outside next 2nd { value: 50, done: false }
// inside j 10
// outside next 3rd { value: 10, done: false }

Ở VD trên:

    Lời gọi next() đầu tiên sẽ trả về value: 100 và dừng lại ở trước yield 100.

    Lời gọi next(20) thứ hai sẽ thay thế yield 100 (nơi đang bị dừng lại sau khi gọi next() đầu tiên) bằng số 20 truyền vào, nghĩa là const i = 5 * (yield 100) sẽ biến thành const i = 5 * 20, và hằng số i lúc này sẽ có giá trị là i = 5 * 20 = 100. Giá trị trả về của next(20) có value(2*i)/4 = (2*100)/4 = 50 và dừng lại trước yield (2*i)/4.

    Lời gọi next(10) thứ ba sẽ thay thế const j = yield (2*i)/4 (nơi đang bị dừng lại sau khi gọi next() thứ hai) bằng const j = 10. Giá trị trả về của next(10) này sẽ có valuej = 10 và dừng lại trước yield j.

 

  • Khi sử dụng phép gán yield cho một biến hay hằng số nào đó trong hàm generator thì phải truyền param vào hàm next (không áp dụng cho next đầu tiên), nếu không biến đó sẽ có giá trị là undefined.

param truyền vào next sẽ thay thế cho yield đang bị dừng lại, nên để phép gán cho yield đó được đúng thì phải truyền param vào, nếu mặc định gọi next() sẽ tương đương với gọi next(undefined).

function* testGen() {
  const i = 5 * (yield 100);
  console.log("inside i", i);
  const j = yield (2 * i) / 4;
  console.log("inside j", j);
}

const gen: Generator = testGen();
console.log("outside next 1st", gen.next());
console.log("outside next 2nd", gen.next());  // === gen.next(undefined)

// === Result === //
// outside next 1st { value: 100, done: false }
// inside i NaN
// outside next 2nd { value: NaN, done: false }

Ở VD trên, vì lời gọi next thứ hai truyền undefined vào nên const i = 5 * (yield 100) sẽ bị thay thế bằng const i = 5 * undefined.

Do đó, i sẽ ra kết quả là NaN và value trả về của yield (2*i)/4 cũng là NaN.

 

  • yield được truyền vào như là một argument của một hàm.
function* testGen() {
  const i = testFunc(yield 100);
  console.log("inside i", i);
}

function testFunc(param: any) {
  return 10 * param;
}

const gen: Generator = testGen();
console.log("outside next 1st", gen.next());
gen.next(20);


// === Result === //
// outside next 1st { value: 100, done: false }  <== gen.next()
// inside i 200   <== gen.next(20)

Hàm testFunc sẽ không được gọi ở next() đầu tiên vì code đang bị dừng lại ở yield đầu tiên. Hàm đó chỉ được gọi ở next(20) tiếp theo.

 

Link tham khảo:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*

https://medium.com/dailyjs/a-simple-guide-to-understanding-javascript-es6-generators-d1c350551950